From 243e6cecd57034ec4d272e29b68e976b08726ca4 Mon Sep 17 00:00:00 2001 From: Simos Mikelatos Date: Sun, 14 Jun 2026 20:34:16 +0000 Subject: [PATCH 01/58] Add browser use workspace panel --- package-lock.json | 2966 ++++++++++++++++- package.json | 55 + server/index.js | 12 +- .../modules/browser-use/browser-use.routes.ts | 76 + .../browser-use/browser-use.service.ts | 345 ++ .../tests/browser-use.service.test.ts | 41 + src/components/browser-use/index.ts | 1 + .../browser-use/view/BrowserUsePanel.tsx | 233 ++ .../main-content/view/MainContent.tsx | 7 +- .../subcomponents/MainContentTabSwitcher.tsx | 4 +- .../view/subcomponents/MainContentTitle.tsx | 5 + src/hooks/useProjectsState.ts | 4 +- src/i18n/locales/de/common.json | 3 +- src/i18n/locales/en/common.json | 3 +- src/i18n/locales/it/common.json | 3 +- src/i18n/locales/ja/common.json | 3 +- src/i18n/locales/ko/common.json | 3 +- src/i18n/locales/ru/common.json | 3 +- src/i18n/locales/tr/common.json | 3 +- src/i18n/locales/zh-CN/common.json | 3 +- src/i18n/locales/zh-TW/common.json | 3 +- src/types/app.ts | 2 +- 22 files changed, 3755 insertions(+), 23 deletions(-) create mode 100644 server/modules/browser-use/browser-use.routes.ts create mode 100644 server/modules/browser-use/browser-use.service.ts create mode 100644 server/modules/browser-use/tests/browser-use.service.test.ts create mode 100644 src/components/browser-use/index.ts create mode 100644 src/components/browser-use/view/BrowserUsePanel.tsx diff --git a/package-lock.json b/package-lock.json index 3223ea93..3faa74aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,6 +88,8 @@ "auto-changelog": "^2.5.0", "autoprefixer": "^10.4.16", "concurrently": "^8.2.2", + "electron": "^38.0.0", + "electron-builder": "^26.15.3", "eslint": "^9.39.3", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-boundaries": "^6.0.2", @@ -1118,6 +1120,600 @@ "node": ">=10" } }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@electron/asar/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/fuses/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/fuses/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/rebuild": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.4.tgz", + "integrity": "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^12.2.0", + "read-binary-file-arch": "^1.0.6" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/rebuild/node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@electron/rebuild/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@electron/rebuild/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@electron/rebuild/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@electron/rebuild/node_modules/node-abi": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.31.0.tgz", + "integrity": "sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/rebuild/node_modules/node-gyp": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@electron/rebuild/node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@electron/rebuild/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@electron/rebuild/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/rebuild/node_modules/tar": { + "version": "7.5.16", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", + "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@electron/rebuild/node_modules/undici": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.26.0.tgz", + "integrity": "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/@electron/rebuild/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@electron/rebuild/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/windows-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/@emnapi/core": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", @@ -2467,6 +3063,19 @@ "node": ">=12" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.12", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", @@ -2591,6 +3200,84 @@ "@lezer/lr": "^1.0.0" } }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/@marijn/find-cluster-break": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", @@ -3024,6 +3711,19 @@ "@tybys/wasm-util": "^0.10.0" } }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3482,6 +4182,58 @@ "node": ">=16" } }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz", + "integrity": "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/webcrypto": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.7.1.tgz", + "integrity": "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "tslib": "^2.8.1", + "webcrypto-core": "^1.9.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/@phun-ky/typeof": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@phun-ky/typeof/-/typeof-2.0.3.tgz", @@ -4325,6 +5077,19 @@ "url": "https://ko-fi.com/dangreen" } }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@stablelib/base64": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", @@ -4332,6 +5097,19 @@ "license": "MIT", "peer": true }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@tailwindcss/typography": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", @@ -4431,6 +5209,19 @@ "@types/node": "*" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -4500,6 +5291,16 @@ "@types/send": "*" } }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -4509,6 +5310,13 @@ "@types/unist": "*" } }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -4529,6 +5337,16 @@ "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", "license": "MIT" }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -4608,6 +5426,16 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", @@ -4652,6 +5480,17 @@ "@types/node": "*" } }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.56.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", @@ -5308,6 +6147,16 @@ "yauzl": "^2.9.2" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@xterm/addon-clipboard": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.1.0.tgz", @@ -5564,6 +6413,386 @@ "node": ">= 8" } }, + "node_modules/app-builder-lib": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.15.3.tgz", + "integrity": "sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.4", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@noble/hashes": "^2.2.0", + "@peculiar/webcrypto": "^1.7.1", + "@types/fs-extra": "9.0.13", + "ajv": "^8.18.0", + "asn1js": "^3.0.10", + "async-exit-hook": "^2.0.1", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.15.3", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.2.5", + "pkijs": "^3.4.0", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "unzipper": "^0.12.3", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.15.3", + "electron-builder-squirrel-windows": "26.15.3" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/app-builder-lib/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/app-builder-lib/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/app-builder-lib/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/app-builder-lib/node_modules/brace-expansion": { + "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": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/app-builder-lib/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/app-builder-lib/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/app-builder-lib/node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/app-builder-lib/node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/app-builder-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/app-builder-lib/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/tar": { + "version": "7.5.16", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", + "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/app-builder-lib/node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/app-builder-lib/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/app-builder-lib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -5770,6 +6999,21 @@ "safer-buffer": "^2.1.0" } }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/ast-types": { "version": "0.13.4", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", @@ -5783,6 +7027,23 @@ "node": ">=4" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -5813,6 +7074,23 @@ "node": ">= 4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/attr-accept": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", @@ -5920,6 +7198,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -6032,6 +7317,13 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, "node_modules/bn.js": { "version": "4.12.3", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", @@ -6077,6 +7369,15 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -6176,6 +7477,114 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/builder-util": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.15.3.tgz", + "integrity": "sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", + "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -6212,6 +7621,16 @@ "node": ">= 0.8" } }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/c12": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.0.tgz", @@ -6295,6 +7714,51 @@ "dev": true, "license": "ISC" }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -6491,10 +7955,17 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, "node_modules/ci-info": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", - "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", "dev": true, "funding": [ { @@ -6690,6 +8161,29 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone-response/node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -6780,6 +8274,19 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -6820,6 +8327,16 @@ "dot-prop": "^5.1.0" } }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -7168,6 +8685,15 @@ "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "license": "MIT" }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -7372,6 +8898,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -7443,6 +8979,16 @@ "node": ">= 14" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -7487,6 +9033,14 @@ "node": ">=8" } }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -7512,6 +9066,41 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -7531,6 +9120,87 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "license": "MIT" }, + "node_modules/dmg-builder": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.15.3.tgz", + "integrity": "sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0" + } + }, + "node_modules/dmg-builder/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -7579,6 +9249,35 @@ "url": "https://dotenvx.com" } }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -7593,6 +9292,49 @@ "node": ">= 0.4" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -7614,6 +9356,187 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "38.8.6", + "resolved": "https://registry.npmjs.org/electron/-/electron-38.8.6.tgz", + "integrity": "sha512-lyBhcVi9QYAZL6FO6r5twAWAjWnYomo3iVDvrb5SJZlq928BGemHOKG0tPIq41NOLaCu9f3XdEEjMkjQPjprRg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^22.7.7", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.15.3.tgz", + "integrity": "sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.15.3", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.15.3.tgz", + "integrity": "sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-publish": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.15.3.tgz", + "integrity": "sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "aws4": "^1.13.2", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.190", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.190.tgz", @@ -7621,6 +9544,44 @@ "dev": true, "license": "ISC" }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -7901,6 +9862,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/esbuild": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", @@ -8745,6 +10714,43 @@ "node": ">=0.10.0" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fast-content-type-parse": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", @@ -8904,6 +10910,29 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "license": "MIT" }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -9019,6 +11048,46 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", @@ -9065,6 +11134,21 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -9098,6 +11182,13 @@ "dev": true, "license": "ISC" }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -9403,6 +11494,39 @@ "node": ">=10.13.0" } }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/global-directory": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", @@ -9492,6 +11616,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -9617,9 +11767,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -9977,6 +12127,20 @@ "node": ">= 14" } }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -10188,6 +12352,18 @@ "node": ">=8" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -10900,6 +13076,19 @@ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -10956,6 +13145,24 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -11062,6 +13269,14 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -11075,6 +13290,16 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -11232,6 +13457,13 @@ "node": ">=0.10.0" } }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -11669,6 +13901,16 @@ "loose-envify": "cli.js" } }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/lowlight": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", @@ -11749,6 +13991,34 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/matcher/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -13171,6 +15441,29 @@ "node": "^18 || ^20 || >= 21" } }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-api-version/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-exports-info": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", @@ -13292,6 +15585,13 @@ "node": "^16.13.0 || >=18.0.0" } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-pty": { "version": "1.2.0-beta.12", "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.2.0-beta.12.tgz", @@ -13378,6 +15678,19 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -13712,6 +16025,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -13931,6 +16254,16 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -13991,6 +16324,21 @@ "dev": true, "license": "MIT" }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -14062,6 +16410,37 @@ "pathe": "^2.0.3" } }, + "node_modules/pkijs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/pkijs/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/plimit-lit": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", @@ -14075,6 +16454,21 @@ "node": ">=12" } }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -14241,6 +16635,36 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -14302,6 +16726,16 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", @@ -14327,6 +16761,25 @@ "react-is": "^16.13.1" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", @@ -14413,6 +16866,26 @@ "node": ">=6" } }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -14458,6 +16931,19 @@ ], "license": "MIT" }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -14750,6 +17236,19 @@ "react": ">= 0.14.0" } }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -15217,6 +17716,24 @@ "node": ">=0.10.0" } }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -15237,6 +17754,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -15257,6 +17781,19 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -15301,6 +17838,97 @@ "dev": true, "license": "MIT" }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/roarr/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, "node_modules/rollup": { "version": "4.45.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz", @@ -15520,6 +18148,26 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -15552,6 +18200,14 @@ "semver": "bin/semver.js" } }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/send": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", @@ -15600,6 +18256,37 @@ "node": ">= 0.8" } }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -16225,6 +18912,32 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -16341,6 +19054,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/space-separated-tokens": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", @@ -16433,6 +19157,16 @@ "fast-sha256": "^1.3.0" } }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -16781,6 +19515,19 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -17003,6 +19750,70 @@ "dev": true, "license": "ISC" }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -17024,6 +19835,26 @@ "node": ">=0.8" } }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/tinyexec": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", @@ -17082,6 +19913,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -17139,6 +19990,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, "node_modules/ts-algebra": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", @@ -18149,6 +21010,16 @@ "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", "license": "ISC" }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -18193,6 +21064,58 @@ "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, + "node_modules/unzipper": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.3.tgz", + "integrity": "sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "^11.2.0", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, + "node_modules/unzipper/node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/unzipper/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/unzipper/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -18296,6 +21219,13 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -18554,6 +21484,20 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/webcrypto-core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.9.2.tgz", + "integrity": "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -18852,6 +21796,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 2220c0f9..ae75f9c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "@cloudcli-ai/cloudcli", "version": "1.34.0", + "productName": "CloudCLI", "description": "A web-based UI for Claude Code CLI", "type": "module", "main": "dist-server/server/index.js", @@ -8,6 +9,7 @@ "cloudcli": "dist-server/server/cli.js" }, "files": [ + "electron/", "server/", "shared/", "public/api-docs.html", @@ -30,6 +32,10 @@ "server:dev": "tsx --tsconfig server/tsconfig.json server/index.js", "server:dev-watch": "tsx watch --tsconfig server/tsconfig.json server/index.js", "client": "vite", + "desktop": "electron electron/main.js", + "desktop:dev": "ELECTRON_DEV_URL=http://127.0.0.1:5173 electron electron/main.js", + "desktop:pack": "npm run build && electron-builder --dir", + "desktop:dist:mac": "npm run build && electron-builder --mac dmg zip", "build": "npm run build:client && npm run build:server", "build:client": "vite build", "prebuild:server": "node -e \"require('node:fs').rmSync('dist-server', { recursive: true, force: true })\"", @@ -45,6 +51,53 @@ "prepare": "husky", "update:platform": "./update-platform.sh" }, + "build": { + "appId": "ai.cloudcli.desktop", + "productName": "CloudCLI", + "artifactName": "CloudCLI-${version}-${arch}.${ext}", + "directories": { + "output": "release" + }, + "extraMetadata": { + "main": "electron/main.js" + }, + "files": [ + "electron/", + "public/", + "dist/", + "dist-server/", + "shared/", + "server/", + "package.json" + ], + "protocols": [ + { + "name": "CloudCLI", + "schemes": [ + "cloudcli" + ] + } + ], + "mac": { + "category": "public.app-category.developer-tools", + "target": [ + "dmg", + "zip" + ], + "extendInfo": { + "CFBundleName": "CloudCLI", + "CFBundleDisplayName": "CloudCLI", + "CFBundleURLTypes": [ + { + "CFBundleURLName": "CloudCLI", + "CFBundleURLSchemes": [ + "cloudcli" + ] + } + ] + } + } + }, "keywords": [ "claude code", "claude-code", @@ -141,6 +194,8 @@ "auto-changelog": "^2.5.0", "autoprefixer": "^10.4.16", "concurrently": "^8.2.2", + "electron": "^38.0.0", + "electron-builder": "^26.15.3", "eslint": "^9.39.3", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-boundaries": "^6.0.2", diff --git a/server/index.js b/server/index.js index cb8ecc31..f9455b1e 100755 --- a/server/index.js +++ b/server/index.js @@ -72,6 +72,8 @@ import userRoutes from './routes/user.js'; import geminiRoutes from './routes/gemini.js'; import pluginsRoutes from './routes/plugins.js'; import providerRoutes from './modules/providers/provider.routes.js'; +import browserUseRoutes from './modules/browser-use/browser-use.routes.js'; +import { browserUseService } from './modules/browser-use/browser-use.service.js'; import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js'; import { initializeDatabase, projectsDb, sessionsDb } from './modules/database/index.js'; import { configureWebPush } from './services/vapid-keys.js'; @@ -201,6 +203,9 @@ app.use('/api/gemini', authenticateToken, geminiRoutes); // Plugins API Routes (protected) app.use('/api/plugins', authenticateToken, pluginsRoutes); +// Browser Use API Routes (protected) +app.use('/api/browser-use', authenticateToken, browserUseRoutes); + // Unified provider MCP routes (protected) app.use('/api/providers', authenticateToken, providerRoutes); @@ -1694,12 +1699,13 @@ async function startServer() { await closeSessionsWatcher(); // Clean up plugin processes on shutdown - const shutdownPlugins = async () => { + const shutdownRuntimeServices = async () => { + await browserUseService.stopAllSessions(); await stopAllPlugins(); process.exit(0); }; - process.on('SIGTERM', () => void shutdownPlugins()); - process.on('SIGINT', () => void shutdownPlugins()); + process.on('SIGTERM', () => void shutdownRuntimeServices()); + process.on('SIGINT', () => void shutdownRuntimeServices()); } catch (error) { console.error('[ERROR] Failed to start server:', error); process.exit(1); diff --git a/server/modules/browser-use/browser-use.routes.ts b/server/modules/browser-use/browser-use.routes.ts new file mode 100644 index 00000000..f5dc563b --- /dev/null +++ b/server/modules/browser-use/browser-use.routes.ts @@ -0,0 +1,76 @@ +import express from 'express'; + +import { browserUseService } from '@/modules/browser-use/browser-use.service.js'; + +const router = express.Router(); + +type AuthenticatedRequest = express.Request & { + user?: { + id?: string | number; + }; +}; + +function requireUser(req: AuthenticatedRequest): { id: string | number } { + const userId = req.user?.id; + if (userId === undefined || userId === null) { + throw new Error('Authenticated user is required.'); + } + return { id: userId }; +} + +function readParam(value: string | string[] | undefined): string { + return Array.isArray(value) ? value[0] || '' : value || ''; +} + +router.get('/status', (_req, res) => { + res.json({ success: true, data: browserUseService.getStatus() }); +}); + +router.get('/sessions', async (req: AuthenticatedRequest, res) => { + try { + res.json({ success: true, data: { sessions: await browserUseService.listSessions(requireUser(req)) } }); + } catch (error) { + res.status(401).json({ + success: false, + error: error instanceof Error ? error.message : 'Failed to list browser sessions.', + }); + } +}); + +router.post('/sessions', async (req: AuthenticatedRequest, res) => { + try { + const session = await browserUseService.createSession(requireUser(req)); + res.status(session.status === 'unavailable' ? 202 : 201).json({ success: true, data: { session } }); + } catch (error) { + res.status(400).json({ + success: false, + error: error instanceof Error ? error.message : 'Failed to create browser session.', + }); + } +}); + +router.post('/sessions/:sessionId/navigate', async (req: AuthenticatedRequest, res) => { + try { + const session = await browserUseService.navigate(requireUser(req), readParam(req.params.sessionId), String(req.body?.url || '')); + res.json({ success: true, data: { session } }); + } catch (error) { + res.status(400).json({ + success: false, + error: error instanceof Error ? error.message : 'Failed to navigate browser session.', + }); + } +}); + +router.post('/sessions/:sessionId/stop', async (req: AuthenticatedRequest, res) => { + try { + const result = await browserUseService.stopSession(requireUser(req), readParam(req.params.sessionId)); + res.json({ success: true, data: result }); + } catch (error) { + res.status(400).json({ + success: false, + error: error instanceof Error ? error.message : 'Failed to stop browser session.', + }); + } +}); + +export default router; diff --git a/server/modules/browser-use/browser-use.service.ts b/server/modules/browser-use/browser-use.service.ts new file mode 100644 index 00000000..4ca96695 --- /dev/null +++ b/server/modules/browser-use/browser-use.service.ts @@ -0,0 +1,345 @@ +import { createRequire } from 'node:module'; +import { randomUUID } from 'node:crypto'; +import dns from 'node:dns/promises'; +import net from 'node:net'; + +const require = createRequire(import.meta.url); +const IS_PLATFORM = process.env.VITE_IS_PLATFORM === 'true'; +const MAX_SESSIONS_PER_OWNER = Number.parseInt(process.env.CLOUDCLI_BROWSER_USE_MAX_SESSIONS_PER_OWNER || '3', 10); +const SESSION_TTL_MS = Number.parseInt(process.env.CLOUDCLI_BROWSER_USE_SESSION_TTL_MS || String(30 * 60 * 1000), 10); +const ALLOW_PRIVATE_NETWORKS = process.env.CLOUDCLI_BROWSER_USE_ALLOW_PRIVATE_NETWORKS === '1'; + +type BrowserUseRuntime = 'cloud' | 'local'; +type BrowserUseSessionStatus = 'ready' | 'stopped' | 'unavailable'; + +type BrowserUseSession = { + id: string; + ownerId: string; + runtime: BrowserUseRuntime; + status: BrowserUseSessionStatus; + url: string | null; + title: string | null; + screenshotDataUrl: string | null; + createdAt: string; + updatedAt: string; + lastAction: string | null; + message: string | null; +}; + +type PublicBrowserUseSession = Omit; + +type RuntimeHandle = { + browser?: any; + page?: any; +}; + +type BrowserUseOwner = { + id: string | number; +}; + +const sessions = new Map(); +const handles = new Map(); + +function getRuntime(): BrowserUseRuntime { + return IS_PLATFORM ? 'cloud' : 'local'; +} + +function isBrowserUseEnabled(): boolean { + return process.env.CLOUDCLI_BROWSER_USE_ENABLED === '1'; +} + +function getSetupMessage(): string { + if (!isBrowserUseEnabled()) { + return 'Browser Use is disabled. Set CLOUDCLI_BROWSER_USE_ENABLED=1 after provisioning a Playwright/Chromium runtime.'; + } + + return 'Playwright is not available in this runtime. Install/provision Playwright or point CloudCLI at a managed browser worker.'; +} + +function getPlaywright(): any | null { + try { + return require('playwright'); + } catch { + return null; + } +} + +function getOwnerId(owner: BrowserUseOwner): string { + if (owner.id === undefined || owner.id === null || String(owner.id).trim() === '') { + throw new Error('Authenticated user is required.'); + } + + return String(owner.id); +} + +function isPrivateIpv4(address: string): boolean { + const parts = address.split('.').map((part) => Number.parseInt(part, 10)); + if (parts.length !== 4 || parts.some((part) => Number.isNaN(part) || part < 0 || part > 255)) { + return true; + } + + const [first, second] = parts; + return first === 0 + || first === 10 + || first === 127 + || (first === 169 && second === 254) + || (first === 172 && second >= 16 && second <= 31) + || (first === 192 && second === 168) + || first >= 224; +} + +function isPrivateIpv6(address: string): boolean { + const normalized = address.toLowerCase(); + return normalized === '::1' + || normalized === '::' + || normalized.startsWith('fc') + || normalized.startsWith('fd') + || normalized.startsWith('fe80:') + || normalized.startsWith('::ffff:127.') + || normalized.startsWith('::ffff:10.') + || normalized.startsWith('::ffff:192.168.') + || /^::ffff:172\.(1[6-9]|2\d|3[0-1])\./.test(normalized) + || /^::ffff:169\.254\./.test(normalized); +} + +export function isBlockedBrowserUseAddress(address: string): boolean { + const version = net.isIP(address); + if (version === 4) { + return isPrivateIpv4(address); + } + if (version === 6) { + return isPrivateIpv6(address); + } + return true; +} + +async function assertPublicHttpTarget(parsedUrl: URL): Promise { + if (ALLOW_PRIVATE_NETWORKS) { + return; + } + + const hostname = parsedUrl.hostname; + if (!hostname) { + throw new Error('URL hostname is required.'); + } + + if (net.isIP(hostname)) { + if (isBlockedBrowserUseAddress(hostname)) { + throw new Error('Browser Use cannot navigate to private or local network addresses.'); + } + return; + } + + const addresses = await dns.lookup(hostname, { all: true, verbatim: true }); + if (addresses.length === 0 || addresses.some((entry) => isBlockedBrowserUseAddress(entry.address))) { + throw new Error('Browser Use cannot navigate to private or local network addresses.'); + } +} + +async function normalizeUrl(rawUrl: string): Promise { + const trimmed = rawUrl.trim(); + if (!trimmed) { + throw new Error('URL is required.'); + } + + const withProtocol = /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(trimmed) + ? trimmed + : `https://${trimmed}`; + const parsed = new URL(withProtocol); + if (!['http:', 'https:'].includes(parsed.protocol)) { + throw new Error('Only http and https URLs are supported.'); + } + + await assertPublicHttpTarget(parsed); + + return parsed.toString(); +} + +async function assertAllowedBrowserRequest(rawUrl: string): Promise { + const parsed = new URL(rawUrl); + if (!['http:', 'https:'].includes(parsed.protocol)) { + return; + } + + await assertPublicHttpTarget(parsed); +} + +async function attachRequestGuard(page: any): Promise { + await page.route('**/*', async (route: any) => { + try { + await assertAllowedBrowserRequest(route.request().url()); + await route.continue(); + } catch { + await route.abort('blockedbyclient'); + } + }); +} + +function publicSession(session: BrowserUseSession): PublicBrowserUseSession { + const { ownerId: _ownerId, ...publicFields } = session; + return publicFields; +} + +function ownerSessions(ownerId: string): BrowserUseSession[] { + return [...sessions.values()].filter((session) => session.ownerId === ownerId); +} + +async function closeHandle(sessionId: string): Promise { + const handle = handles.get(sessionId); + handles.delete(sessionId); + await handle?.browser?.close().catch(() => undefined); +} + +async function expireStaleSessions(now = Date.now()): Promise { + await Promise.all([...sessions.values()].map(async (session) => { + if (session.status !== 'ready') { + return; + } + + const updatedAt = Date.parse(session.updatedAt); + if (!Number.isFinite(updatedAt) || now - updatedAt <= SESSION_TTL_MS) { + return; + } + + await closeHandle(session.id); + session.status = 'stopped'; + session.updatedAt = new Date(now).toISOString(); + session.lastAction = 'expire'; + session.message = 'Browser session expired after inactivity.'; + })); +} + +async function captureSession(session: BrowserUseSession, page: any): Promise { + const screenshot = await page.screenshot({ type: 'jpeg', quality: 72, fullPage: false }); + session.screenshotDataUrl = `data:image/jpeg;base64,${Buffer.from(screenshot).toString('base64')}`; + session.title = await page.title().catch(() => null); + session.url = page.url() || session.url; + session.updatedAt = new Date().toISOString(); +} + +export const browserUseService = { + getStatus() { + const playwright = getPlaywright(); + const enabled = isBrowserUseEnabled() && Boolean(playwright); + + return { + enabled, + runtime: getRuntime(), + available: enabled, + sessionCount: sessions.size, + mcpRecommended: true, + message: enabled + ? 'Browser Use runtime is available.' + : getSetupMessage(), + }; + }, + + async listSessions(owner: BrowserUseOwner) { + const ownerId = getOwnerId(owner); + await expireStaleSessions(); + return ownerSessions(ownerId).map(publicSession); + }, + + async createSession(owner: BrowserUseOwner) { + const ownerId = getOwnerId(owner); + await expireStaleSessions(); + + const now = new Date().toISOString(); + const session: BrowserUseSession = { + id: randomUUID(), + ownerId, + runtime: getRuntime(), + status: 'unavailable', + url: null, + title: null, + screenshotDataUrl: null, + createdAt: now, + updatedAt: now, + lastAction: 'create', + message: null, + }; + + const activeOwnerSessions = ownerSessions(ownerId).filter((item) => item.status === 'ready'); + if (activeOwnerSessions.length >= MAX_SESSIONS_PER_OWNER) { + throw new Error(`Browser Use is limited to ${MAX_SESSIONS_PER_OWNER} active sessions per user.`); + } + + const playwright = getPlaywright(); + if (!isBrowserUseEnabled() || !playwright) { + session.message = getSetupMessage(); + sessions.set(session.id, session); + return publicSession(session); + } + + const browser = await playwright.chromium.launch({ + headless: true, + args: ['--disable-dev-shm-usage'], + }); + const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); + await attachRequestGuard(page); + session.status = 'ready'; + session.message = 'Browser session is ready.'; + sessions.set(session.id, session); + handles.set(session.id, { browser, page }); + await captureSession(session, page); + return publicSession(session); + }, + + async navigate(owner: BrowserUseOwner, sessionId: string, rawUrl: string) { + const ownerId = getOwnerId(owner); + await expireStaleSessions(); + + const session = sessions.get(sessionId); + if (!session || session.ownerId !== ownerId) { + throw new Error('Browser session not found.'); + } + + if (session.status !== 'ready') { + throw new Error(session.message || 'Browser session is not available.'); + } + + const handle = handles.get(sessionId); + if (!handle?.page) { + throw new Error('Browser runtime handle is not available.'); + } + + const url = await normalizeUrl(rawUrl); + await handle.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 }); + session.lastAction = `navigate:${url}`; + await captureSession(session, handle.page); + return publicSession(session); + }, + + async stopSession(owner: BrowserUseOwner, sessionId: string) { + const ownerId = getOwnerId(owner); + const session = sessions.get(sessionId); + if (!session || session.ownerId !== ownerId) { + return { stopped: false }; + } + + await closeHandle(sessionId); + + session.status = 'stopped'; + session.updatedAt = new Date().toISOString(); + session.lastAction = 'stop'; + session.message = 'Browser session stopped.'; + return { stopped: true, session: publicSession(session) }; + }, + + async stopAllSessions() { + await Promise.all([...sessions.keys()].map(async (sessionId) => { + await closeHandle(sessionId); + const session = sessions.get(sessionId); + if (session) { + session.status = 'stopped'; + session.updatedAt = new Date().toISOString(); + session.lastAction = 'shutdown'; + session.message = 'Browser session stopped during server shutdown.'; + } + })); + }, +}; + +process.once('beforeExit', () => { + void browserUseService.stopAllSessions(); +}); diff --git a/server/modules/browser-use/tests/browser-use.service.test.ts b/server/modules/browser-use/tests/browser-use.service.test.ts new file mode 100644 index 00000000..3a291682 --- /dev/null +++ b/server/modules/browser-use/tests/browser-use.service.test.ts @@ -0,0 +1,41 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { browserUseService, isBlockedBrowserUseAddress } from '@/modules/browser-use/browser-use.service.js'; + +test('browser use blocks private and local network addresses by default', () => { + assert.equal(isBlockedBrowserUseAddress('127.0.0.1'), true); + assert.equal(isBlockedBrowserUseAddress('10.0.0.12'), true); + assert.equal(isBlockedBrowserUseAddress('172.16.4.8'), true); + assert.equal(isBlockedBrowserUseAddress('192.168.1.4'), true); + assert.equal(isBlockedBrowserUseAddress('169.254.169.254'), true); + assert.equal(isBlockedBrowserUseAddress('::1'), true); + assert.equal(isBlockedBrowserUseAddress('8.8.8.8'), false); + assert.equal(isBlockedBrowserUseAddress('2001:4860:4860::8888'), false); +}); + +test('browser use sessions are listed only for their owner', async () => { + const originalEnabled = process.env.CLOUDCLI_BROWSER_USE_ENABLED; + process.env.CLOUDCLI_BROWSER_USE_ENABLED = '0'; + + const ownerA = { id: `owner-a-${Date.now()}-${Math.random()}` }; + const ownerB = { id: `owner-b-${Date.now()}-${Math.random()}` }; + + try { + const ownerASession = await browserUseService.createSession(ownerA); + await browserUseService.createSession(ownerB); + + const ownerASessions = await browserUseService.listSessions(ownerA); + const ownerBSessions = await browserUseService.listSessions(ownerB); + + assert.equal(ownerASessions.some((session) => session.id === ownerASession.id), true); + assert.equal(ownerBSessions.some((session) => session.id === ownerASession.id), false); + assert.equal(Object.hasOwn(ownerASession, 'ownerId'), false); + } finally { + if (originalEnabled === undefined) { + delete process.env.CLOUDCLI_BROWSER_USE_ENABLED; + } else { + process.env.CLOUDCLI_BROWSER_USE_ENABLED = originalEnabled; + } + } +}); diff --git a/src/components/browser-use/index.ts b/src/components/browser-use/index.ts new file mode 100644 index 00000000..a4e5be12 --- /dev/null +++ b/src/components/browser-use/index.ts @@ -0,0 +1 @@ +export { default as BrowserUsePanel } from './view/BrowserUsePanel'; diff --git a/src/components/browser-use/view/BrowserUsePanel.tsx b/src/components/browser-use/view/BrowserUsePanel.tsx new file mode 100644 index 00000000..d2494a2a --- /dev/null +++ b/src/components/browser-use/view/BrowserUsePanel.tsx @@ -0,0 +1,233 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { ExternalLink, Globe, MonitorPlay, Navigation, Pause, RefreshCw, Square } from 'lucide-react'; + +import { Badge, Button } from '../../../shared/view/ui'; +import { authenticatedFetch } from '../../../utils/api'; + +type BrowserUseStatus = { + enabled: boolean; + available: boolean; + runtime: 'cloud' | 'local'; + sessionCount: number; + mcpRecommended: boolean; + message: string; +}; + +type BrowserUseSession = { + id: string; + runtime: 'cloud' | 'local'; + status: 'ready' | 'stopped' | 'unavailable'; + url: string | null; + title: string | null; + screenshotDataUrl: string | null; + createdAt: string; + updatedAt: string; + lastAction: string | null; + message: string | null; +}; + +type BrowserUsePanelProps = { + isVisible: boolean; +}; + +async function readJson(response: Response): Promise { + const data = await response.json(); + if (!response.ok || data.success === false) { + throw new Error(data.error || data.details || `Request failed (${response.status})`); + } + return data as T; +} + +export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) { + const [status, setStatus] = useState(null); + const [sessions, setSessions] = useState([]); + const [selectedSessionId, setSelectedSessionId] = useState(null); + const [targetUrl, setTargetUrl] = useState('https://example.com'); + const [isBusy, setIsBusy] = useState(false); + const [error, setError] = useState(null); + + const selectedSession = useMemo( + () => sessions.find((session) => session.id === selectedSessionId) || sessions[0] || null, + [selectedSessionId, sessions], + ); + + const refresh = useCallback(async () => { + const [statusResponse, sessionsResponse] = await Promise.all([ + authenticatedFetch('/api/browser-use/status'), + authenticatedFetch('/api/browser-use/sessions'), + ]); + const statusData = await readJson<{ data: BrowserUseStatus }>(statusResponse); + const sessionsData = await readJson<{ data: { sessions: BrowserUseSession[] } }>(sessionsResponse); + setStatus(statusData.data); + setSessions(sessionsData.data.sessions); + setSelectedSessionId((current) => ( + current && sessionsData.data.sessions.some((session) => session.id === current) + ? current + : sessionsData.data.sessions[0]?.id || null + )); + }, []); + + useEffect(() => { + if (!isVisible) return; + void refresh().catch((err) => setError(err instanceof Error ? err.message : 'Failed to load Browser Use')); + }, [isVisible, refresh]); + + const runAction = useCallback(async (action: () => Promise) => { + setIsBusy(true); + setError(null); + try { + await action(); + await refresh(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Browser Use action failed'); + } finally { + setIsBusy(false); + } + }, [refresh]); + + const createSession = () => runAction(async () => { + const response = await authenticatedFetch('/api/browser-use/sessions', { method: 'POST' }); + const data = await readJson<{ data: { session: BrowserUseSession } }>(response); + setSelectedSessionId(data.data.session.id); + }); + + const navigate = () => runAction(async () => { + if (!selectedSession) { + throw new Error('Create a browser session first.'); + } + const response = await authenticatedFetch(`/api/browser-use/sessions/${selectedSession.id}/navigate`, { + method: 'POST', + body: JSON.stringify({ url: targetUrl }), + }); + await readJson(response); + }); + + const stopSession = () => runAction(async () => { + if (!selectedSession) return; + const response = await authenticatedFetch(`/api/browser-use/sessions/${selectedSession.id}/stop`, { method: 'POST' }); + await readJson(response); + }); + + return ( +
+
+
+
+ +

Browser Use

+ {status && ( + + {status.runtime} + + )} +
+

+ Managed Playwright browser sessions with owner-scoped screenshots and navigation. +

+
+
+ + +
+
+ +
+ + +
+
+ setTargetUrl(event.target.value)} + className="h-9 min-w-[220px] flex-1 rounded-md border border-input bg-background px-3 text-sm outline-none focus:ring-1 focus:ring-ring" + placeholder="https://example.com" + /> + + + +
+ + {error && ( +
+ {error} +
+ )} + +
+
+
+ + {selectedSession?.url || 'No page loaded'} +
+
+ {selectedSession?.screenshotDataUrl ? ( + Browser session screenshot + ) : ( +
+ +
+ {selectedSession?.message || 'Create a browser session to start.'} +
+

+ This panel shows captured browser screenshots. Interactive agent control should use the guarded Browser Use API. +

+
+ )} +
+
+
+
+
+
+ ); +} diff --git a/src/components/main-content/view/MainContent.tsx b/src/components/main-content/view/MainContent.tsx index f0a29a70..2f98b7f1 100644 --- a/src/components/main-content/view/MainContent.tsx +++ b/src/components/main-content/view/MainContent.tsx @@ -5,6 +5,7 @@ import FileTree from '../../file-tree/view/FileTree'; import StandaloneShell from '../../standalone-shell/view/StandaloneShell'; import GitPanel from '../../git-panel/view/GitPanel'; import PluginTabContent from '../../plugins/view/PluginTabContent'; +import { BrowserUsePanel } from '../../browser-use'; import type { MainContentProps } from '../types/types'; import { useTaskMaster } from '../../../contexts/TaskMasterContext'; import { usePaletteOpsRegister } from '../../../contexts/PaletteOpsContext'; @@ -175,7 +176,11 @@ function MainContent({ {shouldShowTasksTab && } -
+ {activeTab === 'browser' && ( +
+ +
+ )} {activeTab.startsWith('plugin:') && (
diff --git a/src/components/main-content/view/subcomponents/MainContentTabSwitcher.tsx b/src/components/main-content/view/subcomponents/MainContentTabSwitcher.tsx index 51a5d649..83c641df 100644 --- a/src/components/main-content/view/subcomponents/MainContentTabSwitcher.tsx +++ b/src/components/main-content/view/subcomponents/MainContentTabSwitcher.tsx @@ -1,6 +1,7 @@ -import { MessageSquare, Terminal, Folder, GitBranch, ClipboardCheck, type LucideIcon } from 'lucide-react'; +import { MessageSquare, Terminal, Folder, GitBranch, ClipboardCheck, MonitorPlay, type LucideIcon } from 'lucide-react'; import type { Dispatch, SetStateAction } from 'react'; import { useTranslation } from 'react-i18next'; + import { Tooltip, PillBar, Pill } from '../../../../shared/view/ui'; import type { AppTab } from '../../../../types/app'; import { usePlugins } from '../../../../contexts/PluginsContext'; @@ -34,6 +35,7 @@ const BASE_TABS: BuiltInTab[] = [ { kind: 'builtin', id: 'shell', labelKey: 'tabs.shell', icon: Terminal }, { kind: 'builtin', id: 'files', labelKey: 'tabs.files', icon: Folder }, { kind: 'builtin', id: 'git', labelKey: 'tabs.git', icon: GitBranch }, + { kind: 'builtin', id: 'browser', labelKey: 'tabs.browser', icon: MonitorPlay }, ]; const TASKS_TAB: BuiltInTab = { diff --git a/src/components/main-content/view/subcomponents/MainContentTitle.tsx b/src/components/main-content/view/subcomponents/MainContentTitle.tsx index 6cc88ba1..78f0dfc0 100644 --- a/src/components/main-content/view/subcomponents/MainContentTitle.tsx +++ b/src/components/main-content/view/subcomponents/MainContentTitle.tsx @@ -1,4 +1,5 @@ import { useTranslation } from 'react-i18next'; + import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo'; import type { AppTab, Project, ProjectSession } from '../../../../types/app'; import { usePlugins } from '../../../../contexts/PluginsContext'; @@ -27,6 +28,10 @@ function getTabTitle(activeTab: AppTab, shouldShowTasksTab: boolean, t: (key: st return 'TaskMaster'; } + if (activeTab === 'browser') { + return 'Browser Use'; + } + return 'Project'; } diff --git a/src/hooks/useProjectsState.ts b/src/hooks/useProjectsState.ts index e6005693..29768beb 100644 --- a/src/hooks/useProjectsState.ts +++ b/src/hooks/useProjectsState.ts @@ -221,7 +221,7 @@ const isUpdateAdditive = ( ); }; -const VALID_TABS: Set = new Set(['chat', 'files', 'shell', 'git', 'tasks', 'preview']); +const VALID_TABS: Set = new Set(['chat', 'files', 'shell', 'git', 'tasks', 'browser']); const isValidTab = (tab: string): tab is AppTab => { return VALID_TABS.has(tab) || tab.startsWith('plugin:'); @@ -631,7 +631,7 @@ export function useProjectsState({ (session: ProjectSession) => { setSelectedSession(session); - if (activeTab === 'tasks' || activeTab === 'preview') { + if (activeTab === 'tasks' || activeTab === 'browser') { setActiveTab('chat'); } diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 94260cff..636d2e8d 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -22,7 +22,8 @@ "shell": "Terminal", "files": "Dateien", "git": "Quellcodeverwaltung", - "tasks": "Aufgaben" + "tasks": "Aufgaben", + "browser": "Browser" }, "status": { "loading": "Lädt...", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index f35eb3c5..9137da9a 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -22,7 +22,8 @@ "shell": "Shell", "files": "Files", "git": "Source Control", - "tasks": "Tasks" + "tasks": "Tasks", + "browser": "Browser" }, "status": { "loading": "Loading...", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index 7993c69d..79f9f675 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -22,7 +22,8 @@ "shell": "Terminale", "files": "File", "git": "Controllo Versione", - "tasks": "Attività" + "tasks": "Attività", + "browser": "Browser" }, "status": { "loading": "Caricamento...", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 0651e25c..498ee46c 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -22,7 +22,8 @@ "shell": "シェル", "files": "ファイル", "git": "ソース管理", - "tasks": "タスク" + "tasks": "タスク", + "browser": "Browser" }, "status": { "loading": "読み込み中...", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index fcf1de52..03244458 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -22,7 +22,8 @@ "shell": "Shell", "files": "파일", "git": "소스 관리", - "tasks": "작업" + "tasks": "작업", + "browser": "Browser" }, "status": { "loading": "로딩 중...", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 24331f4a..fc71abe1 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -22,7 +22,8 @@ "shell": "Терминал", "files": "Файлы", "git": "Система контроля версий", - "tasks": "Задачи" + "tasks": "Задачи", + "browser": "Browser" }, "status": { "loading": "Загрузка...", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 3b9a6d27..f1fa66b9 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -22,7 +22,8 @@ "shell": "Shell", "files": "Dosyalar", "git": "Kaynak Kontrolü", - "tasks": "Görevler" + "tasks": "Görevler", + "browser": "Browser" }, "status": { "loading": "Yükleniyor...", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 05e0369d..69cd159a 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -22,7 +22,8 @@ "shell": "终端", "files": "文件", "git": "源代码管理", - "tasks": "任务" + "tasks": "任务", + "browser": "Browser" }, "status": { "loading": "加载中...", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index ea05e41e..419be285 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -22,7 +22,8 @@ "shell": "終端機", "files": "檔案", "git": "版本控制", - "tasks": "任務" + "tasks": "任務", + "browser": "Browser" }, "status": { "loading": "載入中...", diff --git a/src/types/app.ts b/src/types/app.ts index aed51fd4..975d1211 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -17,7 +17,7 @@ export type ProviderModelsCacheInfo = { source: 'memory' | 'disk' | 'fresh'; }; -export type AppTab = 'chat' | 'files' | 'shell' | 'git' | 'tasks' | 'preview' | `plugin:${string}`; +export type AppTab = 'chat' | 'files' | 'shell' | 'git' | 'tasks' | 'browser' | `plugin:${string}`; export interface ProjectSession { id: string; From 861cfecbaae1ed41534f063f08fabd47d57c3e19 Mon Sep 17 00:00:00 2001 From: Simos Mikelatos Date: Mon, 15 Jun 2026 16:21:05 +0000 Subject: [PATCH 02/58] feat: add electron app support --- .gitignore | 5 + electron/assets/logo-macos.icns | Bin 0 -> 32391 bytes electron/assets/logo-macos.png | Bin 0 -> 13690 bytes electron/cloud.js | 240 ++++++ electron/desktopWindow.js | 685 +++++++++++++++ electron/launcher/index.html | 14 + electron/launcher/launcher.css | 1 + electron/launcher/launcher.js | 520 ++++++++++++ electron/localServer.js | 483 +++++++++++ electron/main.js | 789 ++++++++++++++++++ electron/preload.cjs | 28 + electron/scripts/generate-macos-icon.js | 62 ++ electron/tabs.js | 71 ++ package-lock.json | 6 +- package.json | 3 + server/index.js | 42 + .../computer-use/computer-use.routes.ts | 19 + .../computer-use/computer-use.service.ts | 22 + src/components/computer-use/index.ts | 1 + .../computer-use/view/ComputerUsePanel.tsx | 132 +++ .../main-content/view/MainContent.tsx | 7 + .../subcomponents/MainContentTabSwitcher.tsx | 3 +- .../view/subcomponents/MainContentTitle.tsx | 4 + .../view/subcomponents/GitHubStarBadge.tsx | 2 +- .../view/subcomponents/SidebarHeader.tsx | 14 +- .../view/subcomponents/SidebarProjectItem.tsx | 4 +- .../view/subcomponents/SidebarSessionItem.tsx | 4 +- src/hooks/useProjectsState.ts | 4 +- src/i18n/locales/de/common.json | 3 +- src/i18n/locales/en/common.json | 3 +- src/i18n/locales/it/common.json | 3 +- src/i18n/locales/ja/common.json | 3 +- src/i18n/locales/ko/common.json | 3 +- src/i18n/locales/ru/common.json | 3 +- src/i18n/locales/tr/common.json | 3 +- src/i18n/locales/zh-CN/common.json | 3 +- src/i18n/locales/zh-TW/common.json | 3 +- src/types/app.ts | 2 +- 38 files changed, 3166 insertions(+), 28 deletions(-) create mode 100644 electron/assets/logo-macos.icns create mode 100644 electron/assets/logo-macos.png create mode 100644 electron/cloud.js create mode 100644 electron/desktopWindow.js create mode 100644 electron/launcher/index.html create mode 100644 electron/launcher/launcher.css create mode 100644 electron/launcher/launcher.js create mode 100644 electron/localServer.js create mode 100644 electron/main.js create mode 100644 electron/preload.cjs create mode 100644 electron/scripts/generate-macos-icon.js create mode 100644 electron/tabs.js create mode 100644 server/modules/computer-use/computer-use.routes.ts create mode 100644 server/modules/computer-use/computer-use.service.ts create mode 100644 src/components/computer-use/index.ts create mode 100644 src/components/computer-use/view/ComputerUsePanel.tsx diff --git a/.gitignore b/.gitignore index e6b7985b..4b893c9e 100755 --- a/.gitignore +++ b/.gitignore @@ -142,3 +142,8 @@ tasks/ # Git worktrees .worktrees/ + +# Local desktop packaging artifacts +cloudcli-sidebar-app-source.tar.gz +cloudcli-sidebar.html +electron/*.tar.gz diff --git a/electron/assets/logo-macos.icns b/electron/assets/logo-macos.icns new file mode 100644 index 0000000000000000000000000000000000000000..c0d548ee185f715b1f1e48ef393d7152eb2e5734 GIT binary patch literal 32391 zcmeHw30PCtw(tg2R1~z%6VX;n>p(ziQ4oS!MM@QizETwlI5btzh|EI!TH)MqS#uVI9-ReXGpj&KQSshEf>)DhaO(%Gh~&=Gw9A zEowevYFYjLJlu)TC_294;Or$!$DY64vtVlTLHiG*zYhFTq*_G96-$-Pln=XG^g=wd zj?_J)TP_pg`C{C_JG*|3%a_}GTJT$0_k(PYcx?%vJqCc-ttn1HwzEHjTmY}t>p!}+ zGT<EJ28nf@8TTMTeF90IA3*KE6O_gv?P0c zMOX~`;>P8&@juv}PIaqyRE{+}2j++-upO;F4HsdxJNI_Y^S}3#_0swgFPs)irmdFFp?`ssrq7N1@GrP%JWN84qNdFOgJ>#8d8an^7{>NN9(W-Hm$iQOZfJdAEh z&=gSvhvD-#$C^j+k1Q^pGAJ{wRyuw(gM2krrU>i&?jR~)#e}m z6aThMahQ6xu`6?QW%2y9l=IQ9*Y92Nx8|NwppDY zU4^9|vwvt+ed%54Dksu5QF-a)3}xo<&bVMR*Z<q2?LONFKLc=;GJXH+?uFu(1iA1* zS!&Hb4B#VcOEZTW@UvGgvhhKeU^Elb1q=JzHE_6XGPyG7Sud(1BbuUmvsmHP@D z_A7hk{JtfbW(V*)w)zV``>jvywE3SERh@jpR9-6ViaV7+nx5Xm-_Gslv_CsX z*#oZHvobrYgmTKIC8-56`f#=|S}b0>_*^D=SN7H9ncy6$9$>27+!oJd@HrE(TP*+f z5oHmeZM0&y#FHVK0M>tJH5(882$g%2mTh5b%pZ^rVeLgJVyko1Wk911s#~U&E#Q~p z!3B%U-UDktc^B#y%vJ!8dNX!`1&Fo;z1HCMEbzef&jFMvEm+_DFx=j1nk^g^za2_T z0T_ArKMy4{h-C(dq2zb)$1ncQP#PIDG?bp~-%guU+E`yU?eyCLBaga{tYW|C{q~Qa zTyc2U8JJBP)%ro;@#8LO4>Kk{yZ7^Hg(p<`)Ry0nw|OroaV(j z^5yO=+k^1(%vlS_H-B2^k)W!m;DV=<-vJwPhk^M?z@z0Y@S~qOIO`3r92^BcwKrow zn)fdP6tOPnKfR{#i*eX4$VJy|qF!Pz=v-0y(k{_E=oPbjni9*KRGiXA=QlN73#E6qB)^{ z+|tKW_WStWx|z-p8%oLuj~xKbky@-~jY);U7ZS4j+$;4i^)!$mi4*UV33_6u1;^Nd4g-8n3! ztoKD<6Mq4iF%|F(7b$QYUi0-AW@+T3MPCmWEx@M^4Aq+;_;XOu%W+gls~I}lhvx*_ z%&M<|C{5{kj-u0tst+vL#xjd4S=QMcE1{Ic9i*{b@H6>nj0bNZ8j@%;rXEXEN&=1#j;&B)BP8 z;c{*rSI}M8qBt17W+c?tR;p5nKkHJRRmcRBt}a>$tdqs=r{cLU*ibFWl&@jw3M}<> zXH7p3W-hkt8!OZ{mrFPJ>4Y|mE`Ws&{@N{<$D1E`)Z9o5|9Yh8ebjc^J0Sf7Tjpo9 z)8Nv*D9x738_gRU(+`ef$8+I|VatSzNtEWg-|tr*xZksl@$-kUx4?qlteOMAi&bP9 zAnQ34Pn!P$ATbIeU&l*1y3fb%)c`o@^>?p2I)ZQ%%!)awp^>)aPNPNl4+`_;hM&zp z8Ww=*r842H{w=b?4qN8dBj13y$%htHy{sc!wz59NU6sOomCWkK=5Z6)IG!RhsPBte zG=t%td#?;a8BWJ&-YjN@%jys_P=k;BYgfU${}bylWwv>k8$t!mZyL%<0XW{|7Zx+6&8@uuAqKt7< ztG@~8&H(ohjI?_-PBh$d<2XmN&xXCV^2o56XG;=1%QAQR|^2vP++9t1QWj2Pc_20 zf`iTxLR32e5WOjdFEh_La3_{Z`fCwAR{hyLO<(*cOZD{hpRJd#|7;U{u{8XwzO}-_PPuQ6P+OfXpFoX^(${P+l@uz!mG@?!k!!wPw<+UdsU($~5GhqZ=+9Qn zr0ACA>HQdO5?-)4uGrWkvkc`;1BP#;vBs-%23H`cix&4p986^1_lkL2?jDI7Q<~+4 zu-*;tOKK8NYOQw!QO_&=4+iQJY^ZgDzU|bP-tpC4p|R>^0==IPg?>KvF8H!*8d;cO zlvDg-Rg!NKt%Z+$eRB9rLrwpQq-o}!Po{x&Zf!?HV_RZ*!D?#mS*ESm=&^?=c*6_1 zjC!7B>-f$X_SZ~(_7pxDFMXm?dREw~TIhAP%;tY%7By^KHNwM*94)bsSP1IKZTdV- z!%evjt~+SZ-e2xJ!ec@{U0H}bRaJ_odBxV8Y(67j!cqqqwiGXS9^qjj?Dh=Rmy+Et zF$HVqKaF?CpZ33=(!$+V=0pP@L}^*r4d)8+M6aW|DqgLFc}b%aur^3ATs4oX_13+T z7#%WHPA8(TjbU4~RvG5q3@g`40n0b9 zBK>M_`KdjPb1#>Z#v5+>QcqB-&Zn1yr6O*(EZ(rz<%yvC;g)`?QNUZaqbDKK&k8 zI!=o5JS)V*y~gTVBT2^wYXLc`=&H(T#irC0b)I*R%r-RH>;R}&lbNr1Y08c|Zm)XS z%vr#q0n|qG=(x9wP{34&7;;Pfz5o-|$T2T#p5Te)nRQMwU%HTj8Ftz`g5$AJgI6hu z<-(+bCq5&V&o2k>b=@SXi&%3nv*_A4*%~D$_dcwHzP!{p^Yrr!4x_Pgs4_^pY(2$t z?vl=6kWbSc@*rxq3)(qgtqUH(Qhuq^mip}g6C*2mVD0Fz zX3g9|F1(WjBp3MLqQ306>zHpA!bj^{7FX_swdudlf{$eHG3=&_tBUw?_@E)yO^r;7 zFnNS&Xqe^*(`0E{HU7x*iTA{`@0*e%kw)*=~4K~kmYZ%r!yg{2oaYh*5=$ILeP<*AI6&Rkyp?I)J|uj1Kj zEv2P`?gIwp<-z>`rcf7DZLhI+dsC&ms(iDKlsg%}G_(o8V^eUQx1{BUKUNVPE|J)N z!>S+RN!YxA)fOi)j<2B!nlO#beW*K#t5u)OH;xdcS_FWJN%qnCbiH?Sec$zt9vTVm ztsqm+>jb??2aV-i|Kk+>`NwGgF>ilAtYRqhnc(TOLk@5g5?Qa-K+mcy(STS-v z=>(|yE3eD??YFgIe_lI5ET0?i}f)>~f* zx9X>p@Hcl+`}R@=(b*}V9M^hTm4P!IiNWv9jD?ubiY@8dIt;!kLml|2n)`#W3e0et zOVv+cmE%9oA*x^RgqrQ$e&=8idJEBvVx^K%J*A_xqlMfeti!Ky{3Iz|TMPLzoxeUu-VsTnE3oH%o~)vMx-G@E zt^|U(^bJH0ijXCQ18W6(Y# z1z=E*OWg#JQmYO0z)BzSV^}#7RYv$@Kv}vNRhFa5k6rCY207Cnfu0eLsGtpa$9X6}dcCMx z1Yyzhp(rj$3qB`d!z9%3I%K8eG{jmS4(Z3Ipb~Pz_)Dndgi04s3Hen#qP9E~mCm8k z6jb^SmEc=qNPHqH!I#O9_@^y5>?BI6qQ&Mw*eTfOPqGCp_Ow7S(HF5eZ6_=&K?OKA zd>v5%+QAobm^%ex&nN8kHRSLt;?N(;aRYHkc!nNvSnCaY(IXBC+v*XAwL!2B4yD4! zQ1&i2=o41H`}gU;6INRWoP+a+`Yc@`=7TsW{bOhoVv_8iLmf1*KXz(CTcnU@Fk9mW z>G-hPj>>C_>@t&3)fZmy#Zl8Oa}theK+P8eYY|*r#0jk$BpAs0z@ovUpks5D zSl*(5vTjK+J20Es!yyMp)`#}9=01fy*$%U45Ha5ma6b@1nficlG;6)<0IbVGsgZ4; ztPyqDSz}#X=m#@rjf8KAZn7m)_&g~iMmRG=Pm^QGZTaAQdI?y%!6&-ssQfawK%hz* zF30loStVWEvjR(Y^iS~pTHWbt@2+b;%#%hLSf3nFbL)(e5qO=d)P}I?1fN3ry$HQ4 zFPNn1TqW^UxYT#$#|61Adu5xg=SJfBb@)Ct_mz=WNg@N~C)56y1l0ZqV|k{o|Ki~3 zKT^2C=Btl=%EqjT@wY)1zGmK^@iLF{1=ZP-8%n>A~}SAQpnJsS)Ju{-NJcwO4< zFJTay=k~ePH*3ZQtZtsbeed7Q?yl;VIL&)``|AB8mM)vtANRPv!Rp;{6M}wlR5@H- z=`iVP*dve~K>O0_!FOL7<;jJ#FBElddX-a3adF#UCNRL4;cV5f-kn|I2I+O6XzshqYk-hcz|)pV#Jp zFvMth4DrW|H4(b8$)NwxtxX1n9`X+-0MiKl|Bk-@DlOtxjC&e5*^?e?3TF1P`kyzigM1{ql0D`2z36PhY(V(%-DQCpDJ!85=!s z$%VZSgZTanq{_wxdQPFD_h!{op>ey6UU=;qwd&Bm_eX$+TZ~s7<#MGZNy;+N>=ic4 zqJ?crK zhz*eJB2S8oMoD7q6}H`_CZ_Fn(UqsLy2<4GQW}t~Z~z{;EDg`TR=0SLSK{sco~%UC zv!oH%q>*6TcV^&Y#)}!GI{Y!&-%CjPcCJ@?^)nxRVW)(2QfOnAGtvTl7u~pkzm3`z z*^|Y53CWk(Q^J)mlZNZw85VeBCt$xVH87_1OAUn`ZR%NxbK5|Srm{*+*ZB)4gVb+e z2kw28tvW^)RE@6bl)#|2V|DdnKlrx58!?h&PC(QoXkEwAB~rW+RhJkl{w+CGf3CDi zv5hVNXc`Cz{r7VD6UH zr;eI|$=ylP-fB$3igGjBu1K7_sQOv#iB$FUHnpoLLOcc>C^2fCm$x@U0WN=u%bcpz zLXD4ME;!*-4gjtCnWDw0Q)n_-UWsA7ZmI{_#s$JBk>;YMMX>4DLOISdd`&i-fu_+H zvsvvg)_|!rXt?q#sg#RqjH5}#pcl2NvxL3Rv3tI)QX4bLEU4CLDyWxb#u~C$;&FYZ zo?QAXKzW=L7blE%GX4T#7hGe=y%+dywk+zf;7BMi?Lk#R+cRUP9X*YzoadF;Z#9M9 zMb<~K7DI*+{QfgVjhki^RcGgwXq`?qKF>C;<~AcG+}HV8v8GCCu98yuVBD%oHEh=;sB*`i}rhEhsu1jR>ARajuuRhK|*T z3r7QG_cYKTV&JU2^UQV7HZk?}Iuf}=>TD(nT?|BKdT%;2Oc+)y#!z>PCK|nqYJ8V9 zADn=vxonGmDk*NhajQMltE5nIXhDzKN7Vl_RO%J1QJ8m{ITn)4YsTu;+LaztEgu13 z*?uUVca189Qg_|4VHoxr7!xxXUXVWzD*jU0R<$2_P}dBB#K%mu)C1My#wueMbZwV> zBcJ0wiA5P6EDnHuE1j`dTH`e`2K%5D@bofSv*HF@z6bUm5T}Ft&5-}!N$jUoFj&fu6BnT zaLd47yE6U$|Pk7)p2#;x|cg1Fe!LhH5UY z_rYpgH2Q@B`Na^@P|U0EhvTdt4#eC39QTl&tYgBcS}lD805)47$-vc|1$E$zYs321+>6)^PuYs^b^vm&f`UyEAHyk@r ziaS!0<;Zu+AhM@$0(8IIF+$a8g&BK1G?;U5M&Jg8)vYREOi!!0a~?R?{(6M+Rj4df&B3_wg)z zc@#OIX~Y3TbHX0^uA0t`b=)m9Y*+~Klmm zo`imCyrm@Y7J3qjF?DQGwaK|m!^t#(n5InAB4JtL2)td;ev;G0V@R6yl6uA-A^whUKUELomQGXlcTTadtEJa|f=iO-F6n=s zeX*y?^Vjgmf*37Brj8Wgy;wJlPU121ykgdH+@jyXwvHN#d$1-^v754fzM!KCdngsA zV7~LcDnk2F#3t`oB>ZRn@hYxFilun-nQ1VJp7nGU$1g$1yf-u@Qf~NnAK!ANX0?cb1|nt8lmY1_Qw#`NVH zg^cG#!nu2Hp29{-R6z@eqEPs%78ne_y#rSpPHJE^iqyg26u!!siBmP@vyp`@z}=i8)5yu_h7&u#i}S1ZLxV? zHzNCL1VUA~0E}CM!gz4v!N)~upruu!A|_5MF@vWgDAJW44J)%%MHI&SSe})I1(*v% zSgb_49-{WGlXj@_z?lF>{7~(2A4qxK<=CbY6mF-o0%3qO4R-0%-A(q|+aWdjvJ9jX z7J-9|>_d#4G@fKdxMS1D1oNj3@a=O3^1W#%S5w&0Qxy=PUp_z`-_wJr$n|(-v8KZz zgOM`G_6M3Ah#!O^=hPTwYERXUzqRpFJ`~Qqp3Bv?EI{?K(+6pJ^&(n|jxJ>j%fb4@ zAf+^%Xy~Th#cCW2Drm)@Wi=ty>p!Us3vG}hTB(z=y#B6_DCtJo*O+?y8`ry zFxZGfhouksFv_pW6=?NwI-6#XL3?l#ka~1}3T*}*iw_YEVc&(0;Kl72h|yp3Z00>7 zV(|ni^&O7*aT%VoF{} zB}s4Dd}2@x=~ymmqQaiUl8dR_L-L$KeJj3$`c6!#_hc&D3A?5K26kisb~M3RomBXW zj}#q~9o1+vvCK8ZbD^-CdhBw=KC) zpLDB}9JLw?pI+THCR~sw)V$DE?Fq|I{)& zXjZo@jvJqpldDG@g2!y`?2 zx&?nF2VEdNItVe!xRt;y%oX-+pQ4^3Sd9C_wr7d9H(mAUNX@-U%u-!9?xy$WV-6n^ zPFi^<6ZvS3nre{xqLWHWmrd$Ghi!V@Yii>2dfagGhQ3PJ7iVy@$(S_^X(lp}-$xA1 zk|$#0kN9JSvcV?U2OfZ4>0jS|ix}jLcAHxYilH*8U)Vc~Q!H(U$FGFu>_Rp}Uem7G z`xWF02akpgOiG3unV?*&Rvl#}roG6I*(W7MY1(i4=s31I<3^_FjsNtfsp~*KJQsJp?o!C!Gig{gwHQ4d$ zV`jyw$=hGyaAe5oF|rAKfk+`Ps4sHQn6-dlW2Upp6;H2vcCnHoKVt{j#-9o1)iD$s zJUuC%1>kWvVKwzZ*P)tyqX-7~D_R?IRd2Tq$XHZ_25;SyC(8|7e_PB9&Sq$a*?ol4LLNbkv~py02Q;yz z35X&ho_32U9!A9!qKGJ`og#|X@Rr}<+f1T&bo#ks>i}sBcuqb0=0!pdc>L;d`ehkm z0ssxUwqt;)D~L=s@@Uc00O;ctE(4-2g&giVNsuo`JA$^(0ZG?DCtph&ppTBpSGWvl z^O7TK{A!?sF-U3U#Hb+7@DGRO4DxqF&Z00K3ULz|X zG6V2R1Hxd-CnIilDWN`rM232|Lo*7fPANgUN=6D5^^Za9_aKFW95UJjxYQ2-;Hmw% zv}QDUfPvkC3t?%Na;t4hbyPZEUFFOjFNE+a-CZUzR`MVeIK4{@1Z|L z^Hs)W7Yi`+8#4YfR8O<7t@afYwX3Is26mjx$1Rt4iC`!J5sSXNAjgRAQu)G-25h?; z-Pka|drZN?u-!Mw!&DC`&}1R#sst}q->iSK`a>|l`#5wFgV|(}q|Qni5O6BQYEW22 zDdKsZG`L$q62kjU2Somc_I-E*9_IH@k~&w#=ySxxWG%f5;R6#1jv0)5FVok}PlHk669dSEaQ1&B=-D_KG$yleOm(utM|mmFbSPI zA_D;*y zK;$vz$o)0U)vCD8ZqYCjJIM~2Gk}4Tb>{4=O3``sL z+dQa_aDn?jo;@4Bo|&>|{}aiy|JAc+JHnscnNt4#RhfiMDSu5Ve@(fF|HV@=n^OLo zQvMER3obIH{Qav^@Bh7&zmD)P_n1=t{<*njI7Ha?1DkkKQnK$Ddn#z<*zB_ zuPNoPDI>8dHM1#4^-z{jQ?l;2P{v48)^bz&c9_Ece_bAXQ_5dc%3qo(<*zB_uPNoP eDdn#z7BBw-8&gw7vpzy;u-}TE)~Vt@d854}w}t6%;A*5J-ZrOBFFT zw#9dBL9L5Q@BtApnJ9>FQBZ>hiGYZJAwWVBGMSmX&jh?(_e0mc{@40{xqKjVn0?NE z{PsR)X7=8@K3^34_Q25tF${Z~KmU`(7}ghteX;)i;39ULv>h%3HqHMk7Q%>oeieg%hPa zoc$Ir+8uQIbC(YW4~_iQ+vIg(w%4!|n{Q(I;St}uT>o)9sXwTU_{Q)wfH3()hCUfo zdGK*s{-Zd#p)<)-z9Z=1LD7rLLOM73OftR8Tj2&eRsC%Cte|0lspIQh(bY>6fi9_q zfllapyK@w}gy)u{3l=Lx24M$Ue9%R`LdHVZ7q4)KH#hbAIT#Z(~i3-P4|7$h7 zEevA4F$#U&7^%1Zb2R_W5d$Hl-ppC=dZCrQ4*e4pdL0Vj{OJMEyF&q?KR+t}S9U0a z!q^m($H~6a}Ot1HbyH?o)l=dZuqDl_AFQU zxaF?i=*-cH?GaNA?3pQXJWFYYDS0Aa`g8Nk{LT{g%BQ**x`E`7>Uv^iB!Em^#xmJvwPKJ%X;SqwbB@ zR2VjR@GCZ$mWwwD=~|iU0{zgCIGGCUQ#DA+!V(W<>JmisW5Jg7c#IGE*LhNprwfDS zwVcGZ(_}PdG`3rYspCHfwo`(6epVw#xE^o1ERbfe^|ReQCT`<$OLY;Lc9s_wR4BH} zr&imhy~mec*cdF9s_$p^JE=>+mjB4YJ{3M3!+A>I;)d~Y&JP~^Q&smv2<0m+Cj)1* z3i>-?KW?d=q*yL!O=v$adIaptrt;#9k22q~jqkfEXAPG6zRoTjVbcv8dP0Hg- zO-F<#MXp{z{Cus>usl`&$tWy*mE88rHH$ICRwa;n1c-wTqr$r8NUrH{SnQ3df8}-1 z^}N`wN0LrH0JJgK;8ugY`j-4UE@Ks}iNI2?jumDZ2=}&Y1Yeq6Y;a-+=Ht(&H`W;< zQ}s(h!g~U9Q7pl=7wIgS9(+qpTA(P7-mMwK3ZGnxVd>Ub+FDKJ$claz$;a`f!<*OO zu^VWY=)}KJfzDNXce1eIZJD}`DoQI(3?SWSNj;`i-A~?=Wf|RM@mI%}yJDNl$S&WR z&9z{_(QoNSAB_dKhS)u@JwA6aETZbZp@Fm+tY^i1>E@0A)Af9E5`HJ4k9tN4Ed2+r zr^WVvaP9lSHN@3aar47@*vJTQ+}-0@yu%hsV$Gr;ikd9v@s0=7mCzSDHcRUUu#PeT zE+3OmxP0ku;w-_v_L^Ya%}GhYxA>6ffOg0~iQ92V5#TNs^o>dYgCnkME;ZdJ&w55@ z3yc%^QnSm5=vFQfFP;VpBYNq5!;=8(0D;YuFCBDBKt9MP=gM#5A$q zAQwopOIm_;$sbXH1FANrgSx5Fl*&b901({1_TAV@_=?JRT8Rna$zlDl8BRPKjdp}w zFZJM;KZ&Ml;_!i(v27IgOf96vMF&I+!8b`YH8u{nOy|qe#!kc3EMh^lXfwX~YB`0( z8R{aUTLt6?;)&QEr*E)SC$oT)GLc+16>!xm4O=I*TSC<4-vQL)R4rb#PxLmhEUKq! zEM2pMMmpcbFz4p2Y4Vc-9IS4=$QIhy+lBAu3HhL3kbr(lFk`s-M^@f~O{W_2yo??Ur%YL!Y z=XgV~vaJxstgc(pTcYsVN{E3ALBWP~VdfXX^#-~6?SgV}C9CT){oP08vMFFm6nTqI z@usMORe|sV?)+RqJxj91ErX3$WqWQANN~Ilvu3N+U)La^AZ%JX<1Ag%z``pqzfMQn zBP!+P0Q0q)5URStN=yzfz6z9QSLHQk*k@+gh!?^nfiY{u_j5W7if}MzPMZa%XWR0> z0x_kwVqNTel*pwY*x#4e;jW_~j2`LJ#J0tDa=0s4*x%h=`q0#J4&riGr@ADx-ofQU zrpjC`>Re`9i}*bEOOhKaG)L2M!?rJABdjtCc*jHwe+DeuRSnK5Y;2F06SjlYS_F%5 z_XV(b5)Bu19OwSyZ^PtZd?A_&0;Aq$tW1HPNm!(C{r&gjwQdVE07?Uff3 zeR{Z|c)+#ryP3&CgWvFb{y8w`Vn2y`C!+WAnd-UuMOF zFdK#Ky9d9F?Y}8aBx~F+@Vn|=A1vtey6L!$Sw*!u?3`_9$-mf8DW-TNic z`|Si@)4gAdy&vZPkEsZ{S=F2&X_X7L^@O%YxDKbshnfWheK4z>d$Cx zrpI*igZ#ton&8*zkx5Bi!bVGiiteD=AUjDXP2eZZlLTxTPc0kJllKr06HsRw%?edZ z9d$#8AEQDi@+)G^C}UIfdJ_FRv0Ee4>ZoH26rya%qMyGvN3tRv7hUN&5_vgcZM!$n zcV$W6L7}2B|N2r48#WI00%Vx**^rR@&6yW7o$?#lBbT0Rd!m?GAbwdNT1huUCNn3{ zdiGSE{KV^QPWd6%Rdb#{HZ}wwZ zQfDEF3m=r=9_!BRrTS}4k&}C}Q1qMzNQPfnO&A5ldvPEMEy zq_D)&Xac5oF4VPH=v8Ae$oQeVyF-BU!lTJ`WhmV)5U+v+X*P%qX=~&1*SF}%P_dn} z$7xl!A{&q*8DxeyK9Cyi8l@QN&^u&5=)HK2%&Kf@*Mx@?GacONb?u0Yo}eqQT3fvG zgvYwo{)@E$=0~E;xgg1y)vj6nuQhz*4cy~&p{%Q661tx}x|F@4UW4{@ zbIL{l z8CI5?h$whjTodO_J#mdHzv0L*in%lGAY!LGW$I*V41rE4@y~cnH$`Cek`&CtCqLrYB5on-D+fo!Y3pc`+)kAS!ISWU3V*v$AX>kqUY4%*IiUrVO)v4Sd@ zF{fK^#Vw>-h~dQ@q4CtphNo3}} z;wH|e;zn1s_5@!c!oKMsSx2jiaO=Y^&ARa7N!`&OQ4gCUJHm{Mh$qxqx&H|9(SE2T z$;30gn1SN9sF4oS(O1T#)bp6~5lY0;*SfAi(*D@vXu7tOT&ruX<&^d5)?aYj6Wkhb zUP9iTF0+@cNbB~h#G!}~P7jMbQK{=nJ3F{LYKEXd2sqE4X?<`;kg)(?Wl~Bvgi_n|-^^ z@kXXIIp$r5lMx^;PcIc3cwMoXmJ!N{w5osAq9aI8e|<6|QH2gmEnZs;B~7Sqoo4Vq zwIvcMD$GrnDGTw=l_M-8RMY4%So;&R_KZJ)N@{_caLgYwu^Xq z0bGW2wZ*hkEX{scoU}p5NwhYf39(7sZR8gs^+hKX9EKUuP0WEQE8Mh(K9QE&YPblG zq$#{jtHe!_vsLD!PeDv;7LC0vMJ`+ya`|o*(c7hpwd z`&JJdXB4r3}3_;-_yoU%0@U#I|D;WYj>_A5K1cu;P5x@|N9fa|Spm414Mg&s| zLl`?05z2pJSb0Ni>@!6<2tJ4)n20RjGZY?&-Gd1Q4 zBDgum1JRgabmSdibQ*#S_$&c8jO%1Ti@6CivhO0Nd!d6ZTg40!^Xyz^xD5@DF+;>T zdoME_1YLgHE{GWEsQNi;nS-?x)TzgvIn3Ar<=3Enk8U$Az`&97TO3LqM{M#@M5||n zLyTAWIZT}a8m`M=*xyBO1Wm6Tmd=A9pC94ikILk;{2XW=@kWauI|R5Q(C#w3f~nze z+g#v~0HyxH+2)|M~=)}Z2MMJ;2& z*$YtVNcl{Sbnp5Sj}IXQwbAO+dpaW{Bc6dj&IGi-w;u z!<}eYS&q=kf*yozn~NA!P{DBDB4*<SH!;v;$8v##D+lZul zq7wzHI#!0TI*0&On_ZFm?Fdk;fQw!N{AwHosM-(Cu1ALl=6E~sW1OuvUqXarMxiJL z+S^46W<|Uf5sDoI)YdSosMtZch6w)M1hhgEoy|E_{&!?wcyT0Xkkrv}$K=y_@CGB! zf)_9Q_V_jg3hQ$*`-3?21al6yc|>tb?tT0H_&;Ncht`b8~Z(%eiTbYOs%d8Eobh| z9f>_lO=%1XD3l*z6iP?Twp^Z6K%!o%PTbLc)8HDZYBoH8xvk2wq+3A3IVoDvOb@>db?wt_5#{-#apFnjX``WcTNA?A`eGG`A2pIyV^Kfu;-|{UVd&KG8nh2uO|HwGj_fq_onecZX^U8>b3tKt z8FjXdscA2rMGe^+On(+kM>P@9oTohxog|}-`Otqs-@7JnO{>^0yHrHyi)~BoQ=$LW zd;~P;=SChN6Q~FKi9*kfrS5hKRTE6RiWtm&5a+7UOH?&wwyb!`?hF_9L6=9p m|MX(e8wb66;eX;hNSOM`r+#JoT|Y)gAAjzmPjY96Z~G@sx5tA3 literal 0 HcmV?d00001 diff --git a/electron/cloud.js b/electron/cloud.js new file mode 100644 index 00000000..d7b74809 --- /dev/null +++ b/electron/cloud.js @@ -0,0 +1,240 @@ +import crypto from 'node:crypto'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { safeStorage } from 'electron'; + +function encryptSecret(secret) { + if (!safeStorage.isEncryptionAvailable()) { + return { encrypted: false, value: secret }; + } + + return { + encrypted: true, + value: safeStorage.encryptString(secret).toString('base64'), + }; +} + +function decryptSecret(record) { + if (!record?.value) return null; + if (!record.encrypted) return record.value; + return safeStorage.decryptString(Buffer.from(record.value, 'base64')); +} + +export class CloudController { + constructor({ storePath, controlPlaneUrl, callbackUrl, onChange }) { + this.storePath = storePath; + this.controlPlaneUrl = controlPlaneUrl; + this.callbackUrl = callbackUrl; + this.onChange = onChange; + this.cloudAccount = null; + this.cloudEnvironments = []; + this.authState = 'logged_out'; + } + + getAccount() { + return this.cloudAccount; + } + + getAuthState() { + return this.authState; + } + + getEnvironments() { + return this.cloudEnvironments; + } + + getEnvironmentUrl(environment) { + return environment.access_url || `https://${environment.subdomain}.cloudcli.ai`; + } + + async getEnvironmentLaunchUrl(environment) { + if (!environment?.id) { + return this.getEnvironmentUrl(environment); + } + + const data = await this.cloudApi(`/api/v1/environments/${encodeURIComponent(environment.id)}/launch`, { + method: 'POST', + }); + + return data.launch_url || data.environment_url || this.getEnvironmentUrl(environment); + } + + findEnvironment(environmentId) { + return this.cloudEnvironments.find((item) => item.id === environmentId) || null; + } + + async loadCloudAccount() { + try { + const raw = await fs.readFile(this.storePath, 'utf8'); + const stored = JSON.parse(raw); + const apiKey = decryptSecret(stored.apiKey); + this.cloudAccount = { + deviceId: stored.deviceId || crypto.randomUUID(), + email: stored.email || null, + apiKey: apiKey || null, + }; + this.authState = apiKey ? 'connected' : (stored.email ? 'expired' : 'logged_out'); + return this.cloudAccount; + } catch { + this.cloudAccount = { + deviceId: crypto.randomUUID(), + email: null, + apiKey: null, + }; + this.authState = 'logged_out'; + return this.cloudAccount; + } + } + + async saveCloudAccount(account) { + const payload = { + deviceId: account.deviceId || crypto.randomUUID(), + email: account.email || null, + apiKey: account.apiKey ? encryptSecret(account.apiKey) : null, + }; + + await fs.mkdir(path.dirname(this.storePath), { recursive: true }); + await fs.writeFile(this.storePath, JSON.stringify(payload, null, 2), 'utf8'); + this.cloudAccount = { + deviceId: payload.deviceId, + email: payload.email, + apiKey: account.apiKey || null, + }; + this.authState = account.apiKey ? 'connected' : 'logged_out'; + this.onChange?.(); + return this.cloudAccount; + } + + async clearCloudAccount() { + this.cloudAccount = { + deviceId: crypto.randomUUID(), + email: null, + apiKey: null, + }; + this.cloudEnvironments = []; + this.authState = 'logged_out'; + await fs.rm(this.storePath, { force: true }); + this.onChange?.(); + } + + async invalidateCloudAccount() { + this.cloudEnvironments = []; + if (!this.cloudAccount) { + this.cloudAccount = { + deviceId: crypto.randomUUID(), + email: null, + apiKey: null, + }; + } else { + this.cloudAccount = { + ...this.cloudAccount, + apiKey: null, + }; + } + this.authState = this.cloudAccount.email ? 'expired' : 'logged_out'; + const payload = { + deviceId: this.cloudAccount.deviceId, + email: this.cloudAccount.email || null, + apiKey: null, + }; + await fs.mkdir(path.dirname(this.storePath), { recursive: true }); + await fs.writeFile(this.storePath, JSON.stringify(payload, null, 2), 'utf8'); + this.onChange?.(); + } + + async cloudApi(pathname, options = {}) { + if (!this.cloudAccount?.apiKey) { + throw new Error('Connect your CloudCLI account first.'); + } + + const response = await fetch(`${this.controlPlaneUrl}${pathname}`, { + ...options, + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': this.cloudAccount.apiKey, + ...(options.headers || {}), + }, + }); + + const body = await response.json().catch(() => ({})); + if (!response.ok) { + if (response.status === 401 || response.status === 403) { + await this.invalidateCloudAccount(); + } + throw new Error(body.error || `CloudCLI API request failed: ${response.status}`); + } + + return body; + } + + async refreshCloudEnvironments() { + if (!this.cloudAccount?.apiKey) { + this.cloudEnvironments = []; + this.onChange?.(); + return []; + } + + const data = await this.cloudApi('/api/v1/environments'); + this.cloudEnvironments = data.environments || []; + this.onChange?.(); + return this.cloudEnvironments; + } + + async startEnvironment(environment) { + await this.cloudApi(`/api/v1/environments/${encodeURIComponent(environment.id)}/start`, { + method: 'POST', + }); + } + + async stopEnvironment(environment) { + await this.cloudApi(`/api/v1/environments/${encodeURIComponent(environment.id)}/stop`, { + method: 'POST', + }); + } + + async getEnvironmentCredentials(environment) { + return this.cloudApi(`/api/v1/environments/${encodeURIComponent(environment.id)}/credentials`); + } + + async startEnvironmentAndWait(environment, timeoutMs) { + await this.startEnvironment(environment); + + const startedAt = Date.now(); + while (Date.now() - startedAt < timeoutMs) { + const environments = await this.refreshCloudEnvironments(); + const current = environments.find((env) => env.id === environment.id); + if (current?.status === 'running') { + return current; + } + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + + throw new Error(`${environment.name} did not become ready in time.`); + } + + buildConnectUrl() { + if (!this.cloudAccount?.deviceId) { + this.cloudAccount = { + deviceId: crypto.randomUUID(), + email: null, + apiKey: null, + }; + } + + const connectUrl = new URL('/auth/app-connect', this.controlPlaneUrl); + connectUrl.searchParams.set('device_id', this.cloudAccount.deviceId); + connectUrl.searchParams.set('callback_url', this.callbackUrl); + connectUrl.searchParams.set('app_surface', 'cloudcli_desktop'); + connectUrl.searchParams.set('client_platform', 'desktop'); + return connectUrl.toString(); + } + + async saveFromCallback({ apiKey, email }) { + await this.saveCloudAccount({ + deviceId: this.cloudAccount?.deviceId || crypto.randomUUID(), + email, + apiKey, + }); + return this.cloudAccount; + } +} diff --git a/electron/desktopWindow.js b/electron/desktopWindow.js new file mode 100644 index 00000000..a22aa49d --- /dev/null +++ b/electron/desktopWindow.js @@ -0,0 +1,685 @@ +import { BrowserView, BrowserWindow, Menu, Tray, nativeImage, nativeTheme, session } from 'electron'; + +const TITLEBAR_HEIGHT = 44; + +function escapeHtml(value) { + return String(value == null ? '' : value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +function buildPlaceholderHtml(title, message, logs = []) { + const logHtml = logs.length + ? `
${logs.map(escapeHtml).join('\n')}
` + : '
Waiting for process output...
'; + return [ + '', + '', + '
', + `
${escapeHtml(message || `Opening ${title}...`)}
`, + logHtml, + '
', + ].join(''); +} + +export class DesktopWindowManager { + constructor({ + appName, + getWindowIconPath, + getLauncherPath, + getPreloadPath, + openExternalUrl, + getDesktopState, + getDisplayTargetName, + getRemoteEnvironmentMenuItems, + getCloudState, + getLocalState, + actions, + tabs, + }) { + this.appName = appName; + this.getWindowIconPath = getWindowIconPath; + this.getLauncherPath = getLauncherPath; + this.getPreloadPath = getPreloadPath; + this.openExternalUrl = openExternalUrl; + this.getDesktopState = getDesktopState; + this.getDisplayTargetName = getDisplayTargetName; + this.getRemoteEnvironmentMenuItems = getRemoteEnvironmentMenuItems; + this.getCloudState = getCloudState; + this.getLocalState = getLocalState; + this.actions = actions; + this.tabs = tabs; + + this.mainWindow = null; + this.tray = null; + this.launcherLoaded = false; + this.activeContentView = null; + this.tabViews = new Map(); + } + + getMainWindow() { + return this.mainWindow; + } + + getTrayImage() { + const image = nativeImage.createFromPath(this.getWindowIconPath()); + return image.resize({ width: 18, height: 18 }); + } + + getContentViewBounds() { + if (!this.mainWindow) return { x: 0, y: TITLEBAR_HEIGHT, width: 0, height: 0 }; + const [width, height] = this.mainWindow.getContentSize(); + return { + x: 0, + y: TITLEBAR_HEIGHT, + width, + height: Math.max(0, height - TITLEBAR_HEIGHT), + }; + } + + configureChildWebContents(webContents) { + webContents.setWindowOpenHandler(({ url }) => { + void this.openExternalUrl(url).catch((error) => this.actions.showError('Could not open external link', error)); + return { action: 'deny' }; + }); + } + + detachActiveContentView() { + if (!this.mainWindow || this.mainWindow.isDestroyed() || !this.activeContentView) return; + try { + if (this.mainWindow.getBrowserViews().includes(this.activeContentView)) { + this.mainWindow.removeBrowserView(this.activeContentView); + } + } catch { + // BrowserViews may already be gone during BrowserWindow teardown. + } + this.activeContentView = null; + } + + getOrCreateTabView(tabId) { + let view = this.tabViews.get(tabId); + if (view) return view; + + view = new BrowserView({ + webPreferences: { + contextIsolation: true, + nodeIntegration: false, + sandbox: true, + preload: this.getPreloadPath(), + }, + }); + this.configureChildWebContents(view.webContents); + this.tabViews.set(tabId, view); + return view; + } + + attachContentView(view) { + if (!this.mainWindow || this.mainWindow.isDestroyed()) return; + if (this.activeContentView && this.activeContentView !== view) { + this.detachActiveContentView(); + } + this.activeContentView = view; + try { + if (!this.mainWindow.getBrowserViews().includes(view)) { + this.mainWindow.addBrowserView(view); + } + } catch { + return; + } + view.setBounds(this.getContentViewBounds()); + view.setAutoResize({ width: true, height: true }); + } + + async showTabPlaceholder(target, message) { + const tabId = this.tabs.getTabIdForTarget(target); + const view = this.getOrCreateTabView(tabId); + this.attachContentView(view); + const html = buildPlaceholderHtml(target.name || this.appName, message); + await view.webContents.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`); + view.__cloudcliStartupHtml = html; + view.__cloudcliLoadedUrl = null; + } + + async showLocalStartupTarget(target, logs) { + const tabId = this.tabs.getTabIdForTarget(target); + const view = this.getOrCreateTabView(tabId); + if (view.__cloudcliLoadingUrl) return; + this.attachContentView(view); + const html = buildPlaceholderHtml(target.name || this.appName, 'Starting Local CloudCLI...', logs); + if (view.__cloudcliStartupHtml === html) return; + await view.webContents.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`); + view.__cloudcliStartupHtml = html; + view.__cloudcliLoadedUrl = null; + } + + async showContentTarget(target) { + const tabId = this.tabs.getTabIdForTarget(target); + const view = this.getOrCreateTabView(tabId); + this.attachContentView(view); + if (view.__cloudcliLoadedUrl !== target.url) { + view.__cloudcliLoadingUrl = target.url; + try { + await view.webContents.loadURL(target.url); + view.__cloudcliLoadedUrl = target.url; + view.__cloudcliStartupHtml = null; + } finally { + if (view.__cloudcliLoadingUrl === target.url) { + view.__cloudcliLoadingUrl = null; + } + } + } + } + + destroyTabView(tabId) { + const view = this.tabViews.get(tabId); + if (!view) return; + if (this.mainWindow && !this.mainWindow.isDestroyed()) { + try { + if (this.mainWindow.getBrowserViews().includes(view)) { + this.mainWindow.removeBrowserView(view); + } + } catch { + // Ignore teardown races; Electron owns final destruction during quit. + } + } + if (this.activeContentView === view) { + this.activeContentView = null; + } + try { + if (!view.webContents.isDestroyed()) { + view.webContents.destroy(); + } + } catch { + // The view may already be destroyed by its parent BrowserWindow. + } + this.tabViews.delete(tabId); + } + + emitDesktopState() { + if (!this.mainWindow || this.mainWindow.webContents.isDestroyed()) return; + this.mainWindow.webContents.send('cloudcli-desktop:state-updated', this.getDesktopState()); + } + + async showTarget(target, { trackTab = true } = {}) { + if (!this.mainWindow) return; + if (trackTab) { + this.tabs.upsertTarget(target); + } + this.actions.setActiveTarget(target); + this.buildAppMenu(); + this.mainWindow.setTitle(`${this.appName} - ${target.name}`); + await this.showContentTarget(target); + this.emitDesktopState(); + } + + async showLauncher() { + if (!this.mainWindow) return; + const target = { kind: 'launcher', name: this.appName, url: null }; + this.tabs.upsertTarget(target); + this.actions.setActiveTarget(target); + this.detachActiveContentView(); + this.buildAppMenu(); + this.mainWindow.setTitle(this.appName); + if (!this.launcherLoaded) { + await this.mainWindow.loadFile(this.getLauncherPath()); + this.launcherLoaded = true; + } else { + this.emitDesktopState(); + } + } + + async switchDesktopTab(tabId) { + const tab = this.tabs.activate(tabId); + if (!tab || !this.mainWindow) return this.getDesktopState(); + + if (tab.id === 'home' || tab.kind === 'launcher') { + await this.showLauncher(); + return this.getDesktopState(); + } + + if (!tab.target?.url) { + throw new Error('This tab does not have a target URL.'); + } + + await this.showTarget(tab.target, { trackTab: false }); + return this.getDesktopState(); + } + + async closeDesktopTab(tabId) { + const tab = this.tabs.remove(tabId); + if (!tab) return this.getDesktopState(); + this.destroyTabView(tabId); + if (this.tabs.activeTabId === 'home') { + await this.showLauncher(); + } else { + this.emitDesktopState(); + } + return this.getDesktopState(); + } + + buildEnvironmentActionsSubmenu(environment) { + const items = []; + const statusSuffix = environment.status === 'running' ? '' : ` (${environment.status})`; + items.push({ + label: 'Open Environment', + click: () => void this.actions.openEnvironmentInDesktop(environment) + .catch((error) => this.actions.showError(`Could not open ${environment.name || environment.subdomain}${statusSuffix}`, error)), + }); + items.push({ + label: 'Open in Browser', + click: () => void this.actions.openEnvironmentInBrowser(environment) + .catch((error) => this.actions.showError('Could not open environment in browser', error)), + }); + items.push({ + label: 'Open in VS Code', + click: () => void this.actions.openEnvironmentInIde(environment, 'vscode') + .catch((error) => this.actions.showError('Could not open environment in VS Code', error)), + }); + items.push({ + label: 'Open in Cursor', + click: () => void this.actions.openEnvironmentInIde(environment, 'cursor') + .catch((error) => this.actions.showError('Could not open environment in Cursor', error)), + }); + items.push({ + label: 'Open SSH Terminal', + click: () => void this.actions.openEnvironmentInSsh(environment) + .catch((error) => this.actions.showError('Could not open SSH terminal', error)), + }); + items.push({ + label: 'Copy Mobile/Web URL', + click: () => this.actions.copyText(this.actions.getEnvironmentUrl(environment)), + }); + if (environment.status !== 'running') { + items.unshift({ + label: environment.status === 'paused' ? 'Resume' : 'Start', + click: () => void this.actions.startEnvironment(environment) + .catch((error) => this.actions.showError('Could not start environment', error)), + }); + } + if (environment.status === 'running') { + items.push({ + label: 'Stop', + click: () => void this.actions.stopEnvironment(environment) + .catch((error) => this.actions.showError('Could not stop environment', error)), + }); + } + return items; + } + + buildTrayEnvironmentSection() { + const cloudState = this.getCloudState(); + if (!cloudState.account?.apiKey) { + return [ + { + label: cloudState.account?.email ? `Reconnect ${cloudState.account.email}` : 'Login', + click: () => void this.actions.connectCloudAccount() + .catch((error) => this.actions.showError('Could not connect CloudCLI account', error)), + }, + ]; + } + + if (!cloudState.environments.length) { + return [{ label: 'No environments found', enabled: false }]; + } + + return cloudState.environments.map((environment) => ({ + label: `${environment.name || environment.subdomain} - ${environment.status}`, + submenu: this.buildEnvironmentActionsSubmenu(environment), + })); + } + + buildAppMenu() { + if (!this.mainWindow) return; + const cloudState = this.getCloudState(); + const localState = this.getLocalState(); + const remoteItems = this.getRemoteEnvironmentMenuItems(); + const cloudAccountLabel = cloudState.account?.apiKey + ? (cloudState.account?.email ? `Connected: ${cloudState.account.email}` : 'CloudCLI Connected') + : (cloudState.account?.email ? `Reconnect: ${cloudState.account.email}` : 'Connect CloudCLI Account...'); + + const template = [ + { + label: this.appName, + submenu: [ + { label: `About ${this.appName}`, role: 'about' }, + { type: 'separator' }, + { + label: 'Show Launcher', + accelerator: 'CmdOrCtrl+Shift+L', + click: () => void this.showLauncher().catch((error) => this.actions.showError('Could not show launcher', error)), + }, + { + label: 'Switch Environment', + accelerator: 'CmdOrCtrl+Shift+E', + click: () => void this.actions.showEnvironmentPicker().catch((error) => this.actions.showError('Could not switch environment', error)), + }, + { type: 'separator' }, + { + label: 'Services', + submenu: [ + { + label: 'Computer Use Preview', + click: () => void this.actions.showComputerUsePreview(), + }, + ], + }, + { + label: 'Diagnostics', + submenu: [ + { + label: 'Copy Diagnostics', + click: () => void this.actions.copyDiagnostics(), + }, + ], + }, + { type: 'separator' }, + { + label: process.platform === 'darwin' ? `Hide ${this.appName}` : 'Hide', + role: 'hide', + visible: process.platform === 'darwin', + }, + { label: 'Hide Others', role: 'hideOthers', visible: process.platform === 'darwin' }, + { label: 'Show All', role: 'unhide', visible: process.platform === 'darwin' }, + { type: 'separator', visible: process.platform === 'darwin' }, + { label: `Quit ${this.appName}`, accelerator: 'CmdOrCtrl+Q', role: 'quit' }, + ], + }, + { + label: 'Environment', + submenu: [ + { + label: 'Show Launcher', + accelerator: 'CmdOrCtrl+Shift+L', + click: () => void this.showLauncher().catch((error) => this.actions.showError('Could not show launcher', error)), + }, + { + label: 'Switch Environment', + accelerator: 'CmdOrCtrl+Shift+E', + click: () => void this.actions.showEnvironmentPicker().catch((error) => this.actions.showError('Could not switch environment', error)), + }, + { type: 'separator' }, + { + label: 'Open Local CloudCLI', + accelerator: 'CmdOrCtrl+L', + click: () => void this.actions.openLocalInDesktop().catch((error) => this.actions.showError('Could not open local CloudCLI', error)), + }, + { + label: 'Open Local Web UI in Browser', + accelerator: 'CmdOrCtrl+Shift+W', + click: () => void this.actions.openLocalWebUi().catch((error) => this.actions.showError('Could not open local web UI', error)), + }, + { + label: 'Copy Local Web URL', + accelerator: 'CmdOrCtrl+Shift+U', + click: () => void this.actions.copyLocalWebUrl().catch((error) => this.actions.showError('Could not copy local web URL', error)), + }, + { type: 'separator' }, + { + label: 'Keep Local Server Running After Quit', + type: 'checkbox', + checked: localState.desktopSettings.keepLocalServerRunning, + click: (menuItem) => void this.actions.updateDesktopSetting('keepLocalServerRunning', menuItem.checked) + .catch((error) => this.actions.showError('Could not update desktop setting', error)), + }, + { + label: 'Allow LAN Access to Local Server', + type: 'checkbox', + checked: localState.desktopSettings.exposeLocalServerOnNetwork, + click: (menuItem) => void this.actions.updateDesktopSetting('exposeLocalServerOnNetwork', menuItem.checked) + .catch((error) => this.actions.showError('Could not update desktop setting', error)), + }, + ], + }, + { + label: 'Cloud', + submenu: [ + { + label: cloudAccountLabel, + accelerator: 'CmdOrCtrl+Shift+C', + click: () => void this.actions.connectCloudAccount().catch((error) => this.actions.showError('Could not connect CloudCLI account', error)), + }, + { + label: 'Refresh Cloud Environments', + click: () => void this.actions.refreshCloudEnvironments().catch((error) => this.actions.showError('Could not load CloudCLI environments', error)), + enabled: Boolean(cloudState.account?.apiKey), + }, + { + label: 'Disconnect Cloud Account', + click: () => void this.actions.clearCloudAccount().catch((error) => this.actions.showError('Could not disconnect cloud account', error)), + enabled: Boolean(cloudState.account?.apiKey), + }, + { type: 'separator' }, + { + label: 'Remote Environments', + submenu: remoteItems, + }, + ], + }, + { + label: 'Edit', + submenu: [ + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + { role: 'selectAll' }, + ], + }, + { + label: 'View', + submenu: [ + { role: 'reload' }, + { role: 'forceReload' }, + { role: 'toggleDevTools' }, + { type: 'separator' }, + { role: 'resetZoom' }, + { role: 'zoomIn' }, + { role: 'zoomOut' }, + { type: 'separator' }, + { role: 'togglefullscreen' }, + ], + }, + { + label: 'Window', + submenu: [ + { role: 'minimize' }, + { role: 'zoom' }, + ...(process.platform === 'darwin' ? [{ type: 'separator' }, { role: 'front' }] : []), + ], + }, + { + label: 'Help', + submenu: [ + { + label: 'Open cloudcli.ai', + click: () => void this.actions.openCloudDashboard(), + }, + { + label: 'Copy Diagnostics', + click: () => void this.actions.copyDiagnostics(), + }, + ], + }, + ]; + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); + this.buildTrayMenu(); + } + + buildTrayMenu() { + if (!this.tray) return; + const cloudState = this.getCloudState(); + const localState = this.getLocalState(); + + const template = [ + { + label: 'Local', + submenu: [ + { + label: localState.localServerRunning ? 'Open Local in CloudCLI' : 'Start Local in CloudCLI', + click: () => void this.actions.openLocalInDesktop().catch((error) => this.actions.showError('Could not open local CloudCLI', error)), + }, + { + label: 'Open Local in Browser', + click: () => void this.actions.openLocalWebUi().catch((error) => this.actions.showError('Could not open local web UI', error)), + }, + { + label: 'Copy Local URL', + click: () => void this.actions.copyLocalWebUrl().catch((error) => this.actions.showError('Could not copy local web URL', error)), + }, + ], + }, + { + label: 'Cloud Environments', + submenu: this.buildTrayEnvironmentSection(), + }, + { type: 'separator' }, + { + label: cloudState.account?.email ? `Connected: ${cloudState.account.email}` : 'Login', + click: () => void this.actions.connectCloudAccount().catch((error) => this.actions.showError('Could not connect CloudCLI account', error)), + }, + { + label: 'Disconnect Cloud Account', + click: () => void this.actions.clearCloudAccount().catch((error) => this.actions.showError('Could not disconnect cloud account', error)), + enabled: Boolean(cloudState.account?.apiKey), + }, + { type: 'separator' }, + { + label: `Quit ${this.appName}`, + role: 'quit', + }, + ]; + + this.tray.setToolTip(`${this.appName}${this.actions.getActiveTarget()?.name ? ` - ${this.actions.getActiveTarget().name}` : ''}`); + this.tray.setContextMenu(Menu.buildFromTemplate(template)); + } + + async showDesktopAppMenu() { + if (!this.mainWindow) return this.getDesktopState(); + const menu = Menu.buildFromTemplate([ + { + label: 'Copy Diagnostics', + click: () => void this.actions.copyDiagnostics(), + }, + { + label: 'Computer Use Preview', + click: () => void this.actions.showComputerUsePreview(), + }, + ]); + menu.popup({ window: this.mainWindow }); + return this.getDesktopState(); + } + + async showActiveEnvironmentActionsMenu() { + if (!this.mainWindow) return this.getDesktopState(); + const activeTarget = this.actions.getActiveTarget(); + if (activeTarget?.kind !== 'remote') return this.getDesktopState(); + + const environment = this.getCloudState().environments.find((item) => item.id === activeTarget.id); + if (!environment) return this.getDesktopState(); + + const menu = Menu.buildFromTemplate(this.buildEnvironmentActionsSubmenu(environment)); + menu.popup({ window: this.mainWindow }); + return this.getDesktopState(); + } + + async showEnvironmentActionsMenu(environmentId) { + if (!this.mainWindow) return this.getDesktopState(); + const environment = this.getCloudState().environments.find((item) => item.id === environmentId); + if (!environment) return this.getDesktopState(); + + const menu = Menu.buildFromTemplate(this.buildEnvironmentActionsSubmenu(environment)); + menu.popup({ window: this.mainWindow }); + return this.getDesktopState(); + } + + configurePermissions() { + session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => { + const sourceUrl = webContents.getURL(); + const isCloudCliOrigin = sourceUrl.startsWith('http://127.0.0.1:') + || sourceUrl.startsWith(this.getCloudState().controlPlaneUrl) + || /^https:\/\/[a-z0-9-]+\.cloudcli\.ai/i.test(sourceUrl); + const allowedPermissions = new Set(['clipboard-read', 'media']); + callback(isCloudCliOrigin && allowedPermissions.has(permission)); + }); + } + + createTray() { + if (this.tray) return; + this.tray = new Tray(this.getTrayImage()); + this.tray.on('click', () => { + if (!this.mainWindow) return; + if (this.mainWindow.isVisible()) { + this.mainWindow.focus(); + } else { + this.mainWindow.show(); + } + }); + this.buildTrayMenu(); + } + + async createWindow() { + this.mainWindow = new BrowserWindow({ + width: 1440, + height: 960, + minWidth: 1024, + minHeight: 720, + show: false, + backgroundColor: '#0f172a', + title: this.appName, + icon: this.getWindowIconPath(), + titleBarStyle: 'hidden', + ...(process.platform === 'darwin' + ? { trafficLightPosition: { x: 18, y: 14 } } + : { + titleBarOverlay: { + color: nativeTheme.shouldUseDarkColors ? '#111111' : '#f7f8fa', + symbolColor: nativeTheme.shouldUseDarkColors ? '#a1a1a1' : '#5b6470', + height: 44, + }, + }), + webPreferences: { + contextIsolation: true, + nodeIntegration: false, + sandbox: true, + preload: this.getPreloadPath(), + }, + }); + + this.mainWindow.once('ready-to-show', () => { + this.mainWindow?.show(); + }); + + this.mainWindow.webContents.setWindowOpenHandler(({ url }) => { + void this.openExternalUrl(url).catch((error) => this.actions.showError('Could not open external link', error)); + return { action: 'deny' }; + }); + + this.mainWindow.on('resize', () => { + if (this.activeContentView) { + this.activeContentView.setBounds(this.getContentViewBounds()); + } + }); + + this.mainWindow.on('closed', () => { + this.tabViews.clear(); + this.activeContentView = null; + this.mainWindow = null; + this.launcherLoaded = false; + }); + + this.buildAppMenu(); + await this.showLauncher(); + } +} diff --git a/electron/launcher/index.html b/electron/launcher/index.html new file mode 100644 index 00000000..51ac2a00 --- /dev/null +++ b/electron/launcher/index.html @@ -0,0 +1,14 @@ + + + + + + +CloudCLI Desktop + + + +
+ + + diff --git a/electron/launcher/launcher.css b/electron/launcher/launcher.css new file mode 100644 index 00000000..76fbed5f --- /dev/null +++ b/electron/launcher/launcher.css @@ -0,0 +1 @@ +*{box-sizing:border-box}html,body{margin:0;height:100%}:root{--bg:#0a0a0a;--s1:#111111;--s2:#1a1a1a;--s3:#202020;--b-subtle:#1f1f1f;--b:#262626;--b-strong:#333333;--tx:#fafafa;--tx2:#a1a1a1;--tx3:#6b7280;--brand:#0b60ea;--brand-2:#60A5FA;--brand-faint:rgba(11,96,234,.16);--ok:#10b981;--warn:#f59e0b;--err:#ef4444;--tab-hover-bg:rgba(255,255,255,.10);--tab-active-bg:rgba(255,255,255,.16);--tab-active-border:rgba(255,255,255,.18);--mono:'Geist Mono','JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,monospace;--sans:'Geist','Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;color-scheme:dark}@media (prefers-color-scheme:light){:root{--bg:#ffffff;--s1:#f7f8fa;--s2:#eef0f3;--s3:#e6e9ee;--b-subtle:#eceef1;--b:#dfe3e8;--b-strong:#c8d0d9;--tx:#0b0d10;--tx2:#5b6470;--tx3:#8a929e;--brand-faint:rgba(11,96,234,.10);--tab-hover-bg:rgba(0,0,0,.06);--tab-active-bg:rgba(0,0,0,.10);--tab-active-border:rgba(0,0,0,.12);color-scheme:light}}body{background:var(--bg);color:var(--tx);font-family:var(--sans);font-size:14px;-webkit-font-smoothing:antialiased;overflow:hidden;user-select:none}input{user-select:text}#app{height:100vh;display:flex;flex-direction:column;min-height:0}button{font:inherit;color:inherit;cursor:pointer;border:0;background:none}input{font:inherit}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{background:var(--b);border-radius:6px;border:2px solid transparent;background-clip:content-box}.mono{font-family:var(--mono)}.lbl{font-family:var(--mono);font-size:11px;letter-spacing:1.2px;text-transform:uppercase;color:var(--tx2)}svg{display:block}.dot{width:8px;height:8px;border-radius:50%;background:var(--tx3);flex:0 0 auto;display:inline-block}.titlebar{-webkit-app-region:drag;display:flex;align-items:center;gap:12px;height:44px;padding:0 12px;border-bottom:1px solid var(--b-subtle);background:var(--s1);flex:0 0 auto}.titlebar button,.titlebar input,.titlebar .no-drag{-webkit-app-region:no-drag}.brand{display:flex;align-items:center;gap:8px;font-weight:600}.brand .mk{width:22px;height:22px;display:block;flex:0 0 auto;object-fit:contain}.tb-acc{display:inline-flex;align-items:center;gap:7px;font-size:12px;color:var(--tx2);max-width:38vw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.btn{display:inline-flex;align-items:center;gap:7px;height:32px;padding:0 13px;border-radius:7px;border:1px solid var(--b);background:var(--s1);color:var(--tx);font-weight:500;transition:border-color .12s,background .12s,filter .12s}.btn:hover{border-color:var(--b-strong);background:var(--s2)}.btn.pri{background:var(--brand);border-color:var(--brand);color:#fff}.btn.pri:hover{filter:brightness(1.08);background:var(--brand)}.btn.sm{height:28px;padding:0 10px;font-size:12px}.btn:disabled{opacity:.55;cursor:default}.icon-btn{width:30px;height:30px;display:grid;place-items:center;border-radius:7px;border:1px solid transparent;color:var(--tx2)}.icon-btn:hover{background:var(--s2);border-color:var(--b);color:var(--tx)}.badge{display:inline-flex;align-items:center;gap:6px;height:21px;padding:0 9px;border-radius:999px;font-size:11px;background:var(--s2);color:var(--tx2);font-family:var(--mono);white-space:nowrap}.badge.ok{color:var(--ok)}.badge.warn{color:var(--warn)}.badge.idle{color:var(--tx3)}.cc-body{flex:1;min-height:0;overflow:auto;position:relative}.statusbar{flex:0 0 auto;display:flex;align-items:center;gap:12px;height:27px;padding:0 12px;border-top:1px solid var(--b-subtle);background:var(--s1);font-size:11px;color:var(--tx2);font-family:var(--mono)}.statusbar .sep{opacity:.4}.status-msg.progress{color:var(--brand-2)}.status-msg.error{color:var(--err)}.cc-overlay{position:fixed;inset:0;background:rgba(0,0,0,.45);display:none;z-index:50;align-items:center;justify-content:center;padding:20px}.cc-overlay.open{display:flex}.cc-sheet{width:420px;max-width:92vw;max-height:86vh;background:var(--s1);border:1px solid var(--b);border-radius:10px;padding:16px;overflow:auto;display:flex;flex-direction:column;gap:18px;box-shadow:0 20px 70px rgba(0,0,0,.35)}.cc-sheet-h{display:flex;align-items:center;justify-content:space-between}.cc-grp{display:flex;flex-direction:column;gap:10px}.cc-row2{display:grid;grid-template-columns:1fr 1fr;gap:8px}.cc-meta{color:var(--tx2);font-size:12px}.cc-toggle{display:grid;grid-template-columns:18px 1fr;gap:10px;align-items:start;color:var(--tx2);font-size:12px;line-height:1.4}.cc-toggle input{width:16px;height:16px;margin-top:1px;accent-color:var(--brand)}.cc-toggle b{color:var(--tx)}.cc-about{margin-top:auto}.v-sidebar{display:grid;grid-template-columns:248px 1fr;overflow:hidden}.sb{display:flex;flex-direction:column;gap:8px;padding:14px 12px;border-right:1px solid var(--b-subtle);background:var(--s1);overflow:auto}.sb-grp{display:flex;flex-direction:column;gap:3px}.sb-grp .lbl{padding:6px 8px}.sb-item{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;color:var(--tx2);text-align:left}.sb-item>span:nth-child(2){flex:1}.sb-item .sb-meta{font-size:11px;color:var(--tx3);font-family:var(--mono)}.sb-item:hover{background:var(--s2)}.sb-item.active{background:var(--brand-faint);color:var(--tx)}.sb-item.active svg{color:var(--brand-2)}.sb-main{overflow:auto;padding:24px;min-width:0}.pane-h{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:18px}.pane-title{margin:0;font-size:18px;font-weight:600}.pane-sub{margin:4px 0 0;color:var(--tx2);font-size:13px}.card{border:1px solid var(--b);border-radius:10px;background:var(--s1);padding:18px;display:flex;flex-direction:column;gap:16px;max-width:560px}.card-actions{display:flex;gap:8px;flex-wrap:wrap}.env{display:flex;align-items:center;gap:12px;cursor:pointer;padding:12px 14px;border:1px solid var(--b);border-radius:10px;background:var(--s1);margin-bottom:8px}.env:hover{border-color:var(--b-strong)}.env-i{flex:1;min-width:0}.env-n{font-weight:500}.env-u{font-size:12px;color:var(--tx3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.env-tags{display:flex;gap:6px}.tag{font-family:var(--mono);font-size:11px;color:var(--tx2);background:var(--s2);border:1px solid var(--b-subtle);border-radius:5px;padding:2px 7px;white-space:nowrap}.empty{border:1px dashed var(--b);border-radius:10px;padding:28px;text-align:center;color:var(--tx2);max-width:560px}body.mac .titlebar{padding-left:92px;padding-right:12px}body.win .titlebar{padding-right:150px}.titlebar .brand{margin-right:6px}.tb-tabs{display:flex;align-items:center;gap:5px;min-width:0;overflow:hidden}.tb-tab{display:inline-flex;align-items:center;gap:10px;min-width:112px;max-width:232px;flex:0 0 auto;height:30px;padding:0 7px 0 12px;border:1px solid transparent;border-radius:8px;color:var(--tx2);font-size:12px;background:transparent;transition:background .12s,color .12s}.tb-tab:hover{background:var(--tab-hover-bg)}.tb-tab.active{background:var(--tab-active-bg);backdrop-filter:blur(8px);color:var(--tx)}.tb-tab span:first-child{flex:1;min-width:0;max-width:20ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tb-close{display:grid;width:20px;height:20px;margin-left:8px;place-items:center;border-radius:6px;color:var(--tx3);font-size:14px;line-height:1;flex:0 0 auto}.tb-close:hover{background:rgba(255,255,255,.14);color:var(--tx)}.tb-env-actions{display:flex;align-items:center;gap:6px;min-width:0}.tb-env-actions .btn{height:28px;padding:0 9px;font-size:12px}.tb-action{flex:0 0 auto}.card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.card-tools{display:flex;align-items:center;gap:8px}@media (max-width:760px){.v-sidebar{grid-template-columns:1fr}.sb{flex-direction:row;align-items:center;overflow:auto}.env-tags{display:none}} diff --git a/electron/launcher/launcher.js b/electron/launcher/launcher.js new file mode 100644 index 00000000..39651ecf --- /dev/null +++ b/electron/launcher/launcher.js @@ -0,0 +1,520 @@ +window.__APP_VERSION__ = '1.34.0'; +window.__MOCK_STATE__ = { + account: { connected: true, email: 'you@cloudcli.ai' }, + activeTarget: { kind: 'launcher', name: 'Launcher', url: null }, + cloudLoading: false, + desktopSettings: { keepLocalServerRunning: false, exposeLocalServerOnNetwork: false }, + localWebUrl: 'http://localhost:3001', + shareableWebUrl: 'http://localhost:3001', + localServerRunning: false, + localStartupLogs: [], + environments: [ + { id: 'env-api', name: 'api-gateway', subdomain: 'api-gateway', access_url: 'https://api-gateway.cloudcli.ai', status: 'running', region: 'fra1', agent: 'Claude Code' }, + { id: 'env-web', name: 'web-frontend', subdomain: 'web-frontend', access_url: 'https://web-frontend.cloudcli.ai', status: 'stopped', region: 'sfo1', agent: 'Codex' }, + { id: 'env-data', name: 'data-pipeline', subdomain: 'data-pipeline', access_url: 'https://data-pipeline.cloudcli.ai', status: 'stopped', region: 'fra1', agent: 'Cursor' }, + { id: 'env-ml', name: 'ml-trainer', subdomain: 'ml-trainer', access_url: 'https://ml-trainer.cloudcli.ai', status: 'paused', region: 'iad1', agent: 'Gemini' }, + ], +}; + +(function cloudCliLauncher() { + var MOCK = window.__MOCK_STATE__ || {}; + var VERSION = window.__APP_VERSION__ || ''; + var LOGO_URL = new URL('../../public/logo-32.png', window.location.href).toString(); + + function clone(value) { + return JSON.parse(JSON.stringify(value)); + } + + var mockState = clone(MOCK); + var mockBridge = { + getState: function () { return Promise.resolve(clone(mockState)); }, + openLocal: function () { + mockState.localServerRunning = true; + mockState.activeTarget = { kind: 'local', name: 'Local CloudCLI', url: mockState.localWebUrl }; + return Promise.resolve(clone(mockState)); + }, + openLocalWebUi: function () { + mockState.localServerRunning = true; + return Promise.resolve(clone(mockState)); + }, + copyLocalWebUrl: function () { return Promise.resolve(clone(mockState)); }, + connectCloud: function () { + mockState.account = { connected: true, email: 'you@cloudcli.ai' }; + return Promise.resolve(clone(mockState)); + }, + refreshEnvironments: function () { return Promise.resolve(clone(mockState)); }, + copyDiagnostics: function () { return Promise.resolve(clone(mockState)); }, + showComputerUsePreview: function () { return Promise.resolve(clone(mockState)); }, + showEnvironmentPicker: function () { return Promise.resolve(clone(mockState)); }, + showLauncher: function () { return Promise.resolve(clone(mockState)); }, + showDesktopAppMenu: function () { return Promise.resolve(clone(mockState)); }, + showActiveEnvironmentActionsMenu: function () { return Promise.resolve(clone(mockState)); }, + openCloudDashboard: function () { return Promise.resolve(clone(mockState)); }, + runActiveEnvironmentAction: function () { return Promise.resolve(clone(mockState)); }, + switchTab: function (id) { mockState.activeTabId = id; return Promise.resolve(clone(mockState)); }, + closeTab: function (id) { + mockState.tabs = (mockState.tabs || []).filter(function (tab) { return tab.id === 'home' || tab.id !== id; }); + if (mockState.activeTabId === id) mockState.activeTabId = 'home'; + return Promise.resolve(clone(mockState)); + }, + updateSetting: function (key, value) { + mockState.desktopSettings = mockState.desktopSettings || {}; + mockState.desktopSettings[key] = !!value; + return Promise.resolve(clone(mockState)); + }, + openEnvironment: function (id) { + var env = (mockState.environments || []).filter(function (item) { return item.id === id; })[0]; + if (env) { + env.status = 'starting'; + setTimeout(function () { + env.status = 'running'; + mockState.activeTarget = { kind: 'remote', id: id, name: env.name, url: env.access_url }; + }, 1700); + } + return Promise.resolve(clone(mockState)); + }, + }; + + var bridge = window.cloudcliDesktop || mockBridge; + + var ICONS = { + terminal: '', + cloud: '', + refresh: '', + settings: '', + gear: '', + play: '', + arrow: '', + copy: '', + cloudPlus: '', + monitor: '', + phone: '', + x: '', + }; + var FILLED = { play: true }; + + function icon(name, size) { + size = size || 16; + return '' + (ICONS[name] || '') + ''; + } + + function esc(value) { + return String(value == null ? '' : value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + function statusMeta(status) { + var map = { + running: { label: 'Running', cls: 'ok', dot: '#10b981', verb: 'Opening', open: 'Open' }, + starting: { label: 'Starting', cls: 'warn', dot: '#f59e0b', verb: 'Starting', open: 'Open', busy: true }, + stopped: { label: 'Stopped', cls: 'idle', dot: '#6b7280', verb: 'Starting', open: 'Start & open' }, + paused: { label: 'Paused', cls: 'warn', dot: '#f59e0b', verb: 'Resuming', open: 'Resume' }, + }; + return map[status] || { label: status || 'Unknown', cls: 'idle', dot: '#6b7280', verb: 'Starting', open: 'Start & open' }; + } + + function connected(state) { + return !!(state && state.account && state.account.connected); + } + + function authState(state) { + return state && state.account ? (state.account.authState || (state.account.connected ? 'connected' : 'logged_out')) : 'logged_out'; + } + + function accountLabel(state) { + if (authState(state) === 'expired') return 'Reconnect'; + if (state && state.account && state.account.email) return state.account.email; + if (connected(state)) return 'Connected'; + return 'Log in'; + } + + function localUrl(state) { + return (state && (state.shareableWebUrl || state.localWebUrl)) || ''; + } + + function envCount(state) { + var count = state && state.environments ? state.environments.length : 0; + return count + ' environment' + (count === 1 ? '' : 's'); + } + + function errMsg(error) { + return error && error.message ? error.message : String(error); + } + + var CC = { + icon: icon, + esc: esc, + statusMeta: statusMeta, + connected: connected, + authState: authState, + accountLabel: accountLabel, + localUrl: localUrl, + envCount: envCount, + version: VERSION, + logoUrl: LOGO_URL, + platform: 'win', + state: clone(MOCK), + ui: {}, + _busyEnv: null, + _status: { msg: '', tone: '' }, + _reg: {}, + _wired: false, + _poll: null, + }; + + window.CC = CC; + + var app; + var overlay; + + CC.setState = function (state) { + if (state && typeof state === 'object') CC.state = state; + CC.render(CC.state); + }; + + CC.refresh = function () { + return Promise.resolve(bridge.getState()).then(function (state) { + CC.setState(state); + return state; + }); + }; + + CC.run = function (label, fn) { + CC._status = { msg: label, tone: 'progress' }; + CC.render(CC.state); + return Promise.resolve() + .then(fn) + .then(function (state) { + if (state && state.environments) CC.state = state; + return CC.refresh(); + }) + .then(function () { + CC._status = { msg: '', tone: '' }; + CC.render(CC.state); + }) + .catch(function (error) { + CC._status = { msg: errMsg(error), tone: 'error' }; + CC.render(CC.state); + }); + }; + + CC.startPolling = function () { + if (CC._poll) return; + var ticks = 0; + CC._poll = setInterval(function () { + ticks += 1; + Promise.resolve(bridge.getState()).then(function (state) { + CC.setState(state); + var anyStarting = (state.environments || []).some(function (environment) { return environment.status === 'starting'; }); + if (!anyStarting || ticks > 16) { + clearInterval(CC._poll); + CC._poll = null; + if (!anyStarting) { + CC._status = { msg: '', tone: '' }; + CC.render(CC.state); + } + } + }); + }, 1500); + }; + + CC.openEnv = function (id) { + var env = (CC.state.environments || []).filter(function (environment) { return environment.id === id; })[0]; + var meta = statusMeta(env ? env.status : ''); + CC._busyEnv = id; + CC._status = { msg: (meta.verb || 'Opening') + ' ' + ((env && (env.name || env.subdomain)) || 'environment') + '...', tone: 'progress' }; + if (env) { + var tabId = 'remote:' + env.id; + var tabs = CC.state.tabs && CC.state.tabs.length ? CC.state.tabs : [{ id: 'home', title: 'Home', kind: 'launcher', closable: false }]; + tabs = tabs.map(function (tab) { + tab.active = false; + return tab; + }); + var existing = tabs.filter(function (tab) { return tab.id === tabId; })[0]; + if (existing) { + existing.active = true; + existing.title = env.name || env.subdomain; + } else { + tabs.push({ id: tabId, title: env.name || env.subdomain, kind: 'remote', closable: true, active: true }); + } + CC.state.tabs = tabs; + CC.state.activeTabId = tabId; + } + if (env && env.status !== 'running') env.status = 'starting'; + CC.render(CC.state); + return Promise.resolve(bridge.openEnvironment(id)).then(function (state) { + if (state && state.environments) CC.setState(state); + CC.startPolling(); + }).catch(function (error) { + CC._busyEnv = null; + if (env) env.status = 'stopped'; + CC._status = { msg: errMsg(error), tone: 'error' }; + CC.render(CC.state); + }); + }; + + CC.act = function (name, node) { + switch (name) { + case 'local': + return CC.run('Starting Local CloudCLI...', function () { return bridge.openLocal(); }); + case 'connect': + return CC.run('Opening cloudcli.ai to connect your account...', function () { return bridge.connectCloud(); }); + case 'open-web': + return CC.run('Opening local web UI in your browser...', function () { return bridge.openLocalWebUi(); }); + case 'copy-web': + return CC.run('Copied local URL to clipboard', function () { return bridge.copyLocalWebUrl(); }); + case 'diagnostics': + return CC.run('Copied diagnostics to clipboard', function () { return bridge.copyDiagnostics(); }); + case 'computer-use': + return CC.run('Opening Computer Use preview...', function () { return bridge.showComputerUsePreview(); }); + case 'set-setting': + return CC.run('Saved', function () { return bridge.updateSetting(node.key, node.value); }); + case 'settings-toggle': + return CC.run('Opening settings...', function () { return bridge.showDesktopAppMenu(); }); + case 'dashboard': + return CC.run('Opening CloudCLI dashboard...', function () { return bridge.openCloudDashboard(); }); + case 'env-action': + return CC.run('Opening environment...', function () { return bridge.runActiveEnvironmentAction(node.getAttribute('data-cc-env-action')); }); + case 'env-menu': + return CC.run('Opening environment actions...', function () { return bridge.showActiveEnvironmentActionsMenu(); }); + case 'env-row-menu': + return CC.run('Opening environment actions...', function () { return bridge.showEnvironmentActionsMenu(node.getAttribute('data-cc-environment-id')); }); + case 'local-settings-toggle': + CC.renderLocalSettings(); + overlay.classList.toggle('open'); + return; + case 'settings-close': + overlay.classList.remove('open'); + return; + default: + return; + } + }; + + function renderTabs(state) { + var tabs = state.tabs && state.tabs.length ? state.tabs : [{ id: 'home', title: 'Home', closable: false, active: true }]; + return tabs.map(function (tab) { + var title = tab.title || ''; + var visibleChars = Math.min(title.length, 20); + var tabWidth = Math.max(112, Math.min(232, (visibleChars * 8) + (tab.closable ? 56 : 38))); + return ''; + }).join(''); + } + + CC.titlebar = function (state) { + var conn = connected(state); + var activeRemote = state.activeTarget && state.activeTarget.kind === 'remote'; + var envActions = activeRemote ? '' : ''; + return '
' + + '
CloudCLI
' + + '
' + renderTabs(state) + '
' + + '' + + envActions + + '' + + '' + + '
'; + }; + + CC.statusbar = function (state) { + var status = CC._status || {}; + var running = !!state.localServerRunning; + return '
' + + ' local ' + (running ? 'running · ' + esc(localUrl(state)) : 'idle') + '' + + '·' + esc(envCount(state)) + '' + + '·' + (authState(state) === 'expired' ? 'session expired' : (connected(state) ? esc(accountLabel(state)) : 'not connected')) + '' + + '' + + (status.msg ? '' + esc(status.msg) + '·' : '') + + 'v' + esc(VERSION) + '' + + '
'; + }; + + CC.renderLocalSettings = function () { + var state = CC.state || {}; + var settings = state.desktopSettings || {}; + var url = localUrl(state) || 'starts on demand'; + overlay.innerHTML = + '
' + + '
Local Settings
' + + '
Local server
' + + '
' + esc(url) + '
' + + '
' + + '' + + '' + + '
' + + '
'; + }; + + CC.render = function (state) { + state = state || CC.state; + var titlebar = (CC._reg.titlebar || CC.titlebar)(state); + var statusbar = (CC._reg.statusbar || CC.statusbar)(state); + var body = CC._reg.renderBody ? CC._reg.renderBody(state) : ''; + app.innerHTML = titlebar + '
' + body + '
' + statusbar; + if (CC._reg.afterRender) CC._reg.afterRender(state); + }; + + function wireEvents() { + if (CC._wired) return; + CC._wired = true; + + document.addEventListener('click', function (event) { + if (CC._reg.onClick && CC._reg.onClick(event)) return; + var closeTab = event.target.closest('[data-cc-close-tab]'); + if (closeTab) { + event.stopPropagation(); + CC.run('Closing tab...', function () { return bridge.closeTab(closeTab.getAttribute('data-cc-close-tab')); }); + return; + } + var tab = event.target.closest('[data-cc-tab]'); + if (tab) { + CC.run('Switching tab...', function () { return bridge.switchTab(tab.getAttribute('data-cc-tab')); }); + return; + } + var action = event.target.closest('[data-cc-action]'); + if (action) { + CC.act(action.getAttribute('data-cc-action'), action); + return; + } + var env = event.target.closest('[data-cc-env]'); + if (env) { + CC.openEnv(env.getAttribute('data-cc-env')); + return; + } + if (overlay.classList.contains('open') && !event.target.closest('.cc-sheet')) { + overlay.classList.remove('open'); + } + }); + + document.addEventListener('change', function (event) { + var setting = event.target.closest('[data-cc-setting]'); + if (setting) { + CC.act('set-setting', { + key: setting.getAttribute('data-cc-setting'), + value: setting.checked, + }); + } + }); + + document.addEventListener('keydown', function (event) { + if (event.key === 'Escape' && overlay.classList.contains('open')) { + overlay.classList.remove('open'); + return; + } + if ((event.metaKey || event.ctrlKey) && event.key === ',') { + event.preventDefault(); + CC.act('settings-toggle'); + return; + } + if (overlay.classList.contains('open')) return; + if (CC._reg.onKey) CC._reg.onKey(event, CC.state); + }); + } + + function boot() { + app = document.getElementById('app'); + overlay = document.createElement('div'); + overlay.id = 'cc-overlay'; + overlay.className = 'cc-overlay'; + document.body.appendChild(overlay); + + var isMac = /Mac/i.test(navigator.platform) || /Mac OS X/i.test(navigator.userAgent); + var isWin = /Win/i.test(navigator.platform); + CC.platform = isMac ? 'mac' : (isWin ? 'win' : 'linux'); + document.body.classList.add(CC.platform); + + wireEvents(); + if (bridge.onStateUpdated) { + bridge.onStateUpdated(function (state) { CC.setState(state); }); + } + CC.refresh().catch(function (error) { + CC._status = { msg: errMsg(error), tone: 'error' }; + CC.render(CC.state); + }); + } + + CC.register = function (registry) { + CC._reg = registry || {}; + }; + + CC.start = function () { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', boot); + } else { + boot(); + } + }; +})(); + +(function sidebarApp() { + var CC = window.CC; + + function navItem(id, iconName, label, meta, selected) { + return ''; + } + + function localPane(state) { + return '

Local CloudCLI

Run the open-source app on this machine. No account required.

' + + '
Local server
' + CC.esc(CC.localUrl(state) || 'Starts on demand') + '
' + + '
'; + } + + function envRow(environment) { + var meta = CC.statusMeta(environment.status); + var tags = (environment.agent ? '' + CC.esc(environment.agent) + '' : '') + (environment.region ? '' + CC.esc(environment.region) + '' : ''); + return '
' + + '
' + CC.esc(environment.name || environment.subdomain) + '
' + CC.esc(environment.access_url || '') + '
' + + '
' + tags + '
' + + '' + meta.label + '' + + '' + + '
'; + } + + function cloudPane(state) { + var header = '

Environments

' + CC.esc(CC.envCount(state)) + '

'; + if (CC.authState(state) === 'expired') { + return header + '
Your CloudCLI session expired.
'; + } + if (!CC.connected(state)) { + return header + '
Connect your CloudCLI account to list hosted environments.
'; + } + if (state.cloudLoading && !(state.environments || []).length) { + return header + '
Loading your CloudCLI environments...
'; + } + + var list = (state.environments || []).map(envRow).join(''); + if (!list) list = '
No hosted environments yet.
'; + return header + list; + } + + function renderBody(state) { + var section = CC.ui.section || ((CC.connected(state) || CC.authState(state) === 'expired') ? 'cloud' : 'local'); + CC.ui.section = section; + var nav = '
Workspace
' + + navItem('local', 'terminal', 'Local', state.localServerRunning ? 'on' : 'idle', section) + + navItem('cloud', 'cloud', 'Cloud', (state.environments || []).length, section) + + '
'; + return nav + '
' + (section === 'local' ? localPane(state) : cloudPane(state)) + '
'; + } + + function onClick(event) { + var nav = event.target.closest('[data-cc-nav]'); + if (!nav) return false; + CC.ui.section = nav.getAttribute('data-cc-nav'); + CC.render(CC.state); + return true; + } + + CC.register({ + bodyClass: 'v-sidebar', + renderBody: renderBody, + onClick: onClick, + }); + CC.start(); +})(); diff --git a/electron/localServer.js b/electron/localServer.js new file mode 100644 index 00000000..391e0c82 --- /dev/null +++ b/electron/localServer.js @@ -0,0 +1,483 @@ +import { spawn } from 'node:child_process'; +import fs from 'node:fs/promises'; +import http from 'node:http'; +import net from 'node:net'; +import os from 'node:os'; +import path from 'node:path'; + +const DEFAULT_PORT = 3001; +const HOST = '127.0.0.1'; +const DISPLAY_HOST = 'localhost'; +const HEALTH_TIMEOUT_MS = 1000; +const SERVER_START_TIMEOUT_MS = 30000; +const MAX_STARTUP_LOG_LINES = 300; +const SERVER_MARKER_PATH = path.join(os.homedir(), '.cloudcli', 'local-server.json'); +const LOCAL_SERVER_URL_ENV_KEYS = [ + 'CLOUDCLI_DESKTOP_LOCAL_SERVER_URL', + 'CLOUDCLI_LOCAL_SERVER_URL', + 'ELECTRON_LOCAL_SERVER_URL', +]; +const LOCAL_SERVER_PORT_ENV_KEYS = [ + 'CLOUDCLI_DESKTOP_LOCAL_SERVER_PORT', + 'CLOUDCLI_SERVER_PORT', + 'SERVER_PORT', + 'PORT', +]; + +function requestJson(url, timeoutMs = HEALTH_TIMEOUT_MS) { + return new Promise((resolve) => { + const req = http.get(url, { timeout: timeoutMs }, (res) => { + let body = ''; + + res.setEncoding('utf8'); + res.on('data', (chunk) => { + body += chunk; + }); + res.on('end', () => { + try { + resolve({ + ok: res.statusCode >= 200 && res.statusCode < 300, + json: JSON.parse(body), + }); + } catch { + resolve({ ok: false, json: null }); + } + }); + }); + + req.on('timeout', () => { + req.destroy(); + resolve({ ok: false, json: null }); + }); + req.on('error', () => resolve({ ok: false, json: null })); + }); +} + +async function isCloudCliServer(baseUrl) { + const response = await requestJson(`${baseUrl}/health`); + return response.ok + && response.json?.status === 'ok' + && typeof response.json?.installMode === 'string'; +} + +function isPortAvailable(port, host = HOST) { + return new Promise((resolve) => { + const server = net.createServer(); + + server.once('error', () => resolve(false)); + server.once('listening', () => { + server.close(() => resolve(true)); + }); + server.listen(port, host); + }); +} + +function getFreePort() { + return new Promise((resolve, reject) => { + const server = net.createServer(); + + server.once('error', reject); + server.once('listening', () => { + const address = server.address(); + const port = typeof address === 'object' && address ? address.port : DEFAULT_PORT; + server.close(() => resolve(port)); + }); + server.listen(0, HOST); + }); +} + +async function chooseServerPort(host) { + if (await isPortAvailable(DEFAULT_PORT, host)) { + return DEFAULT_PORT; + } + + return getFreePort(); +} + +function getDesktopPath() { + const currentPath = process.env.PATH || ''; + const commonPaths = process.platform === 'win32' + ? [] + : ['/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin']; + + return [...commonPaths, currentPath].filter(Boolean).join(path.delimiter); +} + +function getNodeRuntime(usePackagedElectronRuntime) { + if (process.env.ELECTRON_NODE_PATH) { + return { command: process.env.ELECTRON_NODE_PATH, env: {}, label: 'ELECTRON_NODE_PATH' }; + } + + if (usePackagedElectronRuntime && process.versions.electron) { + return { + command: process.execPath, + env: { ELECTRON_RUN_AS_NODE: '1' }, + label: `Electron ${process.versions.electron} Node ${process.versions.node}`, + }; + } + + if (process.env.npm_node_execpath) { + return { command: process.env.npm_node_execpath, env: {}, label: 'npm_node_execpath' }; + } + + return { command: 'node', env: {}, label: 'PATH node' }; +} + +function stripTrailingSlash(value) { + return value.endsWith('/') ? value.slice(0, -1) : value; +} + +function addCandidateUrl(urls, rawUrl) { + if (!rawUrl) return; + try { + const parsed = new URL(String(rawUrl)); + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return; + parsed.hash = ''; + parsed.search = ''; + const normalized = stripTrailingSlash(parsed.toString()); + if (!urls.includes(normalized)) urls.push(normalized); + } catch { + // Ignore invalid user-provided discovery values. + } +} + +function addCandidatePort(urls, rawPort) { + const port = Number.parseInt(String(rawPort || ''), 10); + if (!Number.isInteger(port) || port < 1 || port > 65535) return; + addCandidateUrl(urls, `http://${HOST}:${port}`); +} + +function getPortFromUrl(baseUrl) { + try { + const parsed = new URL(baseUrl); + if (parsed.port) return Number.parseInt(parsed.port, 10); + return parsed.protocol === 'https:' ? 443 : 80; + } catch { + return null; + } +} + +function getDisplayUrl(baseUrl) { + try { + const parsed = new URL(baseUrl); + if (parsed.hostname === HOST) { + parsed.hostname = DISPLAY_HOST; + } + return stripTrailingSlash(parsed.toString()); + } catch { + return baseUrl; + } +} + +async function readServerMarkerUrl() { + try { + const raw = await fs.readFile(SERVER_MARKER_PATH, 'utf8'); + const marker = JSON.parse(raw); + return marker.url || (marker.port ? `http://${marker.host || HOST}:${marker.port}` : null); + } catch { + return null; + } +} + +async function getExistingServerCandidateUrls(defaultUrl) { + const urls = []; + + for (const key of LOCAL_SERVER_URL_ENV_KEYS) { + addCandidateUrl(urls, process.env[key]); + } + + addCandidateUrl(urls, await readServerMarkerUrl()); + + for (const key of LOCAL_SERVER_PORT_ENV_KEYS) { + addCandidatePort(urls, process.env[key]); + } + + addCandidateUrl(urls, defaultUrl); + return urls; +} + +async function waitForCloudCliServer(baseUrl, timeoutMs) { + const startedAt = Date.now(); + + while (Date.now() - startedAt < timeoutMs) { + if (await isCloudCliServer(baseUrl)) { + return true; + } + await new Promise((resolve) => setTimeout(resolve, 300)); + } + + return false; +} + +export class LocalServerController { + constructor({ appRoot, settingsPath, isPackaged = false, onChange }) { + this.appRoot = appRoot; + this.settingsPath = settingsPath; + this.isPackaged = isPackaged; + this.onChange = onChange; + this.localServerUrl = null; + this.localServerPort = null; + this.ownedServerProcess = null; + this.startupLogs = []; + this.desktopSettings = { + keepLocalServerRunning: false, + exposeLocalServerOnNetwork: false, + }; + } + + getSettings() { + return this.desktopSettings; + } + + getLocalServerUrl() { + return this.localServerUrl; + } + + getHealthCheckUrl() { + if (!this.localServerPort) return this.localServerUrl; + return `http://${HOST}:${this.localServerPort}`; + } + + appendStartupLog(line) { + const text = String(line || '').trimEnd(); + if (!text) return; + const timestamp = new Date().toLocaleTimeString(); + this.startupLogs.push(`[${timestamp}] ${text}`); + if (this.startupLogs.length > MAX_STARTUP_LOG_LINES) { + this.startupLogs.splice(0, this.startupLogs.length - MAX_STARTUP_LOG_LINES); + } + this.onChange?.(); + } + + getStartupLogs() { + return [...this.startupLogs]; + } + + getPendingTarget() { + return { + kind: 'local', + name: 'Local CloudCLI', + url: this.localServerUrl || `http://${DISPLAY_HOST}:${this.localServerPort || DEFAULT_PORT}`, + }; + } + + getLanAddress() { + const interfaces = os.networkInterfaces(); + for (const entries of Object.values(interfaces)) { + for (const entry of entries || []) { + if (entry.family === 'IPv4' && !entry.internal) { + return entry.address; + } + } + } + return null; + } + + getShareableWebUrl() { + if (!this.localServerUrl || !this.localServerPort) return null; + if (this.desktopSettings.exposeLocalServerOnNetwork) { + const lanAddress = this.getLanAddress(); + if (lanAddress) { + return `http://${lanAddress}:${this.localServerPort}`; + } + } + return this.getLocalServerUrl(); + } + + getServerBindHost() { + return this.desktopSettings.exposeLocalServerOnNetwork ? '0.0.0.0' : HOST; + } + + async loadDesktopSettings() { + try { + const raw = await fs.readFile(this.settingsPath, 'utf8'); + const stored = JSON.parse(raw); + this.desktopSettings = { + keepLocalServerRunning: Boolean(stored.keepLocalServerRunning), + exposeLocalServerOnNetwork: Boolean(stored.exposeLocalServerOnNetwork), + }; + } catch { + this.desktopSettings = { + keepLocalServerRunning: false, + exposeLocalServerOnNetwork: false, + }; + } + } + + async saveDesktopSettings(nextSettings = this.desktopSettings) { + this.desktopSettings = { + keepLocalServerRunning: Boolean(nextSettings.keepLocalServerRunning), + exposeLocalServerOnNetwork: Boolean(nextSettings.exposeLocalServerOnNetwork), + }; + await fs.mkdir(path.dirname(this.settingsPath), { recursive: true }); + await fs.writeFile(this.settingsPath, JSON.stringify(this.desktopSettings, null, 2), 'utf8'); + this.onChange?.(); + } + + async updateDesktopSetting(key, value) { + if (!Object.prototype.hasOwnProperty.call(this.desktopSettings, key)) { + throw new Error(`Unknown desktop setting: ${key}`); + } + + const wasExposeSetting = key === 'exposeLocalServerOnNetwork'; + const wasLocalRunning = Boolean(this.localServerUrl); + await this.saveDesktopSettings({ ...this.desktopSettings, [key]: Boolean(value) }); + + return { + desktopSettings: this.desktopSettings, + requiresRestartNotice: wasExposeSetting && wasLocalRunning, + }; + } + + startBundledServer(port) { + const serverEntry = process.env.ELECTRON_SERVER_ENTRY + || path.join(this.appRoot, 'dist-server', 'server', 'index.js'); + const bindHost = this.getServerBindHost(); + const runtime = getNodeRuntime(this.isPackaged); + + const command = `${runtime.command} ${serverEntry}`; + this.appendStartupLog(`$ ${command}`); + this.appendStartupLog(`runtime: ${runtime.label}`); + this.appendStartupLog(`cwd: ${this.appRoot}`); + this.appendStartupLog(`HOST=${bindHost} SERVER_PORT=${port} NODE_ENV=production`); + + this.ownedServerProcess = spawn(runtime.command, [serverEntry], { + cwd: this.appRoot, + detached: true, + env: { + ...process.env, + ...runtime.env, + HOST: bindHost, + SERVER_PORT: String(port), + NODE_ENV: 'production', + PATH: getDesktopPath(), + }, + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + }); + + this.ownedServerProcess.once('error', (error) => { + this.appendStartupLog(`failed to start process: ${error.message}`); + this.ownedServerProcess = null; + }); + + this.ownedServerProcess.stdout?.on('data', (chunk) => { + for (const line of String(chunk).split(/\r?\n/)) { + this.appendStartupLog(line); + } + }); + + this.ownedServerProcess.stderr?.on('data', (chunk) => { + for (const line of String(chunk).split(/\r?\n/)) { + this.appendStartupLog(`stderr: ${line}`); + } + }); + + this.ownedServerProcess.once('exit', (code, signal) => { + this.appendStartupLog(`process exited with code ${code ?? 'null'} and signal ${signal ?? 'null'}`); + if (this.ownedServerProcess) { + console.error(`CloudCLI desktop server exited with code ${code ?? 'null'} and signal ${signal ?? 'null'}`); + } + this.ownedServerProcess = null; + }); + } + + async resolveLocalServerUrl() { + const defaultUrl = `http://${HOST}:${DEFAULT_PORT}`; + const defaultDisplayUrl = `http://${DISPLAY_HOST}:${DEFAULT_PORT}`; + const devUrl = process.env.ELECTRON_DEV_URL; + const forceOwnServer = process.env.ELECTRON_FORCE_OWN_SERVER === '1'; + + if (devUrl) { + const ready = await waitForCloudCliServer(defaultUrl, SERVER_START_TIMEOUT_MS); + if (!ready) { + throw new Error(`Development backend did not become ready at ${defaultDisplayUrl}`); + } + this.localServerPort = DEFAULT_PORT; + return devUrl; + } + + if (!forceOwnServer) { + const candidateUrls = await getExistingServerCandidateUrls(defaultUrl); + for (const candidateUrl of candidateUrls) { + if (await isCloudCliServer(candidateUrl)) { + const displayUrl = getDisplayUrl(candidateUrl); + this.localServerPort = getPortFromUrl(candidateUrl); + this.appendStartupLog(`Using existing Local CloudCLI at ${displayUrl}`); + return displayUrl; + } + } + } + + const port = await chooseServerPort(this.getServerBindHost()); + const serverUrl = `http://${HOST}:${port}`; + const displayUrl = `http://${DISPLAY_HOST}:${port}`; + this.localServerPort = port; + this.startBundledServer(port); + + const ready = await waitForCloudCliServer(serverUrl, SERVER_START_TIMEOUT_MS); + if (!ready) { + const recentLogs = this.getStartupLogs().slice(-20).join('\n'); + this.localServerPort = null; + throw new Error([ + `Bundled backend did not become ready at ${displayUrl}.`, + recentLogs ? `Recent startup output:\n${recentLogs}` : 'No startup output was captured.', + ].join('\n\n')); + } + + this.appendStartupLog(`Local CloudCLI ready at ${displayUrl}`); + this.localServerUrl = displayUrl; + return displayUrl; + } + + async ensureLocalServer() { + if (!this.localServerUrl) { + this.localServerUrl = await this.resolveLocalServerUrl(); + } + return this.localServerUrl; + } + + async getResolvedTarget() { + await this.ensureLocalServer(); + return { + kind: 'local', + name: 'Local CloudCLI', + url: this.localServerUrl, + }; + } + + async loadLocalTarget() { + return { + pendingTarget: this.getPendingTarget(), + target: await this.getResolvedTarget(), + }; + } + + hasOwnedServer() { + return Boolean(this.ownedServerProcess); + } + + detachOwnedServer() { + if (!this.ownedServerProcess) return; + this.ownedServerProcess.unref(); + this.ownedServerProcess = null; + } + + async shutdownOwnedServer() { + if (!this.ownedServerProcess) return; + + const child = this.ownedServerProcess; + this.ownedServerProcess = null; + child.kill('SIGTERM'); + + await new Promise((resolve) => { + const timeout = setTimeout(resolve, 3000); + child.once('exit', () => { + clearTimeout(timeout); + resolve(); + }); + }); + } +} + +export { DEFAULT_PORT, HOST }; diff --git a/electron/main.js b/electron/main.js new file mode 100644 index 00000000..323e8c1a --- /dev/null +++ b/electron/main.js @@ -0,0 +1,789 @@ +import { app, BrowserWindow, clipboard, dialog, ipcMain, shell } from 'electron'; +import { spawn } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { CloudController } from './cloud.js'; +import { DesktopWindowManager } from './desktopWindow.js'; +import { LocalServerController } from './localServer.js'; +import { TabsController } from './tabs.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const APP_NAME = 'CloudCLI'; +const CALLBACK_PROTOCOL = 'cloudcli'; +const CALLBACK_URL = `${CALLBACK_PROTOCOL}://auth/callback`; +const CLOUDCLI_CONTROL_PLANE_URL = process.env.CLOUDCLI_CONTROL_PLANE_URL || 'https://cloudcli.ai'; +const REMOTE_START_TIMEOUT_MS = 30000; + +const tabs = new TabsController(); + +let activeTarget = { kind: 'launcher', name: APP_NAME, url: null }; +let desktopWindow = null; +let localServer = null; +let cloud = null; +let isQuitting = false; +let isRefreshingCloud = false; + +function getAppRoot() { + return app.isPackaged ? app.getAppPath() : path.resolve(__dirname, '..'); +} + +function getLauncherPath() { + return path.join(__dirname, 'launcher', 'index.html'); +} + +function getPreloadPath() { + return path.join(__dirname, 'preload.cjs'); +} + +function getWindowIconPath() { + if (process.platform === 'darwin') { + return path.join(getAppRoot(), 'electron', 'assets', 'logo-macos.png'); + } + return path.join(getAppRoot(), 'public', 'logo-512.png'); +} + +function getStorePath() { + return path.join(app.getPath('userData'), 'cloud-account.json'); +} + +function getSettingsPath() { + return path.join(app.getPath('userData'), 'desktop-settings.json'); +} + +function getDisplayTargetName() { + return activeTarget?.name || APP_NAME; +} + +function getCloudState() { + return { + account: cloud.getAccount(), + environments: cloud.getEnvironments(), + controlPlaneUrl: CLOUDCLI_CONTROL_PLANE_URL, + }; +} + +function getLocalState() { + return { + desktopSettings: localServer.getSettings(), + localServerRunning: Boolean(localServer.getLocalServerUrl()), + localWebUrl: localServer.getLocalServerUrl(), + shareableWebUrl: localServer.getShareableWebUrl(), + }; +} + +function serializeEnvironment(environment) { + return { + id: environment.id, + name: environment.name, + subdomain: environment.subdomain, + access_url: cloud.getEnvironmentUrl(environment), + status: environment.status, + created_at: environment.created_at, + github_url: environment.github_url || null, + region: environment.region || null, + agent: environment.agent || null, + }; +} + +function getDesktopState() { + const cloudAccount = cloud.getAccount(); + const localState = getLocalState(); + const authState = cloud.getAuthState(); + return { + account: { + connected: authState === 'connected', + email: cloudAccount?.email || null, + authState, + requiresReconnect: authState === 'expired', + }, + activeTarget, + desktopSettings: localState.desktopSettings, + localWebUrl: localState.localWebUrl, + shareableWebUrl: localState.shareableWebUrl, + localServerRunning: localState.localServerRunning, + localStartupLogs: localServer.getStartupLogs(), + cloudLoading: isRefreshingCloud, + tabs: tabs.getSerializableTabs(), + activeTabId: tabs.activeTabId, + environments: cloud.getEnvironments().map(serializeEnvironment), + }; +} + +function isSafeExternalUrl(url) { + try { + const parsed = new URL(url); + return ['https:', 'http:', 'mailto:'].includes(parsed.protocol) + || (parsed.protocol === `${CALLBACK_PROTOCOL}:` && parsed.hostname === 'auth'); + } catch { + return false; + } +} + +async function openExternalUrl(url) { + if (!isSafeExternalUrl(url)) { + throw new Error(`Refusing to open unsupported external URL: ${url}`); + } + + if (url.startsWith(`${CALLBACK_PROTOCOL}://`)) { + await handleDeepLink(url); + return; + } + + await shell.openExternal(url); +} + +async function showError(title, error) { + const message = error instanceof Error ? error.message : String(error); + console.error(`${title}: ${message}`); + await dialog.showMessageBox(desktopWindow?.getMainWindow() || undefined, { + type: 'error', + title, + message: title, + detail: message, + }); +} + +function isExpectedNavigationAbort(error) { + const message = error instanceof Error ? error.message : String(error); + return error?.code === 'ERR_ABORTED' || message.includes('ERR_ABORTED') || message.includes('(-3)'); +} + +function syncDesktopState() { + if (!desktopWindow) return; + desktopWindow.buildAppMenu(); + desktopWindow.emitDesktopState(); + if (activeTarget?.kind === 'local' && !localServer?.getLocalServerUrl()) { + void desktopWindow.showLocalStartupTarget(localServer.getPendingTarget(), localServer.getStartupLogs()) + .catch((error) => { + if (isExpectedNavigationAbort(error)) return; + void showError('Could not update local startup log', error); + }); + } +} + +function setActiveTarget(target) { + activeTarget = target; +} + +function getEnvironmentTarget(environment) { + return { + kind: 'remote', + id: environment.id, + name: environment.name || environment.subdomain, + url: cloud.getEnvironmentUrl(environment), + }; +} + +async function getEnvironmentLaunchTarget(environment) { + return { + ...getEnvironmentTarget(environment), + url: await cloud.getEnvironmentLaunchUrl(environment), + }; +} + +function getDiagnosticsText() { + const cloudAccount = cloud.getAccount(); + const localState = getLocalState(); + return JSON.stringify({ + app: APP_NAME, + version: app.getVersion(), + electron: process.versions.electron, + node: process.versions.node, + platform: process.platform, + arch: process.arch, + appPath: getAppRoot(), + userDataPath: app.getPath('userData'), + activeTarget, + localServerUrl: localState.localWebUrl, + localServerPort: localServer.localServerPort, + localWebUrl: localState.localWebUrl, + shareableWebUrl: localState.shareableWebUrl, + desktopSettings: localState.desktopSettings, + cloudConnected: Boolean(cloudAccount?.apiKey), + cloudEmail: cloudAccount?.email || null, + cloudEnvironmentCount: cloud.getEnvironments().length, + controlPlaneUrl: CLOUDCLI_CONTROL_PLANE_URL, + }, null, 2); +} + +async function copyDiagnostics() { + clipboard.writeText(getDiagnosticsText()); + await dialog.showMessageBox(desktopWindow?.getMainWindow() || undefined, { + type: 'info', + title: 'Diagnostics copied', + message: 'CloudCLI desktop diagnostics were copied to the clipboard.', + }); +} + +async function showComputerUsePreview() { + await dialog.showMessageBox(desktopWindow?.getMainWindow() || undefined, { + type: 'info', + buttons: ['OK'], + title: 'Computer Use Preview', + message: 'Computer use needs an explicit safety gate before it can run.', + detail: [ + 'The desktop shell is ready for controlled automation hooks, but full computer use is not enabled yet.', + '', + 'Before this is exposed, CloudCLI needs per-session consent, a stop control, screen-capture permission checks, app/window scoping, and a provider-specific action loop.', + ].join('\n'), + }); +} + +async function refreshCloudEnvironments({ showErrors = false } = {}) { + isRefreshingCloud = true; + syncDesktopState(); + try { + return await cloud.refreshCloudEnvironments(); + } catch (error) { + const authState = cloud.getAuthState(); + if (authState === 'expired') { + const expiredError = new Error('Your CloudCLI session expired. Reconnect your account.'); + if (showErrors) { + await showError('CloudCLI login required', expiredError); + return []; + } + throw expiredError; + } + if (showErrors) { + await showError('Could not load CloudCLI environments', error); + return []; + } + throw error; + } finally { + isRefreshingCloud = false; + syncDesktopState(); + } +} + +async function connectCloudAccount() { + const connectUrl = cloud.buildConnectUrl(); + clipboard.writeText(connectUrl); + await openExternalUrl(connectUrl); + return connectUrl; +} + +async function handleDeepLink(url) { + let parsed; + try { + parsed = new URL(url); + } catch { + return; + } + + if (parsed.protocol !== `${CALLBACK_PROTOCOL}:` || parsed.hostname !== 'auth') { + return; + } + + const apiKey = parsed.searchParams.get('api_key'); + if (!apiKey) { + await showError('CloudCLI account connection failed', new Error('The callback did not include an API key.')); + return; + } + + await cloud.saveFromCallback({ + apiKey, + email: parsed.searchParams.get('email'), + }); + await refreshCloudEnvironments({ showErrors: true }); + + dialog.showMessageBox(desktopWindow?.getMainWindow() || undefined, { + type: 'info', + title: 'CloudCLI account connected', + message: cloud.getAccount()?.email ? `Connected as ${cloud.getAccount().email}.` : 'CloudCLI account connected.', + }).catch(() => {}); +} + +async function copyLocalWebUrl() { + await localServer.ensureLocalServer(); + const shareableUrl = localServer.getShareableWebUrl(); + const localUrl = localServer.getLocalServerUrl(); + + if (!shareableUrl) { + throw new Error('Local CloudCLI URL is not available yet.'); + } + + clipboard.writeText(shareableUrl); + const isLanUrl = shareableUrl !== localUrl; + await dialog.showMessageBox(desktopWindow?.getMainWindow() || undefined, { + type: 'info', + title: 'Web URL copied', + message: isLanUrl ? 'LAN web URL copied.' : 'Local web URL copied.', + detail: isLanUrl + ? `${shareableUrl}\n\nUse this URL from another device on the same network.` + : `${shareableUrl}\n\nThis URL works on this computer. Enable LAN access before starting Local CloudCLI to copy a phone-accessible URL.`, + }); + + return getDesktopState(); +} + +async function openLocalWebUi() { + await localServer.ensureLocalServer(); + const url = localServer.getShareableWebUrl() || localServer.getLocalServerUrl(); + if (!url) { + throw new Error('Local CloudCLI URL is not available yet.'); + } + + await shell.openExternal(url); + return getDesktopState(); +} + +async function updateDesktopSetting(key, value) { + const result = await localServer.updateDesktopSetting(key, value); + syncDesktopState(); + + if (result.requiresRestartNotice) { + await dialog.showMessageBox(desktopWindow?.getMainWindow() || undefined, { + type: 'info', + title: 'Restart local server to apply', + message: 'LAN access changes apply the next time the local server starts.', + detail: 'Quit CloudCLI and stop the local server, then open Local CloudCLI again.', + }); + } + + return getDesktopState(); +} + +async function showEnvironmentPicker() { + const environments = await refreshCloudEnvironments({ showErrors: true }); + const choices = ['Local CloudCLI', ...environments.map((environment) => { + const status = environment.status === 'running' ? '' : ` (${environment.status})`; + return `${environment.name || environment.subdomain}${status}`; + })]; + + const response = await dialog.showMessageBox(desktopWindow?.getMainWindow(), { + type: 'question', + buttons: [...choices, 'Cancel'], + defaultId: 0, + cancelId: choices.length, + title: 'Switch CloudCLI Environment', + message: 'Choose where this desktop window should connect.', + }); + + if (response.response === choices.length) return getDesktopState(); + if (response.response === 0) return openLocalInDesktop(); + return openEnvironmentInDesktop(environments[response.response - 1]); +} + +async function startEnvironment(environment) { + await cloud.startEnvironmentAndWait(environment, REMOTE_START_TIMEOUT_MS); + await refreshCloudEnvironments({ showErrors: true }); + return getDesktopState(); +} + +async function stopEnvironment(environment) { + await cloud.stopEnvironment(environment); + await refreshCloudEnvironments({ showErrors: true }); + return getDesktopState(); +} + +async function openEnvironmentInBrowser(environment) { + await shell.openExternal(await cloud.getEnvironmentLaunchUrl(environment)); + return getDesktopState(); +} + +function getProjectFolder(environment) { + return String(environment.name || environment.subdomain || 'workspace').replace(/[^a-zA-Z0-9-]/g, ''); +} + +function getSshTarget(credentials) { + if (credentials.ssh_command) { + const parts = String(credentials.ssh_command).split(/\s+/); + if (parts.length >= 2) return parts[1]; + } + return `${credentials.username}@ssh.cloudcli.ai`; +} + +function getSshHost(credentials) { + const target = getSshTarget(credentials); + const atIndex = target.indexOf('@'); + return atIndex >= 0 ? target.slice(atIndex + 1) : 'ssh.cloudcli.ai'; +} + +async function getEnvironmentCredentials(environment) { + const credentials = await cloud.getEnvironmentCredentials(environment); + if (credentials.password) { + clipboard.writeText(credentials.password); + } + return credentials; +} + +async function openEnvironmentInIde(environment, ide) { + const credentials = await getEnvironmentCredentials(environment); + const scheme = ide === 'cursor' ? 'cursor' : 'vscode'; + const remoteUri = `${scheme}://vscode-remote/ssh-remote+${credentials.username}@${getSshHost(credentials)}/workspace/${getProjectFolder(environment)}?windowId=_blank`; + await shell.openExternal(remoteUri); + return getDesktopState(); +} + +async function openEnvironmentInSsh(environment) { + const credentials = await getEnvironmentCredentials(environment); + const sshCommand = `ssh -t ${getSshTarget(credentials)} "cd /workspace/${getProjectFolder(environment)} && exec $SHELL -l"`; + + if (process.platform === 'darwin') { + const escaped = sshCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + spawn('osascript', ['-e', `tell application "Terminal" to do script "${escaped}"`], { + detached: true, + stdio: 'ignore', + }).unref(); + } else { + clipboard.writeText(sshCommand); + await dialog.showMessageBox(desktopWindow?.getMainWindow() || undefined, { + type: 'info', + title: 'SSH command copied', + message: 'The SSH command was copied to the clipboard.', + detail: sshCommand, + }); + } + + return getDesktopState(); +} + +async function copyEnvironmentMobileUrl(environment) { + const url = cloud.getEnvironmentUrl(environment); + clipboard.writeText(url); + await dialog.showMessageBox(desktopWindow?.getMainWindow() || undefined, { + type: 'info', + title: 'Environment URL copied', + message: 'Use this URL from your mobile browser.', + detail: url, + }); + return getDesktopState(); +} + +async function openCloudDashboard() { + await shell.openExternal(CLOUDCLI_CONTROL_PLANE_URL); + return getDesktopState(); +} + +function getActiveRemoteEnvironment() { + if (activeTarget?.kind !== 'remote') return null; + return cloud.findEnvironment(activeTarget.id); +} + +async function runActiveEnvironmentAction(action) { + const environment = getActiveRemoteEnvironment(); + if (!environment) { + throw new Error('Open a cloud environment first.'); + } + + switch (action) { + case 'web': + return openEnvironmentInBrowser(environment); + case 'vscode': + return openEnvironmentInIde(environment, 'vscode'); + case 'cursor': + return openEnvironmentInIde(environment, 'cursor'); + case 'ssh': + return openEnvironmentInSsh(environment); + case 'mobile': + return copyEnvironmentMobileUrl(environment); + default: + throw new Error(`Unknown environment action: ${action}`); + } +} + +async function openLocalInDesktop() { + const existingTab = tabs.getTab('local'); + if (existingTab && localServer.getLocalServerUrl()) { + await desktopWindow.showTarget(await localServer.getResolvedTarget()); + return getDesktopState(); + } + + const pendingTarget = localServer.getPendingTarget(); + tabs.upsertTarget(pendingTarget); + setActiveTarget(pendingTarget); + await desktopWindow.showLocalStartupTarget(pendingTarget, localServer.getStartupLogs()); + desktopWindow.emitDesktopState(); + + const target = await localServer.getResolvedTarget(); + await desktopWindow.showTarget(target); + return getDesktopState(); +} + +async function openEnvironmentInDesktop(environment) { + const pendingTarget = getEnvironmentTarget(environment); + const tabId = tabs.getTabIdForTarget(pendingTarget); + const hadTab = Boolean(tabs.getTab(tabId)); + const previousTabId = tabs.activeTabId; + + if (!hadTab) { + await desktopWindow.showTabPlaceholder( + pendingTarget, + `${environment.status === 'running' ? 'Opening' : 'Starting'} ${pendingTarget.name}...`, + ); + tabs.upsertTarget(pendingTarget); + desktopWindow.emitDesktopState(); + } + + let nextEnvironment = environment; + + if (environment.status !== 'running') { + const response = await dialog.showMessageBox(desktopWindow?.getMainWindow(), { + type: 'question', + buttons: ['Start Environment', 'Cancel'], + defaultId: 0, + cancelId: 1, + title: 'Start environment?', + message: `${pendingTarget.name} is ${environment.status}.`, + detail: 'CloudCLI can start it before opening the remote app.', + }); + + if (response.response !== 0) { + if (!hadTab) { + tabs.remove(tabId); + desktopWindow.destroyTabView(tabId); + if (previousTabId && previousTabId !== tabId) { + await desktopWindow.switchDesktopTab(previousTabId); + } else { + await desktopWindow.showLauncher(); + } + } + return getDesktopState(); + } + + if (hadTab) { + await desktopWindow.showTabPlaceholder(pendingTarget, `Starting ${pendingTarget.name}...`); + tabs.upsertTarget(pendingTarget); + desktopWindow.emitDesktopState(); + } + + nextEnvironment = await cloud.startEnvironmentAndWait(environment, REMOTE_START_TIMEOUT_MS); + } + + const target = await getEnvironmentLaunchTarget(nextEnvironment); + await desktopWindow.showTarget(target); + return getDesktopState(); +} + +async function clearCloudAccount() { + await cloud.clearCloudAccount(); + return getDesktopState(); +} + +function getRemoteEnvironmentMenuItems() { + const cloudAccount = cloud.getAccount(); + const environments = cloud.getEnvironments(); + + if (!cloudAccount?.apiKey) { + return [{ label: 'Connect CloudCLI Account...', click: () => void connectCloudAccount() }]; + } + + if (!environments.length) { + return [{ label: 'No environments found', enabled: false }]; + } + + return environments.map((environment) => ({ + label: `${environment.name || environment.subdomain}${environment.status === 'running' ? '' : ` (${environment.status})`}`, + click: () => void openEnvironmentInDesktop(environment) + .catch((error) => showError('Could not open environment', error)), + })); +} + +function registerProtocolHandler() { + const appEntry = path.join(getAppRoot(), 'electron', 'main.js'); + if (process.defaultApp && process.argv.length >= 2) { + app.setAsDefaultProtocolClient(CALLBACK_PROTOCOL, process.execPath, [appEntry]); + } else { + app.setAsDefaultProtocolClient(CALLBACK_PROTOCOL); + } +} + +function registerIpcHandlers() { + ipcMain.handle('cloudcli-desktop:connect-cloud', async () => ({ + ...getDesktopState(), + connectUrl: await connectCloudAccount(), + })); + + ipcMain.handle('cloudcli-desktop:copy-diagnostics', async () => { + await copyDiagnostics(); + return getDesktopState(); + }); + + ipcMain.handle('cloudcli-desktop:copy-local-web-url', async () => copyLocalWebUrl()); + ipcMain.handle('cloudcli-desktop:get-state', () => getDesktopState()); + ipcMain.handle('cloudcli-desktop:open-cloud-dashboard', async () => openCloudDashboard()); + ipcMain.handle('cloudcli-desktop:run-active-environment-action', async (_event, action) => runActiveEnvironmentAction(action)); + ipcMain.handle('cloudcli-desktop:open-environment', async (_event, environmentId) => { + const environment = cloud.findEnvironment(environmentId); + if (!environment) { + throw new Error('Environment not found. Refresh and try again.'); + } + return openEnvironmentInDesktop(environment); + }); + ipcMain.handle('cloudcli-desktop:open-local', async () => openLocalInDesktop()); + ipcMain.handle('cloudcli-desktop:open-local-web-ui', async () => openLocalWebUi()); + ipcMain.handle('cloudcli-desktop:refresh-environments', async () => { + await refreshCloudEnvironments({ showErrors: true }); + return getDesktopState(); + }); + ipcMain.handle('cloudcli-desktop:show-environment-picker', async () => showEnvironmentPicker()); + ipcMain.handle('cloudcli-desktop:show-launcher', async () => { + await desktopWindow.showLauncher(); + return getDesktopState(); + }); + ipcMain.handle('cloudcli-desktop:show-computer-use-preview', async () => { + await showComputerUsePreview(); + return getDesktopState(); + }); + ipcMain.handle('cloudcli-desktop:show-desktop-app-menu', async () => desktopWindow.showDesktopAppMenu()); + ipcMain.handle('cloudcli-desktop:show-active-environment-actions-menu', async () => desktopWindow.showActiveEnvironmentActionsMenu()); + ipcMain.handle('cloudcli-desktop:show-environment-actions-menu', async (_event, environmentId) => desktopWindow.showEnvironmentActionsMenu(environmentId)); + ipcMain.handle('cloudcli-desktop:switch-tab', async (_event, tabId) => desktopWindow.switchDesktopTab(tabId)); + ipcMain.handle('cloudcli-desktop:close-tab', async (_event, tabId) => desktopWindow.closeDesktopTab(tabId)); + ipcMain.handle('cloudcli-desktop:update-setting', async (_event, key, value) => updateDesktopSetting(key, value)); +} + +function registerAppEvents() { + app.on('open-url', (event, url) => { + event.preventDefault(); + void handleDeepLink(url); + }); + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + if (desktopWindow) { + void desktopWindow.createWindow(); + } else { + void createDesktopWindow(); + } + return; + } + + const window = desktopWindow?.getMainWindow(); + if (window) { + window.show(); + window.focus(); + } + }); + + app.on('before-quit', (event) => { + if (isQuitting || !localServer?.hasOwnedServer()) return; + if (localServer.getSettings().keepLocalServerRunning) { + localServer.detachOwnedServer(); + return; + } + + event.preventDefault(); + isQuitting = true; + void localServer.shutdownOwnedServer().finally(() => app.quit()); + }); + + app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } + }); +} + +async function createDesktopWindow() { + desktopWindow = new DesktopWindowManager({ + appName: APP_NAME, + getWindowIconPath, + getLauncherPath, + getPreloadPath, + openExternalUrl, + getDesktopState, + getDisplayTargetName, + getRemoteEnvironmentMenuItems, + getCloudState, + getLocalState, + tabs, + actions: { + copyDiagnostics, + copyText: (text) => clipboard.writeText(text), + clearCloudAccount, + connectCloudAccount, + getActiveTarget: () => activeTarget, + getEnvironmentUrl: (environment) => cloud.getEnvironmentUrl(environment), + openEnvironmentInBrowser, + openEnvironmentInDesktop, + openEnvironmentInIde, + openEnvironmentInSsh, + openLocalInDesktop, + openLocalWebUi, + openCloudDashboard, + refreshCloudEnvironments: () => refreshCloudEnvironments({ showErrors: true }), + setActiveTarget, + showComputerUsePreview, + showEnvironmentPicker, + showError, + startEnvironment, + stopEnvironment, + updateDesktopSetting, + copyLocalWebUrl, + }, + }); + + desktopWindow.createTray(); + desktopWindow.configurePermissions(); + await desktopWindow.createWindow(); +} + +function registerSingleInstance() { + const gotSingleInstanceLock = app.requestSingleInstanceLock(); + if (!gotSingleInstanceLock) { + app.quit(); + return false; + } + + app.on('second-instance', (_event, argv) => { + const deepLink = argv.find((arg) => arg.startsWith(`${CALLBACK_PROTOCOL}://`)); + if (deepLink) { + void handleDeepLink(deepLink); + } + + const window = desktopWindow?.getMainWindow(); + if (window) { + if (window.isMinimized()) window.restore(); + window.show(); + window.focus(); + } + }); + + return true; +} + +async function bootstrap() { + app.name = APP_NAME; + app.setName(APP_NAME); + process.title = APP_NAME; + + await app.whenReady(); + app.setName(APP_NAME); + app.setAboutPanelOptions({ + applicationName: APP_NAME, + applicationVersion: app.getVersion(), + copyright: 'CloudCLI', + }); + + localServer = new LocalServerController({ + appRoot: getAppRoot(), + settingsPath: getSettingsPath(), + isPackaged: app.isPackaged, + onChange: syncDesktopState, + }); + cloud = new CloudController({ + storePath: getStorePath(), + controlPlaneUrl: CLOUDCLI_CONTROL_PLANE_URL, + callbackUrl: CALLBACK_URL, + onChange: syncDesktopState, + }); + + await localServer.loadDesktopSettings(); + await cloud.loadCloudAccount(); + + registerProtocolHandler(); + registerIpcHandlers(); + registerAppEvents(); + await createDesktopWindow(); + void refreshCloudEnvironments({ showErrors: false }); +} + +if (registerSingleInstance()) { + bootstrap().catch(async (error) => { + await showError('CloudCLI failed to start', error); + app.quit(); + }); +} diff --git a/electron/preload.cjs b/electron/preload.cjs new file mode 100644 index 00000000..16de7cb7 --- /dev/null +++ b/electron/preload.cjs @@ -0,0 +1,28 @@ +const { contextBridge, ipcRenderer } = require('electron'); + +if (window.location.protocol === 'file:') { + contextBridge.exposeInMainWorld('cloudcliDesktop', { + connectCloud: () => ipcRenderer.invoke('cloudcli-desktop:connect-cloud'), + copyDiagnostics: () => ipcRenderer.invoke('cloudcli-desktop:copy-diagnostics'), + copyLocalWebUrl: () => ipcRenderer.invoke('cloudcli-desktop:copy-local-web-url'), + getState: () => ipcRenderer.invoke('cloudcli-desktop:get-state'), + openCloudDashboard: () => ipcRenderer.invoke('cloudcli-desktop:open-cloud-dashboard'), + openEnvironment: (environmentId) => ipcRenderer.invoke('cloudcli-desktop:open-environment', environmentId), + runActiveEnvironmentAction: (action) => ipcRenderer.invoke('cloudcli-desktop:run-active-environment-action', action), + openLocal: () => ipcRenderer.invoke('cloudcli-desktop:open-local'), + openLocalWebUi: () => ipcRenderer.invoke('cloudcli-desktop:open-local-web-ui'), + refreshEnvironments: () => ipcRenderer.invoke('cloudcli-desktop:refresh-environments'), + showEnvironmentPicker: () => ipcRenderer.invoke('cloudcli-desktop:show-environment-picker'), + showLauncher: () => ipcRenderer.invoke('cloudcli-desktop:show-launcher'), + showComputerUsePreview: () => ipcRenderer.invoke('cloudcli-desktop:show-computer-use-preview'), + showDesktopAppMenu: () => ipcRenderer.invoke('cloudcli-desktop:show-desktop-app-menu'), + showActiveEnvironmentActionsMenu: () => ipcRenderer.invoke('cloudcli-desktop:show-active-environment-actions-menu'), + showEnvironmentActionsMenu: (environmentId) => ipcRenderer.invoke('cloudcli-desktop:show-environment-actions-menu', environmentId), + switchTab: (tabId) => ipcRenderer.invoke('cloudcli-desktop:switch-tab', tabId), + closeTab: (tabId) => ipcRenderer.invoke('cloudcli-desktop:close-tab', tabId), + updateSetting: (key, value) => ipcRenderer.invoke('cloudcli-desktop:update-setting', key, value), + onStateUpdated: (callback) => { + ipcRenderer.on('cloudcli-desktop:state-updated', (_event, state) => callback(state)); + }, + }); +} diff --git a/electron/scripts/generate-macos-icon.js b/electron/scripts/generate-macos-icon.js new file mode 100644 index 00000000..921b0522 --- /dev/null +++ b/electron/scripts/generate-macos-icon.js @@ -0,0 +1,62 @@ +import fs from 'node:fs/promises'; +import sharp from 'sharp'; + +const size = 1024; +const assetsDir = 'electron/assets'; +const iconPath = 'electron/assets/logo-macos.png'; +const icnsPath = 'electron/assets/logo-macos.icns'; + +function renderSvg(entrySize) { + const scale = entrySize / 32; + return ` + + + +`; +} + +async function renderPng(entrySize) { + return sharp(Buffer.from(renderSvg(entrySize))) + .png() + .toBuffer(); +} + +await fs.mkdir(assetsDir, { recursive: true }); +await fs.writeFile(iconPath, await renderPng(size)); + +const icnsEntries = [ + ['icp4', 16], + ['icp5', 32], + ['icp6', 64], + ['ic07', 128], + ['ic08', 256], + ['ic09', 512], + ['ic10', 1024], + ['ic11', 32], + ['ic12', 64], + ['ic13', 256], + ['ic14', 512], +]; + +const blocks = await Promise.all(icnsEntries.map(async ([type, entrySize]) => { + const png = await renderPng(entrySize); + const block = Buffer.alloc(8 + png.length); + block.write(type, 0, 4, 'ascii'); + block.writeUInt32BE(block.length, 4); + png.copy(block, 8); + return block; +})); + +const totalLength = 8 + blocks.reduce((sum, block) => sum + block.length, 0); +const header = Buffer.alloc(8); +header.write('icns', 0, 4, 'ascii'); +header.writeUInt32BE(totalLength, 4); + +await fs.writeFile(icnsPath, Buffer.concat([header, ...blocks], totalLength)); diff --git a/electron/tabs.js b/electron/tabs.js new file mode 100644 index 00000000..16bb4c75 --- /dev/null +++ b/electron/tabs.js @@ -0,0 +1,71 @@ +export class TabsController { + constructor() { + this.activeTabId = 'home'; + this.tabs = [ + { + id: 'home', + title: 'Home', + kind: 'launcher', + closable: false, + }, + ]; + } + + getTabIdForTarget(target) { + if (target.kind === 'launcher') return 'home'; + if (target.kind === 'remote' && target.id) return `remote:${target.id}`; + return target.kind; + } + + upsertTarget(target) { + const tabId = this.getTabIdForTarget(target); + const existingTab = this.tabs.find((tab) => tab.id === tabId); + const nextTab = { + id: tabId, + title: target.kind === 'launcher' ? 'Home' : target.name, + kind: target.kind, + target, + closable: tabId !== 'home', + }; + + if (existingTab) { + Object.assign(existingTab, nextTab); + } else { + this.tabs.push(nextTab); + } + + this.activeTabId = tabId; + return nextTab; + } + + activate(tabId) { + const tab = this.tabs.find((item) => item.id === tabId); + if (!tab) return null; + this.activeTabId = tab.id; + return tab; + } + + remove(tabId) { + const tab = this.tabs.find((item) => item.id === tabId); + if (!tab || !tab.closable) return null; + this.tabs = this.tabs.filter((item) => item.id !== tabId); + if (this.activeTabId === tabId) { + this.activeTabId = 'home'; + } + return tab; + } + + getTab(tabId) { + return this.tabs.find((item) => item.id === tabId) || null; + } + + getSerializableTabs() { + return this.tabs.map((tab) => ({ + id: tab.id, + title: tab.title, + kind: tab.kind, + closable: tab.closable, + active: tab.id === this.activeTabId, + })); + } +} diff --git a/package-lock.json b/package-lock.json index 3faa74aa..2c8c3c64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7827,9 +7827,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001761", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", - "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", "dev": true, "funding": [ { diff --git a/package.json b/package.json index ae75f9c6..a4579d82 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "desktop:dev": "ELECTRON_DEV_URL=http://127.0.0.1:5173 electron electron/main.js", "desktop:pack": "npm run build && electron-builder --dir", "desktop:dist:mac": "npm run build && electron-builder --mac dmg zip", + "desktop:icon:mac": "node electron/scripts/generate-macos-icon.js", "build": "npm run build:client && npm run build:server", "build:client": "vite build", "prebuild:server": "node -e \"require('node:fs').rmSync('dist-server', { recursive: true, force: true })\"", @@ -54,6 +55,7 @@ "build": { "appId": "ai.cloudcli.desktop", "productName": "CloudCLI", + "asar": false, "artifactName": "CloudCLI-${version}-${arch}.${ext}", "directories": { "output": "release" @@ -80,6 +82,7 @@ ], "mac": { "category": "public.app-category.developer-tools", + "icon": "electron/assets/logo-macos.icns", "target": [ "dmg", "zip" diff --git a/server/index.js b/server/index.js index 0a812920..e0254720 100755 --- a/server/index.js +++ b/server/index.js @@ -63,6 +63,7 @@ import pluginsRoutes from './routes/plugins.js'; import providerRoutes from './modules/providers/provider.routes.js'; import browserUseRoutes from './modules/browser-use/browser-use.routes.js'; import { browserUseService } from './modules/browser-use/browser-use.service.js'; +import computerUseRoutes from './modules/computer-use/computer-use.routes.js'; import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js'; import { initializeDatabase, projectsDb, sessionsDb } from './modules/database/index.js'; import { configureWebPush } from './services/vapid-keys.js'; @@ -198,6 +199,9 @@ app.use('/api/plugins', authenticateToken, pluginsRoutes); // Browser Use API Routes (protected) app.use('/api/browser-use', authenticateToken, browserUseRoutes); +// Computer Use API Routes (protected) +app.use('/api/computer-use', authenticateToken, computerUseRoutes); + // Unified provider MCP routes (protected) app.use('/api/providers', authenticateToken, providerRoutes); @@ -1661,6 +1665,40 @@ const SERVER_PORT = process.env.SERVER_PORT || 3001; const HOST = process.env.HOST || '0.0.0.0'; const DISPLAY_HOST = getConnectableHost(HOST); const VITE_PORT = process.env.VITE_PORT || 5173; +const LOCAL_SERVER_MARKER_PATH = path.join(os.homedir(), '.cloudcli', 'local-server.json'); + +async function writeLocalServerMarker() { + const marker = { + pid: process.pid, + host: HOST, + port: Number.parseInt(String(SERVER_PORT), 10), + url: `http://${DISPLAY_HOST}:${SERVER_PORT}`, + installMode, + appRoot: APP_ROOT, + updatedAt: new Date().toISOString(), + }; + + await fsPromises.mkdir(path.dirname(LOCAL_SERVER_MARKER_PATH), { recursive: true }); + await fsPromises.writeFile(LOCAL_SERVER_MARKER_PATH, JSON.stringify(marker, null, 2), 'utf8'); +} + +async function removeLocalServerMarker() { + try { + const raw = await fsPromises.readFile(LOCAL_SERVER_MARKER_PATH, 'utf8'); + const marker = JSON.parse(raw); + if (marker.pid && marker.pid !== process.pid) return; + } catch (error) { + if (error.code === 'ENOENT') return; + } + + try { + await fsPromises.unlink(LOCAL_SERVER_MARKER_PATH); + } catch (error) { + if (error.code !== 'ENOENT') { + console.warn('[WARN] Could not remove local server marker:', error.message); + } + } +} // Initialize database and start server async function startServer() { @@ -1687,6 +1725,9 @@ async function startServer() { server.listen(SERVER_PORT, HOST, async () => { const appInstallPath = APP_ROOT; + await writeLocalServerMarker().catch((error) => { + console.warn('[WARN] Could not write local server marker:', error.message); + }); console.log(''); console.log(c.dim('═'.repeat(63))); @@ -1712,6 +1753,7 @@ async function startServer() { const shutdownRuntimeServices = async () => { await browserUseService.stopAllSessions(); await stopAllPlugins(); + await removeLocalServerMarker(); process.exit(0); }; process.on('SIGTERM', () => void shutdownRuntimeServices()); diff --git a/server/modules/computer-use/computer-use.routes.ts b/server/modules/computer-use/computer-use.routes.ts new file mode 100644 index 00000000..76aa35ca --- /dev/null +++ b/server/modules/computer-use/computer-use.routes.ts @@ -0,0 +1,19 @@ +import express from 'express'; + +import { computerUseService } from '@/modules/computer-use/computer-use.service.js'; + +const router = express.Router(); + +router.get('/status', (_req, res) => { + res.json({ success: true, data: computerUseService.getStatus() }); +}); + +router.post('/sessions', (_req, res) => { + res.status(409).json({ + success: false, + error: 'Computer Use is not enabled until a local CloudCLI Desktop Agent is connected and approved by the user.', + data: computerUseService.getStatus(), + }); +}); + +export default router; diff --git a/server/modules/computer-use/computer-use.service.ts b/server/modules/computer-use/computer-use.service.ts new file mode 100644 index 00000000..63c1aa38 --- /dev/null +++ b/server/modules/computer-use/computer-use.service.ts @@ -0,0 +1,22 @@ +const IS_PLATFORM = process.env.VITE_IS_PLATFORM === 'true'; + +export const computerUseService = { + getStatus() { + return { + available: false, + bridgeConnected: false, + runtime: IS_PLATFORM ? 'cloud' : 'local', + requiresDesktopBridge: true, + message: IS_PLATFORM + ? 'Cloud Computer Use requires a linked CloudCLI Desktop Agent on the user machine.' + : 'Local Computer Use requires a desktop bridge with screen recording and accessibility permissions.', + capabilities: { + screenshots: false, + mouse: false, + keyboard: false, + clipboard: false, + stopControl: false, + }, + }; + }, +}; diff --git a/src/components/computer-use/index.ts b/src/components/computer-use/index.ts new file mode 100644 index 00000000..2c1a02b8 --- /dev/null +++ b/src/components/computer-use/index.ts @@ -0,0 +1 @@ +export { default as ComputerUsePanel } from './view/ComputerUsePanel'; diff --git a/src/components/computer-use/view/ComputerUsePanel.tsx b/src/components/computer-use/view/ComputerUsePanel.tsx new file mode 100644 index 00000000..e7c58ce9 --- /dev/null +++ b/src/components/computer-use/view/ComputerUsePanel.tsx @@ -0,0 +1,132 @@ +import { useCallback, useEffect, useState } from 'react'; +import { Cable, MonitorCog, RefreshCw, ShieldCheck } from 'lucide-react'; + +import { Badge, Button } from '../../../shared/view/ui'; +import { authenticatedFetch } from '../../../utils/api'; + +type ComputerUseStatus = { + available: boolean; + bridgeConnected: boolean; + runtime: 'cloud' | 'local'; + requiresDesktopBridge: boolean; + message: string; + capabilities: { + screenshots: boolean; + mouse: boolean; + keyboard: boolean; + clipboard: boolean; + stopControl: boolean; + }; +}; + +type ComputerUsePanelProps = { + isVisible: boolean; +}; + +async function readStatus(response: Response): Promise { + const data = await response.json(); + if (!response.ok || data.success === false) { + throw new Error(data.error || `Request failed (${response.status})`); + } + return data.data; +} + +export default function ComputerUsePanel({ isVisible }: ComputerUsePanelProps) { + const [status, setStatus] = useState(null); + const [error, setError] = useState(null); + + const refresh = useCallback(async () => { + setError(null); + try { + const response = await authenticatedFetch('/api/computer-use/status'); + setStatus(await readStatus(response)); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load Computer Use status'); + } + }, []); + + useEffect(() => { + if (isVisible) { + void refresh(); + } + }, [isVisible, refresh]); + + const capabilities = status?.capabilities || { + screenshots: false, + mouse: false, + keyboard: false, + clipboard: false, + stopControl: false, + }; + + return ( +
+
+
+
+ +

Computer Use

+ {status && {status.runtime}} +
+

+ Local desktop control through a user-approved CloudCLI Desktop Agent. +

+
+ +
+ + {error && ( +
+ {error} +
+ )} + +
+
+
+
+ +
+
+
+

Desktop bridge

+ + {status?.bridgeConnected ? 'connected' : 'not connected'} + +
+

+ {status?.message || 'Loading Computer Use status...'} +

+
+
Architecture boundary
+

+ Hosted CloudCLI can request Computer Use only through a linked local agent. The hosted server should never receive a permanent raw ability to control a user machine. +

+
+
+
+
+ + +
+
+ ); +} diff --git a/src/components/main-content/view/MainContent.tsx b/src/components/main-content/view/MainContent.tsx index 12cfe7aa..5c14cd6d 100644 --- a/src/components/main-content/view/MainContent.tsx +++ b/src/components/main-content/view/MainContent.tsx @@ -6,6 +6,7 @@ import StandaloneShell from '../../standalone-shell/view/StandaloneShell'; import GitPanel from '../../git-panel/view/GitPanel'; import PluginTabContent from '../../plugins/view/PluginTabContent'; import { BrowserUsePanel } from '../../browser-use'; +import { ComputerUsePanel } from '../../computer-use'; import type { MainContentProps } from '../types/types'; import { useTaskMaster } from '../../../contexts/TaskMasterContext'; import { usePaletteOpsRegister } from '../../../contexts/PaletteOpsContext'; @@ -178,6 +179,12 @@ function MainContent({
)} + {activeTab === 'computer' && ( +
+ +
+ )} + {activeTab.startsWith('plugin:') && (
- Star + Star {formattedCount && ( {formattedCount} )} diff --git a/src/components/sidebar/view/subcomponents/SidebarHeader.tsx b/src/components/sidebar/view/subcomponents/SidebarHeader.tsx index 8eabab2a..57ac4aa5 100644 --- a/src/components/sidebar/view/subcomponents/SidebarHeader.tsx +++ b/src/components/sidebar/view/subcomponents/SidebarHeader.tsx @@ -67,7 +67,7 @@ export default function SidebarHeader({
-

{t('app.title')}

+

{t('app.title')}

); @@ -138,7 +138,7 @@ export default function SidebarHeader({ onClick={() => onSearchModeChange('projects')} aria-pressed={searchMode === 'projects'} className={cn( - "flex-1 flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-medium transition-all", + "flex-1 flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-normal transition-all", searchMode === 'projects' ? "bg-background shadow-sm text-foreground" : "text-muted-foreground hover:text-foreground" @@ -151,7 +151,7 @@ export default function SidebarHeader({ onClick={() => onSearchModeChange('conversations')} aria-pressed={searchMode === 'conversations'} className={cn( - "flex-1 flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-medium transition-all", + "flex-1 flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-normal transition-all", searchMode === 'conversations' ? "bg-background shadow-sm text-foreground" : "text-muted-foreground hover:text-foreground" @@ -190,7 +190,7 @@ export default function SidebarHeader({ aria-label={t('search.archiveOnlyTooltip', 'Archive only')} title={t('search.archiveOnlyTooltip', 'Archive only')} className={cn( - "flex items-center justify-center rounded-md px-2.5 py-1.5 text-xs font-medium transition-all", + "flex items-center justify-center rounded-md px-2.5 py-1.5 text-xs font-normal transition-all", searchMode === 'archived' ? "bg-background shadow-sm text-foreground" : "text-muted-foreground hover:text-foreground" @@ -278,7 +278,7 @@ export default function SidebarHeader({ onClick={() => onSearchModeChange('projects')} aria-pressed={searchMode === 'projects'} className={cn( - "flex-1 flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-medium transition-all", + "flex-1 flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-normal transition-all", searchMode === 'projects' ? "bg-background shadow-sm text-foreground" : "text-muted-foreground hover:text-foreground" @@ -291,7 +291,7 @@ export default function SidebarHeader({ onClick={() => onSearchModeChange('conversations')} aria-pressed={searchMode === 'conversations'} className={cn( - "flex-1 flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-medium transition-all", + "flex-1 flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-normal transition-all", searchMode === 'conversations' ? "bg-background shadow-sm text-foreground" : "text-muted-foreground hover:text-foreground" @@ -331,7 +331,7 @@ export default function SidebarHeader({ aria-label={t('search.archiveOnlyTooltip', 'Archive only')} title={t('search.archiveOnlyTooltip', 'Archive only')} className={cn( - "flex items-center justify-center rounded-md px-2.5 py-1.5 text-xs font-medium transition-all", + "flex items-center justify-center rounded-md px-2.5 py-1.5 text-xs font-normal transition-all", searchMode === 'archived' ? "bg-background shadow-sm text-foreground" : "text-muted-foreground hover:text-foreground" diff --git a/src/components/sidebar/view/subcomponents/SidebarProjectItem.tsx b/src/components/sidebar/view/subcomponents/SidebarProjectItem.tsx index ff691335..618e0326 100644 --- a/src/components/sidebar/view/subcomponents/SidebarProjectItem.tsx +++ b/src/components/sidebar/view/subcomponents/SidebarProjectItem.tsx @@ -186,7 +186,7 @@ export default function SidebarProjectItem({ ) : ( <>
-

{project.displayName}

+

{project.displayName}

{tasksEnabled && ( ) : (
-
+
{project.displayName}
diff --git a/src/components/sidebar/view/subcomponents/SidebarSessionItem.tsx b/src/components/sidebar/view/subcomponents/SidebarSessionItem.tsx index c55d8ed3..1fdb2c6a 100644 --- a/src/components/sidebar/view/subcomponents/SidebarSessionItem.tsx +++ b/src/components/sidebar/view/subcomponents/SidebarSessionItem.tsx @@ -157,7 +157,7 @@ export default function SidebarSessionItem({
-
{sessionView.sessionName}
+
{sessionView.sessionName}
{isProcessing ? ( @@ -219,7 +219,7 @@ export default function SidebarSessionItem({
-
{sessionView.sessionName}
+
{sessionView.sessionName}
{isProcessing ? ( = new Set(['chat', 'files', 'shell', 'git', 'tasks', 'browser']); +const VALID_TABS: Set = new Set(['chat', 'files', 'shell', 'git', 'tasks', 'browser', 'computer']); const isValidTab = (tab: string): tab is AppTab => { return VALID_TABS.has(tab) || tab.startsWith('plugin:'); @@ -776,7 +776,7 @@ export function useProjectsState({ (session: ProjectSession) => { setSelectedSession(session); - if (activeTab === 'tasks' || activeTab === 'browser') { + if (activeTab === 'tasks' || activeTab === 'browser' || activeTab === 'computer') { setActiveTab('chat'); } diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 636d2e8d..6eb1fca9 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -23,7 +23,8 @@ "files": "Dateien", "git": "Quellcodeverwaltung", "tasks": "Aufgaben", - "browser": "Browser" + "browser": "Browser", + "computer": "Computer" }, "status": { "loading": "Lädt...", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 9137da9a..f8ab8444 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -23,7 +23,8 @@ "files": "Files", "git": "Source Control", "tasks": "Tasks", - "browser": "Browser" + "browser": "Browser", + "computer": "Computer" }, "status": { "loading": "Loading...", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index 79f9f675..1df91a00 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -23,7 +23,8 @@ "files": "File", "git": "Controllo Versione", "tasks": "Attività", - "browser": "Browser" + "browser": "Browser", + "computer": "Computer" }, "status": { "loading": "Caricamento...", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 498ee46c..d61cf4e2 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -23,7 +23,8 @@ "files": "ファイル", "git": "ソース管理", "tasks": "タスク", - "browser": "Browser" + "browser": "Browser", + "computer": "Computer" }, "status": { "loading": "読み込み中...", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index 03244458..58070abf 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -23,7 +23,8 @@ "files": "파일", "git": "소스 관리", "tasks": "작업", - "browser": "Browser" + "browser": "Browser", + "computer": "Computer" }, "status": { "loading": "로딩 중...", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index fc71abe1..fbde3d5f 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -23,7 +23,8 @@ "files": "Файлы", "git": "Система контроля версий", "tasks": "Задачи", - "browser": "Browser" + "browser": "Browser", + "computer": "Computer" }, "status": { "loading": "Загрузка...", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index f1fa66b9..e33ed02b 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -23,7 +23,8 @@ "files": "Dosyalar", "git": "Kaynak Kontrolü", "tasks": "Görevler", - "browser": "Browser" + "browser": "Browser", + "computer": "Computer" }, "status": { "loading": "Yükleniyor...", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 69cd159a..ac9bd9c1 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -23,7 +23,8 @@ "files": "文件", "git": "源代码管理", "tasks": "任务", - "browser": "Browser" + "browser": "Browser", + "computer": "Computer" }, "status": { "loading": "加载中...", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 419be285..e119adb6 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -23,7 +23,8 @@ "files": "檔案", "git": "版本控制", "tasks": "任務", - "browser": "Browser" + "browser": "Browser", + "computer": "Computer" }, "status": { "loading": "載入中...", diff --git a/src/types/app.ts b/src/types/app.ts index f81c3e26..42fe166c 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -17,7 +17,7 @@ export type ProviderModelsCacheInfo = { source: 'memory' | 'disk' | 'fresh'; }; -export type AppTab = 'chat' | 'files' | 'shell' | 'git' | 'tasks' | 'browser' | `plugin:${string}`; +export type AppTab = 'chat' | 'files' | 'shell' | 'git' | 'tasks' | 'browser' | 'computer' | `plugin:${string}`; export interface ProjectSession { id: string; From daac6e3fd3e0a775fb36c7d5abef9799799db778 Mon Sep 17 00:00:00 2001 From: Simos Mikelatos Date: Mon, 15 Jun 2026 17:26:53 +0000 Subject: [PATCH 03/58] ci: add macos desktop release workflow --- .github/workflows/desktop-macos-release.yml | 103 ++++++++++++++++++++ package.json | 1 + 2 files changed, 104 insertions(+) create mode 100644 .github/workflows/desktop-macos-release.yml diff --git a/.github/workflows/desktop-macos-release.yml b/.github/workflows/desktop-macos-release.yml new file mode 100644 index 00000000..d8893018 --- /dev/null +++ b/.github/workflows/desktop-macos-release.yml @@ -0,0 +1,103 @@ +name: Desktop macOS Release + +on: + workflow_dispatch: + inputs: + tag: + description: 'Release tag to create or update (defaults to v)' + required: false + type: string + release_name: + description: 'Release name (defaults to "CloudCLI Desktop macOS ")' + required: false + type: string + prerelease: + description: 'Mark the GitHub release as a prerelease' + required: true + default: false + type: boolean + +jobs: + build-macos: + name: Build signed macOS desktop app + runs-on: macos-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Typecheck + run: npm run typecheck + + - name: Resolve release metadata + id: release + run: | + VERSION="$(node -p "require('./package.json').version")" + TAG="${{ inputs.tag }}" + if [ -z "$TAG" ]; then + TAG="v${VERSION}" + fi + + RELEASE_NAME="${{ inputs.release_name }}" + if [ -z "$RELEASE_NAME" ]; then + RELEASE_NAME="CloudCLI Desktop macOS ${TAG}" + fi + + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "release_name=$RELEASE_NAME" >> "$GITHUB_OUTPUT" + + - name: Verify signing secrets are configured + run: | + test -n "$CSC_LINK" + test -n "$CSC_KEY_PASSWORD" + test -n "$APPLE_ID" + test -n "$APPLE_APP_SPECIFIC_PASSWORD" + test -n "$APPLE_TEAM_ID" + env: + CSC_LINK: ${{ secrets.CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + + - name: Build signed and notarized macOS artifacts + run: npm run desktop:dist:mac -- --publish never + env: + CSC_LINK: ${{ secrets.CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + + - name: Verify macOS artifacts + run: | + test -n "$(find release -maxdepth 1 -name '*.dmg' -print -quit)" + test -n "$(find release -maxdepth 1 -name '*.zip' -print -quit)" + shasum -a 256 release/*.{dmg,zip} > release/SHASUMS256.txt + cat release/SHASUMS256.txt + + - name: Publish GitHub release assets + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.release.outputs.tag }} + name: ${{ steps.release.outputs.release_name }} + prerelease: ${{ inputs.prerelease }} + fail_on_unmatched_files: false + files: | + release/*.dmg + release/*.zip + release/*.yml + release/*.blockmap + release/SHASUMS256.txt diff --git a/package.json b/package.json index a4579d82..0d224b41 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "mac": { "category": "public.app-category.developer-tools", "icon": "electron/assets/logo-macos.icns", + "notarize": true, "target": [ "dmg", "zip" From 260070bae0f6288c01d263038fc66b780e9b891d Mon Sep 17 00:00:00 2001 From: Simos Mikelatos Date: Mon, 15 Jun 2026 17:52:27 +0000 Subject: [PATCH 04/58] feat: add browser use runtime setup settings --- .../modules/browser-use/browser-use.routes.ts | 50 ++++- .../browser-use/browser-use.service.ts | 192 ++++++++++++++++-- .../tests/browser-use.service.test.ts | 25 +-- .../browser-use/view/BrowserUsePanel.tsx | 38 +++- .../settings/constants/constants.ts | 2 + .../settings/hooks/useSettingsController.ts | 2 +- src/components/settings/types/types.ts | 2 +- src/components/settings/view/Settings.tsx | 25 ++- .../settings/view/SettingsSidebar.tsx | 3 +- .../BrowserUseSettingsTab.tsx | 164 +++++++++++++++ src/i18n/locales/en/settings.json | 1 + 11 files changed, 450 insertions(+), 54 deletions(-) create mode 100644 src/components/settings/view/tabs/browser-use-settings/BrowserUseSettingsTab.tsx diff --git a/server/modules/browser-use/browser-use.routes.ts b/server/modules/browser-use/browser-use.routes.ts index f5dc563b..c730dd53 100644 --- a/server/modules/browser-use/browser-use.routes.ts +++ b/server/modules/browser-use/browser-use.routes.ts @@ -22,8 +22,54 @@ function readParam(value: string | string[] | undefined): string { return Array.isArray(value) ? value[0] || '' : value || ''; } -router.get('/status', (_req, res) => { - res.json({ success: true, data: browserUseService.getStatus() }); +router.get('/status', async (_req, res) => { + try { + res.json({ success: true, data: await browserUseService.getStatus() }); + } catch (error) { + res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : 'Failed to load Browser Use status.', + }); + } +}); + +router.get('/settings', async (_req, res) => { + try { + res.json({ success: true, data: { settings: await browserUseService.getSettings() } }); + } catch (error) { + res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : 'Failed to load Browser Use settings.', + }); + } +}); + +router.put('/settings', async (req, res) => { + try { + const settings = await browserUseService.updateSettings(req.body || {}); + res.json({ success: true, data: { settings } }); + } catch (error) { + res.status(400).json({ + success: false, + error: error instanceof Error ? error.message : 'Failed to save Browser Use settings.', + }); + } +}); + +router.post('/runtime/install', async (_req, res) => { + try { + const result = await browserUseService.installRuntime(); + res.status(result.success ? 200 : 500).json({ + success: result.success, + data: result, + error: result.success ? undefined : result.message, + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : 'Failed to install Browser Use runtime.', + }); + } }); router.get('/sessions', async (req: AuthenticatedRequest, res) => { diff --git a/server/modules/browser-use/browser-use.service.ts b/server/modules/browser-use/browser-use.service.ts index 4ca96695..5abcfbf3 100644 --- a/server/modules/browser-use/browser-use.service.ts +++ b/server/modules/browser-use/browser-use.service.ts @@ -1,13 +1,19 @@ import { createRequire } from 'node:module'; import { randomUUID } from 'node:crypto'; +import { spawn } from 'node:child_process'; import dns from 'node:dns/promises'; +import fs from 'node:fs'; +import fsPromises from 'node:fs/promises'; import net from 'node:net'; +import os from 'node:os'; +import path from 'node:path'; const require = createRequire(import.meta.url); const IS_PLATFORM = process.env.VITE_IS_PLATFORM === 'true'; const MAX_SESSIONS_PER_OWNER = Number.parseInt(process.env.CLOUDCLI_BROWSER_USE_MAX_SESSIONS_PER_OWNER || '3', 10); const SESSION_TTL_MS = Number.parseInt(process.env.CLOUDCLI_BROWSER_USE_SESSION_TTL_MS || String(30 * 60 * 1000), 10); const ALLOW_PRIVATE_NETWORKS = process.env.CLOUDCLI_BROWSER_USE_ALLOW_PRIVATE_NETWORKS === '1'; +const SETTINGS_PATH = path.join(os.homedir(), '.cloudcli', 'browser-use-settings.json'); type BrowserUseRuntime = 'cloud' | 'local'; type BrowserUseSessionStatus = 'ready' | 'stopped' | 'unavailable'; @@ -37,23 +43,71 @@ type BrowserUseOwner = { id: string | number; }; +type BrowserUseSettings = { + enabled: boolean; +}; + +type RuntimeReadiness = { + playwright: any | null; + playwrightInstalled: boolean; + chromiumInstalled: boolean; + chromiumExecutablePath: string | null; + installInProgress: boolean; + installMessage: string | null; +}; + const sessions = new Map(); const handles = new Map(); +let installPromise: Promise<{ success: boolean; message: string }> | null = null; +let lastInstallMessage: string | null = null; + +const DEFAULT_SETTINGS: BrowserUseSettings = { + enabled: true, +}; function getRuntime(): BrowserUseRuntime { return IS_PLATFORM ? 'cloud' : 'local'; } -function isBrowserUseEnabled(): boolean { - return process.env.CLOUDCLI_BROWSER_USE_ENABLED === '1'; +async function readSettings(): Promise { + try { + const raw = await fsPromises.readFile(SETTINGS_PATH, 'utf8'); + const parsed = JSON.parse(raw) as Partial; + return { + enabled: parsed.enabled !== false, + }; + } catch (error: any) { + if (error?.code !== 'ENOENT') { + console.warn('[Browser Use] Failed to read settings:', error?.message || error); + } + return DEFAULT_SETTINGS; + } } -function getSetupMessage(): string { - if (!isBrowserUseEnabled()) { - return 'Browser Use is disabled. Set CLOUDCLI_BROWSER_USE_ENABLED=1 after provisioning a Playwright/Chromium runtime.'; +async function writeSettings(settings: BrowserUseSettings): Promise { + const normalized = { + enabled: settings.enabled !== false, + }; + + await fsPromises.mkdir(path.dirname(SETTINGS_PATH), { recursive: true }); + await fsPromises.writeFile(SETTINGS_PATH, JSON.stringify(normalized, null, 2), 'utf8'); + return normalized; +} + +function getSetupMessage(settings: BrowserUseSettings, readiness: RuntimeReadiness): string { + if (!settings.enabled) { + return 'Browser Use is disabled in settings.'; } - return 'Playwright is not available in this runtime. Install/provision Playwright or point CloudCLI at a managed browser worker.'; + if (!readiness.playwrightInstalled) { + return 'Install Playwright and Chromium to use browser sessions.'; + } + + if (!readiness.chromiumInstalled) { + return 'Playwright is installed, but Chromium is missing. Install the Chromium runtime to continue.'; + } + + return readiness.installMessage || 'Browser Use runtime is not ready.'; } function getPlaywright(): any | null { @@ -64,6 +118,85 @@ function getPlaywright(): any | null { } } +function getRuntimeReadiness(): RuntimeReadiness { + const playwright = getPlaywright(); + const readiness: RuntimeReadiness = { + playwright, + playwrightInstalled: Boolean(playwright), + chromiumInstalled: false, + chromiumExecutablePath: null, + installInProgress: Boolean(installPromise), + installMessage: lastInstallMessage, + }; + + if (!playwright) { + return readiness; + } + + try { + const executablePath = playwright.chromium.executablePath(); + readiness.chromiumExecutablePath = executablePath; + readiness.chromiumInstalled = Boolean(executablePath && fs.existsSync(executablePath)); + } catch { + readiness.chromiumInstalled = false; + } + + return readiness; +} + +function runCommand(command: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd: process.cwd(), + env: process.env, + shell: false, + stdio: ['ignore', 'pipe', 'pipe'], + }); + const output: string[] = []; + + child.stdout.on('data', (chunk) => output.push(String(chunk))); + child.stderr.on('data', (chunk) => output.push(String(chunk))); + child.on('error', reject); + child.on('close', (code) => { + if (code === 0) { + resolve(); + return; + } + + reject(new Error(output.join('').trim() || `${command} ${args.join(' ')} exited with code ${code}`)); + }); + }); +} + +async function installRuntime(): Promise<{ success: boolean; message: string }> { + if (installPromise) { + return installPromise; + } + + const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + installPromise = (async () => { + try { + lastInstallMessage = 'Installing Playwright package...'; + await runCommand(npmCommand, ['install', '--no-save', '--no-package-lock', 'playwright']); + + lastInstallMessage = 'Installing Chromium runtime...'; + await runCommand(npmCommand, ['exec', '--', 'playwright', 'install', 'chromium']); + + lastInstallMessage = 'Browser Use runtime installed.'; + return { success: true, message: lastInstallMessage }; + } catch (error) { + lastInstallMessage = error instanceof Error ? error.message : 'Failed to install Browser Use runtime.'; + return { success: false, message: lastInstallMessage }; + } + })(); + + try { + return await installPromise; + } finally { + installPromise = null; + } +} + function getOwnerId(owner: BrowserUseOwner): string { if (owner.id === undefined || owner.id === null || String(owner.id).trim() === '') { throw new Error('Authenticated user is required.'); @@ -218,19 +351,43 @@ async function captureSession(session: BrowserUseSession, page: any): Promise) { + const current = await readSettings(); + return writeSettings({ + ...current, + enabled: settings.enabled ?? current.enabled, + }); + }, + + async getStatus() { + const settings = await readSettings(); + const readiness = getRuntimeReadiness(); + const available = settings.enabled && readiness.playwrightInstalled && readiness.chromiumInstalled; return { - enabled, + enabled: settings.enabled, runtime: getRuntime(), - available: enabled, + available, + playwrightInstalled: readiness.playwrightInstalled, + chromiumInstalled: readiness.chromiumInstalled, + installInProgress: readiness.installInProgress, sessionCount: sessions.size, mcpRecommended: true, - message: enabled + message: available ? 'Browser Use runtime is available.' - : getSetupMessage(), + : getSetupMessage(settings, readiness), + }; + }, + + async installRuntime() { + const result = await installRuntime(); + return { + ...result, + status: await this.getStatus(), }; }, @@ -264,14 +421,15 @@ export const browserUseService = { throw new Error(`Browser Use is limited to ${MAX_SESSIONS_PER_OWNER} active sessions per user.`); } - const playwright = getPlaywright(); - if (!isBrowserUseEnabled() || !playwright) { - session.message = getSetupMessage(); + const settings = await readSettings(); + const readiness = getRuntimeReadiness(); + if (!settings.enabled || !readiness.playwrightInstalled || !readiness.chromiumInstalled || !readiness.playwright) { + session.message = getSetupMessage(settings, readiness); sessions.set(session.id, session); return publicSession(session); } - const browser = await playwright.chromium.launch({ + const browser = await readiness.playwright.chromium.launch({ headless: true, args: ['--disable-dev-shm-usage'], }); diff --git a/server/modules/browser-use/tests/browser-use.service.test.ts b/server/modules/browser-use/tests/browser-use.service.test.ts index 3a291682..162e9439 100644 --- a/server/modules/browser-use/tests/browser-use.service.test.ts +++ b/server/modules/browser-use/tests/browser-use.service.test.ts @@ -15,27 +15,16 @@ test('browser use blocks private and local network addresses by default', () => }); test('browser use sessions are listed only for their owner', async () => { - const originalEnabled = process.env.CLOUDCLI_BROWSER_USE_ENABLED; - process.env.CLOUDCLI_BROWSER_USE_ENABLED = '0'; - const ownerA = { id: `owner-a-${Date.now()}-${Math.random()}` }; const ownerB = { id: `owner-b-${Date.now()}-${Math.random()}` }; - try { - const ownerASession = await browserUseService.createSession(ownerA); - await browserUseService.createSession(ownerB); + const ownerASession = await browserUseService.createSession(ownerA); + await browserUseService.createSession(ownerB); - const ownerASessions = await browserUseService.listSessions(ownerA); - const ownerBSessions = await browserUseService.listSessions(ownerB); + const ownerASessions = await browserUseService.listSessions(ownerA); + const ownerBSessions = await browserUseService.listSessions(ownerB); - assert.equal(ownerASessions.some((session) => session.id === ownerASession.id), true); - assert.equal(ownerBSessions.some((session) => session.id === ownerASession.id), false); - assert.equal(Object.hasOwn(ownerASession, 'ownerId'), false); - } finally { - if (originalEnabled === undefined) { - delete process.env.CLOUDCLI_BROWSER_USE_ENABLED; - } else { - process.env.CLOUDCLI_BROWSER_USE_ENABLED = originalEnabled; - } - } + assert.equal(ownerASessions.some((session) => session.id === ownerASession.id), true); + assert.equal(ownerBSessions.some((session) => session.id === ownerASession.id), false); + assert.equal(Object.hasOwn(ownerASession, 'ownerId'), false); }); diff --git a/src/components/browser-use/view/BrowserUsePanel.tsx b/src/components/browser-use/view/BrowserUsePanel.tsx index d2494a2a..22d1153b 100644 --- a/src/components/browser-use/view/BrowserUsePanel.tsx +++ b/src/components/browser-use/view/BrowserUsePanel.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; -import { ExternalLink, Globe, MonitorPlay, Navigation, Pause, RefreshCw, Square } from 'lucide-react'; +import { Download, ExternalLink, Globe, Loader2, MonitorPlay, Navigation, Pause, RefreshCw, Square } from 'lucide-react'; import { Badge, Button } from '../../../shared/view/ui'; import { authenticatedFetch } from '../../../utils/api'; @@ -8,6 +8,9 @@ type BrowserUseStatus = { enabled: boolean; available: boolean; runtime: 'cloud' | 'local'; + playwrightInstalled: boolean; + chromiumInstalled: boolean; + installInProgress: boolean; sessionCount: number; mcpRecommended: boolean; message: string; @@ -44,6 +47,7 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) { const [selectedSessionId, setSelectedSessionId] = useState(null); const [targetUrl, setTargetUrl] = useState('https://example.com'); const [isBusy, setIsBusy] = useState(false); + const [isInstalling, setIsInstalling] = useState(false); const [error, setError] = useState(null); const selectedSession = useMemo( @@ -108,6 +112,18 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) { await readJson(response); }); + const installRuntime = () => runAction(async () => { + setIsInstalling(true); + try { + const response = await authenticatedFetch('/api/browser-use/runtime/install', { method: 'POST' }); + await readJson(response); + } finally { + setIsInstalling(false); + } + }); + + const canInstallRuntime = Boolean(status?.enabled && (!status.playwrightInstalled || !status.chromiumInstalled)); + return (
@@ -130,7 +146,7 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) { Refresh - @@ -143,6 +159,22 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) {
Runtime
{status?.available ? 'Available' : 'Setup required'}

{status?.message || 'Loading Browser Use status...'}

+ {canInstallRuntime && ( + + )}
@@ -219,7 +251,7 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) { {selectedSession?.message || 'Create a browser session to start.'}

- This panel shows captured browser screenshots. Interactive agent control should use the guarded Browser Use API. + Install the Browser Use runtime from this panel or enable it from Settings.

)} diff --git a/src/components/settings/constants/constants.ts b/src/components/settings/constants/constants.ts index 7d4c353d..37fa9df3 100644 --- a/src/components/settings/constants/constants.ts +++ b/src/components/settings/constants/constants.ts @@ -6,6 +6,7 @@ import { Info, KeyRound, ListChecks, + MonitorPlay, Palette, Plug, } from 'lucide-react'; @@ -32,6 +33,7 @@ export const SETTINGS_MAIN_TABS: SettingsMainTabMeta[] = [ { id: 'git', label: 'Git', keywords: 'git github commits', icon: GitBranch }, { id: 'api', label: 'API Tokens', keywords: 'api tokens auth keys', icon: KeyRound }, { id: 'tasks', label: 'Tasks', keywords: 'tasks taskmaster', icon: ListChecks }, + { id: 'browser', label: 'Browser Use', keywords: 'browser use playwright chromium automation', icon: MonitorPlay }, { id: 'notifications', label: 'Notifications', keywords: 'notifications alerts push', icon: Bell }, { id: 'plugins', label: 'Plugins', keywords: 'plugins extensions integrations', icon: Plug }, { id: 'about', label: 'About', keywords: 'about version info', icon: Info }, diff --git a/src/components/settings/hooks/useSettingsController.ts b/src/components/settings/hooks/useSettingsController.ts index 70e9ed1c..a172b831 100644 --- a/src/components/settings/hooks/useSettingsController.ts +++ b/src/components/settings/hooks/useSettingsController.ts @@ -54,7 +54,7 @@ type NotificationPreferencesResponse = { type ActiveLoginProvider = AgentProvider | ''; -const KNOWN_MAIN_TABS: SettingsMainTab[] = ['agents', 'appearance', 'git', 'api', 'tasks', 'notifications', 'plugins']; +const KNOWN_MAIN_TABS: SettingsMainTab[] = ['agents', 'appearance', 'git', 'api', 'tasks', 'browser', 'notifications', 'plugins', 'about']; const normalizeMainTab = (tab: string): SettingsMainTab => { // Keep backwards compatibility with older callers that still pass "tools". diff --git a/src/components/settings/types/types.ts b/src/components/settings/types/types.ts index f68cacc4..672be1ee 100644 --- a/src/components/settings/types/types.ts +++ b/src/components/settings/types/types.ts @@ -3,7 +3,7 @@ import type { Dispatch, SetStateAction } from 'react'; import type { LLMProvider } from '../../../types/app'; import type { ProviderAuthStatus } from '../../provider-auth/types'; -export type SettingsMainTab = 'agents' | 'appearance' | 'git' | 'api' | 'tasks' | 'notifications' | 'plugins' | 'about'; +export type SettingsMainTab = 'agents' | 'appearance' | 'git' | 'api' | 'tasks' | 'browser' | 'notifications' | 'plugins' | 'about'; export type AgentProvider = LLMProvider; export type AgentCategory = 'account' | 'permissions' | 'mcp'; export type ProjectSortOrder = 'name' | 'date'; diff --git a/src/components/settings/view/Settings.tsx b/src/components/settings/view/Settings.tsx index 8340a547..800440e0 100644 --- a/src/components/settings/view/Settings.tsx +++ b/src/components/settings/view/Settings.tsx @@ -7,6 +7,7 @@ import AgentsSettingsTab from '../view/tabs/agents-settings/AgentsSettingsTab'; import AppearanceSettingsTab from '../view/tabs/AppearanceSettingsTab'; import CredentialsSettingsTab from '../view/tabs/api-settings/CredentialsSettingsTab'; import GitSettingsTab from '../view/tabs/git-settings/GitSettingsTab'; +import BrowserUseSettingsTab from '../view/tabs/browser-use-settings/BrowserUseSettingsTab'; import NotificationsSettingsTab from '../view/tabs/NotificationsSettingsTab'; import TasksSettingsTab from '../view/tabs/tasks-settings/TasksSettingsTab'; import PluginSettingsTab from '../../plugins/view/PluginSettingsTab'; @@ -139,17 +140,19 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set {activeTab === 'tasks' && } - {activeTab === 'notifications' && ( - - )} + {activeTab === 'browser' && } + + {activeTab === 'notifications' && ( + + )} {activeTab === 'api' && } diff --git a/src/components/settings/view/SettingsSidebar.tsx b/src/components/settings/view/SettingsSidebar.tsx index 149c1492..dde32a9e 100644 --- a/src/components/settings/view/SettingsSidebar.tsx +++ b/src/components/settings/view/SettingsSidebar.tsx @@ -1,4 +1,4 @@ -import { Bell, Bot, GitBranch, Info, Key, ListChecks, Palette, Puzzle } from 'lucide-react'; +import { Bell, Bot, GitBranch, Info, Key, ListChecks, MonitorPlay, Palette, Puzzle } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { cn } from '../../../lib/utils'; import { PillBar, Pill } from '../../../shared/view/ui'; @@ -21,6 +21,7 @@ const NAV_ITEMS: NavItem[] = [ { id: 'git', labelKey: 'mainTabs.git', icon: GitBranch }, { id: 'api', labelKey: 'mainTabs.apiTokens', icon: Key }, { id: 'tasks', labelKey: 'mainTabs.tasks', icon: ListChecks }, + { id: 'browser', labelKey: 'mainTabs.browser', icon: MonitorPlay }, { id: 'plugins', labelKey: 'mainTabs.plugins', icon: Puzzle }, { id: 'notifications', labelKey: 'mainTabs.notifications', icon: Bell }, { id: 'about', labelKey: 'mainTabs.about', icon: Info }, diff --git a/src/components/settings/view/tabs/browser-use-settings/BrowserUseSettingsTab.tsx b/src/components/settings/view/tabs/browser-use-settings/BrowserUseSettingsTab.tsx new file mode 100644 index 00000000..4cc0f86b --- /dev/null +++ b/src/components/settings/view/tabs/browser-use-settings/BrowserUseSettingsTab.tsx @@ -0,0 +1,164 @@ +import { useCallback, useEffect, useState } from 'react'; +import { Download, Loader2, MonitorPlay, RefreshCw } from 'lucide-react'; + +import { Button } from '../../../../../shared/view/ui'; +import { authenticatedFetch } from '../../../../../utils/api'; +import SettingsCard from '../../SettingsCard'; +import SettingsRow from '../../SettingsRow'; +import SettingsSection from '../../SettingsSection'; +import SettingsToggle from '../../SettingsToggle'; + +type BrowserUseSettings = { + enabled: boolean; +}; + +type BrowserUseStatus = { + enabled: boolean; + available: boolean; + runtime: 'cloud' | 'local'; + playwrightInstalled: boolean; + chromiumInstalled: boolean; + installInProgress: boolean; + message: string; +}; + +async function readJson(response: Response): Promise { + const data = await response.json(); + if (!response.ok || data.success === false) { + throw new Error(data.error || data.details || `Request failed (${response.status})`); + } + return data as T; +} + +export default function BrowserUseSettingsTab() { + const [settings, setSettings] = useState({ enabled: true }); + const [status, setStatus] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [isInstalling, setIsInstalling] = useState(false); + const [error, setError] = useState(null); + + const loadState = useCallback(async () => { + setError(null); + const [settingsResponse, statusResponse] = await Promise.all([ + authenticatedFetch('/api/browser-use/settings'), + authenticatedFetch('/api/browser-use/status'), + ]); + const settingsData = await readJson<{ data: { settings: BrowserUseSettings } }>(settingsResponse); + const statusData = await readJson<{ data: BrowserUseStatus }>(statusResponse); + setSettings(settingsData.data.settings); + setStatus(statusData.data); + }, []); + + useEffect(() => { + setIsLoading(true); + void loadState() + .catch((err) => setError(err instanceof Error ? err.message : 'Failed to load Browser Use settings')) + .finally(() => setIsLoading(false)); + }, [loadState]); + + const updateEnabled = async (enabled: boolean) => { + setIsSaving(true); + setError(null); + try { + const response = await authenticatedFetch('/api/browser-use/settings', { + method: 'PUT', + body: JSON.stringify({ enabled }), + }); + const data = await readJson<{ data: { settings: BrowserUseSettings } }>(response); + setSettings(data.data.settings); + await loadState(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to save Browser Use settings'); + } finally { + setIsSaving(false); + } + }; + + const installRuntime = async () => { + setIsInstalling(true); + setError(null); + try { + const response = await authenticatedFetch('/api/browser-use/runtime/install', { method: 'POST' }); + await readJson(response); + await loadState(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to install Browser Use runtime'); + } finally { + setIsInstalling(false); + } + }; + + const needsRuntime = Boolean(settings.enabled && status && (!status.playwrightInstalled || !status.chromiumInstalled)); + + return ( +
+ + + + void updateEnabled(value)} + ariaLabel="Enable Browser Use" + disabled={isLoading || isSaving} + /> + + +
+
+
+
+ + Runtime +
+

+ {status?.message || (isLoading ? 'Checking Browser Use runtime...' : 'Runtime status unavailable.')} +

+ {status && ( +
+ Mode: {status.runtime} + + Playwright: {status.playwrightInstalled ? 'installed' : 'missing'} + + + Chromium: {status.chromiumInstalled ? 'installed' : 'missing'} + +
+ )} +
+ +
+ + {needsRuntime && ( + + )} +
+
+ + {error && ( +
+ {error} +
+ )} +
+
+
+
+ ); +} diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index d5bc7900..bae8db89 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -94,6 +94,7 @@ "git": "Git", "apiTokens": "API & Tokens", "tasks": "Tasks", + "browser": "Browser Use", "notifications": "Notifications", "plugins": "Plugins", "about": "About" From e6263dbd1f64323f4459304d403e2f927586844d Mon Sep 17 00:00:00 2001 From: Simos Mikelatos Date: Mon, 15 Jun 2026 17:57:00 +0000 Subject: [PATCH 05/58] refactor: store browser use settings in database --- .../browser-use/browser-use.service.ts | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/server/modules/browser-use/browser-use.service.ts b/server/modules/browser-use/browser-use.service.ts index 5abcfbf3..df99925c 100644 --- a/server/modules/browser-use/browser-use.service.ts +++ b/server/modules/browser-use/browser-use.service.ts @@ -3,17 +3,16 @@ import { randomUUID } from 'node:crypto'; import { spawn } from 'node:child_process'; import dns from 'node:dns/promises'; import fs from 'node:fs'; -import fsPromises from 'node:fs/promises'; import net from 'node:net'; -import os from 'node:os'; -import path from 'node:path'; + +import { appConfigDb } from '@/modules/database/repositories/app-config.js'; const require = createRequire(import.meta.url); const IS_PLATFORM = process.env.VITE_IS_PLATFORM === 'true'; const MAX_SESSIONS_PER_OWNER = Number.parseInt(process.env.CLOUDCLI_BROWSER_USE_MAX_SESSIONS_PER_OWNER || '3', 10); const SESSION_TTL_MS = Number.parseInt(process.env.CLOUDCLI_BROWSER_USE_SESSION_TTL_MS || String(30 * 60 * 1000), 10); const ALLOW_PRIVATE_NETWORKS = process.env.CLOUDCLI_BROWSER_USE_ALLOW_PRIVATE_NETWORKS === '1'; -const SETTINGS_PATH = path.join(os.homedir(), '.cloudcli', 'browser-use-settings.json'); +const BROWSER_USE_SETTINGS_KEY = 'browser_use_settings'; type BrowserUseRuntime = 'cloud' | 'local'; type BrowserUseSessionStatus = 'ready' | 'stopped' | 'unavailable'; @@ -69,28 +68,29 @@ function getRuntime(): BrowserUseRuntime { return IS_PLATFORM ? 'cloud' : 'local'; } -async function readSettings(): Promise { +function readSettings(): BrowserUseSettings { try { - const raw = await fsPromises.readFile(SETTINGS_PATH, 'utf8'); + const raw = appConfigDb.get(BROWSER_USE_SETTINGS_KEY); + if (!raw) { + return DEFAULT_SETTINGS; + } + const parsed = JSON.parse(raw) as Partial; return { enabled: parsed.enabled !== false, }; } catch (error: any) { - if (error?.code !== 'ENOENT') { - console.warn('[Browser Use] Failed to read settings:', error?.message || error); - } + console.warn('[Browser Use] Failed to read settings:', error?.message || error); return DEFAULT_SETTINGS; } } -async function writeSettings(settings: BrowserUseSettings): Promise { +function writeSettings(settings: BrowserUseSettings): BrowserUseSettings { const normalized = { enabled: settings.enabled !== false, }; - await fsPromises.mkdir(path.dirname(SETTINGS_PATH), { recursive: true }); - await fsPromises.writeFile(SETTINGS_PATH, JSON.stringify(normalized, null, 2), 'utf8'); + appConfigDb.set(BROWSER_USE_SETTINGS_KEY, JSON.stringify(normalized)); return normalized; } @@ -356,7 +356,7 @@ export const browserUseService = { }, async updateSettings(settings: Partial) { - const current = await readSettings(); + const current = readSettings(); return writeSettings({ ...current, enabled: settings.enabled ?? current.enabled, @@ -364,7 +364,7 @@ export const browserUseService = { }, async getStatus() { - const settings = await readSettings(); + const settings = readSettings(); const readiness = getRuntimeReadiness(); const available = settings.enabled && readiness.playwrightInstalled && readiness.chromiumInstalled; @@ -421,7 +421,7 @@ export const browserUseService = { throw new Error(`Browser Use is limited to ${MAX_SESSIONS_PER_OWNER} active sessions per user.`); } - const settings = await readSettings(); + const settings = readSettings(); const readiness = getRuntimeReadiness(); if (!settings.enabled || !readiness.playwrightInstalled || !readiness.chromiumInstalled || !readiness.playwright) { session.message = getSetupMessage(settings, readiness); From 6e7e2ff4c1b21c05253ed0dd6edf7228a962243c Mon Sep 17 00:00:00 2001 From: Simos Mikelatos Date: Mon, 15 Jun 2026 18:12:27 +0000 Subject: [PATCH 06/58] feat: make browser use opt-in --- .../browser-use/browser-use.service.ts | 23 ++++++++++---- src/components/main-content/types/types.ts | 1 + .../main-content/view/MainContent.tsx | 30 +++++++++++++++++-- .../view/subcomponents/MainContentHeader.tsx | 2 ++ .../subcomponents/MainContentTabSwitcher.tsx | 16 ++++++++-- .../BrowserUseSettingsTab.tsx | 3 +- 6 files changed, 65 insertions(+), 10 deletions(-) diff --git a/server/modules/browser-use/browser-use.service.ts b/server/modules/browser-use/browser-use.service.ts index df99925c..bb53147f 100644 --- a/server/modules/browser-use/browser-use.service.ts +++ b/server/modules/browser-use/browser-use.service.ts @@ -61,7 +61,7 @@ let installPromise: Promise<{ success: boolean; message: string }> | null = null let lastInstallMessage: string | null = null; const DEFAULT_SETTINGS: BrowserUseSettings = { - enabled: true, + enabled: false, }; function getRuntime(): BrowserUseRuntime { @@ -77,7 +77,7 @@ function readSettings(): BrowserUseSettings { const parsed = JSON.parse(raw) as Partial; return { - enabled: parsed.enabled !== false, + enabled: parsed.enabled === true, }; } catch (error: any) { console.warn('[Browser Use] Failed to read settings:', error?.message || error); @@ -87,7 +87,7 @@ function readSettings(): BrowserUseSettings { function writeSettings(settings: BrowserUseSettings): BrowserUseSettings { const normalized = { - enabled: settings.enabled !== false, + enabled: settings.enabled === true, }; appConfigDb.set(BROWSER_USE_SETTINGS_KEY, JSON.stringify(normalized)); @@ -168,6 +168,14 @@ function runCommand(command: string, args: string[]): Promise { }); } +function formatInstallError(error: unknown): string { + const message = error instanceof Error ? error.message : String(error); + if (message.includes('sudo') && message.includes('password')) { + return 'Installing Chromium system dependencies requires administrator privileges. Run `npx playwright install-deps chromium` on the machine where CloudCLI runs, then try again.'; + } + return message || 'Failed to install Browser Use runtime.'; +} + async function installRuntime(): Promise<{ success: boolean; message: string }> { if (installPromise) { return installPromise; @@ -179,13 +187,18 @@ async function installRuntime(): Promise<{ success: boolean; message: string }> lastInstallMessage = 'Installing Playwright package...'; await runCommand(npmCommand, ['install', '--no-save', '--no-package-lock', 'playwright']); + if (process.platform === 'linux') { + lastInstallMessage = 'Installing Chromium system dependencies...'; + await runCommand(npmCommand, ['exec', '--', 'playwright', 'install-deps', 'chromium']); + } + lastInstallMessage = 'Installing Chromium runtime...'; await runCommand(npmCommand, ['exec', '--', 'playwright', 'install', 'chromium']); lastInstallMessage = 'Browser Use runtime installed.'; return { success: true, message: lastInstallMessage }; } catch (error) { - lastInstallMessage = error instanceof Error ? error.message : 'Failed to install Browser Use runtime.'; + lastInstallMessage = formatInstallError(error); return { success: false, message: lastInstallMessage }; } })(); @@ -359,7 +372,7 @@ export const browserUseService = { const current = readSettings(); return writeSettings({ ...current, - enabled: settings.enabled ?? current.enabled, + enabled: typeof settings.enabled === 'boolean' ? settings.enabled : current.enabled, }); }, diff --git a/src/components/main-content/types/types.ts b/src/components/main-content/types/types.ts index a7398795..6ae16ec8 100644 --- a/src/components/main-content/types/types.ts +++ b/src/components/main-content/types/types.ts @@ -64,6 +64,7 @@ export type MainContentHeaderProps = { selectedProject: Project; selectedSession: ProjectSession | null; shouldShowTasksTab: boolean; + shouldShowBrowserTab: boolean; isMobile: boolean; onMenuClick: () => void; }; diff --git a/src/components/main-content/view/MainContent.tsx b/src/components/main-content/view/MainContent.tsx index 12cfe7aa..5facdb43 100644 --- a/src/components/main-content/view/MainContent.tsx +++ b/src/components/main-content/view/MainContent.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import ChatInterface from '../../chat/view/ChatInterface'; import FileTree from '../../file-tree/view/FileTree'; @@ -11,6 +11,7 @@ import { useTaskMaster } from '../../../contexts/TaskMasterContext'; import { usePaletteOpsRegister } from '../../../contexts/PaletteOpsContext'; import { useTasksSettings } from '../../../contexts/TasksSettingsContext'; import { useUiPreferences } from '../../../hooks/useUiPreferences'; +import { authenticatedFetch } from '../../../utils/api'; import { useEditorSidebar } from '../../code-editor/hooks/useEditorSidebar'; import EditorSidebar from '../../code-editor/view/EditorSidebar'; import type { Project } from '../../../types/app'; @@ -56,8 +57,10 @@ function MainContent({ const { currentProject, setCurrentProject } = useTaskMaster() as TaskMasterContextValue; const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings() as TasksSettingsContextValue; + const [browserUseEnabled, setBrowserUseEnabled] = useState(false); const shouldShowTasksTab = Boolean(tasksEnabled && isTaskMasterInstalled); + const shouldShowBrowserTab = browserUseEnabled; const { editingFile, @@ -91,6 +94,28 @@ function MainContent({ } }, [shouldShowTasksTab, activeTab, setActiveTab]); + const loadBrowserUseSettings = useCallback(async () => { + try { + const response = await authenticatedFetch('/api/browser-use/settings'); + const data = await response.json(); + setBrowserUseEnabled(Boolean(response.ok && data?.success !== false && data?.data?.settings?.enabled)); + } catch { + setBrowserUseEnabled(false); + } + }, []); + + useEffect(() => { + void loadBrowserUseSettings(); + window.addEventListener('browserUseSettingsChanged', loadBrowserUseSettings); + return () => window.removeEventListener('browserUseSettingsChanged', loadBrowserUseSettings); + }, [loadBrowserUseSettings]); + + useEffect(() => { + if (!shouldShowBrowserTab && activeTab === 'browser') { + setActiveTab('chat'); + } + }, [shouldShowBrowserTab, activeTab, setActiveTab]); + usePaletteOpsRegister({ openFile: (filePath: string) => { setActiveTab('files'); @@ -114,6 +139,7 @@ function MainContent({ selectedProject={selectedProject} selectedSession={selectedSession} shouldShowTasksTab={shouldShowTasksTab} + shouldShowBrowserTab={shouldShowBrowserTab} isMobile={isMobile} onMenuClick={onMenuClick} /> @@ -172,7 +198,7 @@ function MainContent({ {shouldShowTasksTab && } - {activeTab === 'browser' && ( + {shouldShowBrowserTab && activeTab === 'browser' && (
diff --git a/src/components/main-content/view/subcomponents/MainContentHeader.tsx b/src/components/main-content/view/subcomponents/MainContentHeader.tsx index a9025c2b..f75013ce 100644 --- a/src/components/main-content/view/subcomponents/MainContentHeader.tsx +++ b/src/components/main-content/view/subcomponents/MainContentHeader.tsx @@ -10,6 +10,7 @@ export default function MainContentHeader({ selectedProject, selectedSession, shouldShowTasksTab, + shouldShowBrowserTab, isMobile, onMenuClick, }: MainContentHeaderProps) { @@ -59,6 +60,7 @@ export default function MainContentHeader({ activeTab={activeTab} setActiveTab={setActiveTab} shouldShowTasksTab={shouldShowTasksTab} + shouldShowBrowserTab={shouldShowBrowserTab} />
{canScrollRight && ( diff --git a/src/components/main-content/view/subcomponents/MainContentTabSwitcher.tsx b/src/components/main-content/view/subcomponents/MainContentTabSwitcher.tsx index 83c641df..bffe39d6 100644 --- a/src/components/main-content/view/subcomponents/MainContentTabSwitcher.tsx +++ b/src/components/main-content/view/subcomponents/MainContentTabSwitcher.tsx @@ -11,6 +11,7 @@ type MainContentTabSwitcherProps = { activeTab: AppTab; setActiveTab: Dispatch>; shouldShowTasksTab: boolean; + shouldShowBrowserTab: boolean; }; type BuiltInTab = { @@ -35,9 +36,15 @@ const BASE_TABS: BuiltInTab[] = [ { kind: 'builtin', id: 'shell', labelKey: 'tabs.shell', icon: Terminal }, { kind: 'builtin', id: 'files', labelKey: 'tabs.files', icon: Folder }, { kind: 'builtin', id: 'git', labelKey: 'tabs.git', icon: GitBranch }, - { kind: 'builtin', id: 'browser', labelKey: 'tabs.browser', icon: MonitorPlay }, ]; +const BROWSER_TAB: BuiltInTab = { + kind: 'builtin', + id: 'browser', + labelKey: 'tabs.browser', + icon: MonitorPlay, +}; + const TASKS_TAB: BuiltInTab = { kind: 'builtin', id: 'tasks', @@ -49,11 +56,16 @@ export default function MainContentTabSwitcher({ activeTab, setActiveTab, shouldShowTasksTab, + shouldShowBrowserTab, }: MainContentTabSwitcherProps) { const { t } = useTranslation(); const { plugins } = usePlugins(); - const builtInTabs: BuiltInTab[] = shouldShowTasksTab ? [...BASE_TABS, TASKS_TAB] : BASE_TABS; + const builtInTabs: BuiltInTab[] = [ + ...BASE_TABS, + ...(shouldShowBrowserTab ? [BROWSER_TAB] : []), + ...(shouldShowTasksTab ? [TASKS_TAB] : []), + ]; const pluginTabs: PluginTab[] = plugins .filter((p) => p.enabled) diff --git a/src/components/settings/view/tabs/browser-use-settings/BrowserUseSettingsTab.tsx b/src/components/settings/view/tabs/browser-use-settings/BrowserUseSettingsTab.tsx index 4cc0f86b..361482e6 100644 --- a/src/components/settings/view/tabs/browser-use-settings/BrowserUseSettingsTab.tsx +++ b/src/components/settings/view/tabs/browser-use-settings/BrowserUseSettingsTab.tsx @@ -31,7 +31,7 @@ async function readJson(response: Response): Promise { } export default function BrowserUseSettingsTab() { - const [settings, setSettings] = useState({ enabled: true }); + const [settings, setSettings] = useState({ enabled: false }); const [status, setStatus] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); @@ -67,6 +67,7 @@ export default function BrowserUseSettingsTab() { }); const data = await readJson<{ data: { settings: BrowserUseSettings } }>(response); setSettings(data.data.settings); + window.dispatchEvent(new Event('browserUseSettingsChanged')); await loadState(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to save Browser Use settings'); From 042652240600f088f4b421a81a9b8bc4dd7073bd Mon Sep 17 00:00:00 2001 From: Simos Mikelatos Date: Mon, 15 Jun 2026 19:47:58 +0000 Subject: [PATCH 07/58] feat: expose browser use to agents via MCP --- server/browser-use-mcp.ts | 390 +++++++++++++++++ server/cli.js | 8 + server/index.js | 4 + .../browser-use/browser-use-mcp.routes.ts | 120 ++++++ .../modules/browser-use/browser-use.routes.ts | 36 ++ .../browser-use/browser-use.service.ts | 407 +++++++++++++++++- .../browser-use/view/BrowserUsePanel.tsx | 60 ++- .../BrowserUseSettingsTab.tsx | 22 +- 8 files changed, 1030 insertions(+), 17 deletions(-) create mode 100644 server/browser-use-mcp.ts create mode 100644 server/modules/browser-use/browser-use-mcp.routes.ts diff --git a/server/browser-use-mcp.ts b/server/browser-use-mcp.ts new file mode 100644 index 00000000..22a4c3e4 --- /dev/null +++ b/server/browser-use-mcp.ts @@ -0,0 +1,390 @@ +#!/usr/bin/env node +import './load-env.js'; + +type JsonRpcRequest = { + jsonrpc: '2.0'; + id?: string | number | null; + method: string; + params?: Record; +}; + +type ToolDefinition = { + name: string; + description: string; + inputSchema: Record; +}; + +const textResponse = (text: string) => ({ + content: [{ type: 'text', text }], +}); + +const jsonResponse = (value: unknown) => textResponse(JSON.stringify(value, null, 2)); + +const readString = (value: unknown, name: string): string => { + if (typeof value !== 'string' || value.trim() === '') { + throw new Error(`${name} is required.`); + } + return value.trim(); +}; + +const readOptionalString = (value: unknown): string | undefined => + typeof value === 'string' && value.trim() ? value.trim() : undefined; + +const readNumber = (value: unknown): number | undefined => + typeof value === 'number' && Number.isFinite(value) ? value : undefined; + +const apiUrl = (process.env.CLOUDCLI_BROWSER_USE_API_URL || 'http://127.0.0.1:3001/api/browser-use-mcp').replace(/\/$/, ''); +const apiToken = process.env.CLOUDCLI_BROWSER_USE_MCP_TOKEN || ''; + +async function callBrowserUseApi(toolName: string, input: Record) { + if (!apiToken) { + throw new Error('CLOUDCLI_BROWSER_USE_MCP_TOKEN is not configured.'); + } + + const response = await fetch(`${apiUrl}/tools/${encodeURIComponent(toolName)}`, { + method: 'POST', + headers: { + Authorization: `Bearer ${apiToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(input), + }); + const data = await response.json() as { success?: boolean; data?: unknown; error?: string }; + if (!response.ok || data.success === false) { + throw new Error(data.error || `Browser Use API request failed (${response.status})`); + } + return data.data; +} + +const sessionIdSchema = { + type: 'object', + properties: { + sessionId: { type: 'string', description: 'Browser Use session id.' }, + }, + required: ['sessionId'], +}; + +const tools: ToolDefinition[] = [ + { + name: 'browser_create_session', + description: 'Create a temporary Browser Use session that the agent can control. Optionally provide a background profileName to reuse cookies and storage.', + inputSchema: { + type: 'object', + properties: { + profileName: { type: 'string', description: 'Optional background profile name for persistent browser storage.' }, + }, + }, + }, + { + name: 'browser_list_sessions', + description: 'List Browser Use sessions currently available to agents.', + inputSchema: { type: 'object', properties: {} }, + }, + { + name: 'browser_snapshot', + description: 'Capture current page metadata, screenshot data URL, and visible body text for a Browser Use session.', + inputSchema: sessionIdSchema, + }, + { + name: 'browser_take_screenshot', + description: 'Capture the latest screenshot for a Browser Use session.', + inputSchema: sessionIdSchema, + }, + { + name: 'browser_navigate', + description: 'Navigate a Browser Use session to an HTTP or HTTPS URL.', + inputSchema: { + type: 'object', + properties: { + sessionId: { type: 'string' }, + url: { type: 'string' }, + }, + required: ['sessionId', 'url'], + }, + }, + { + name: 'browser_click', + description: 'Click an element by CSS selector, visible text, or x/y coordinates.', + inputSchema: { + type: 'object', + properties: { + sessionId: { type: 'string' }, + selector: { type: 'string' }, + text: { type: 'string' }, + x: { type: 'number' }, + y: { type: 'number' }, + }, + required: ['sessionId'], + }, + }, + { + name: 'browser_type', + description: 'Type text into the focused page or fill a CSS selector. Set submit to press Enter after typing.', + inputSchema: { + type: 'object', + properties: { + sessionId: { type: 'string' }, + selector: { type: 'string' }, + text: { type: 'string' }, + submit: { type: 'boolean' }, + }, + required: ['sessionId', 'text'], + }, + }, + { + name: 'browser_fill_form', + description: 'Fill multiple form fields using CSS selectors.', + inputSchema: { + type: 'object', + properties: { + sessionId: { type: 'string' }, + fields: { + type: 'array', + items: { + type: 'object', + properties: { + selector: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['selector', 'value'], + }, + }, + }, + required: ['sessionId', 'fields'], + }, + }, + { + name: 'browser_press_key', + description: 'Press a keyboard key, for example Enter, Escape, Tab, or Control+A.', + inputSchema: { + type: 'object', + properties: { + sessionId: { type: 'string' }, + key: { type: 'string' }, + }, + required: ['sessionId', 'key'], + }, + }, + { + name: 'browser_select_option', + description: 'Select option values in a select element found by CSS selector.', + inputSchema: { + type: 'object', + properties: { + sessionId: { type: 'string' }, + selector: { type: 'string' }, + values: { type: 'array', items: { type: 'string' } }, + }, + required: ['sessionId', 'selector', 'values'], + }, + }, + { + name: 'browser_wait_for', + description: 'Wait for visible text, a URL pattern, or a short timeout.', + inputSchema: { + type: 'object', + properties: { + sessionId: { type: 'string' }, + text: { type: 'string' }, + url: { type: 'string' }, + timeoutMs: { type: 'number' }, + }, + required: ['sessionId'], + }, + }, + { + name: 'browser_tabs', + description: 'List, open, select, or close tabs in a Browser Use session.', + inputSchema: { + type: 'object', + properties: { + sessionId: { type: 'string' }, + action: { type: 'string', enum: ['list', 'new', 'select', 'close'] }, + index: { type: 'number' }, + url: { type: 'string' }, + }, + required: ['sessionId'], + }, + }, + { + name: 'browser_close_session', + description: 'Stop a Browser Use session controlled by agents.', + inputSchema: sessionIdSchema, + }, +]; + +async function callTool(name: string, args: Record) { + switch (name) { + case 'browser_create_session': + return jsonResponse(await callBrowserUseApi(name, { + profileName: readOptionalString(args.profileName), + })); + case 'browser_list_sessions': + return jsonResponse(await callBrowserUseApi(name, {})); + case 'browser_snapshot': + return jsonResponse(await callBrowserUseApi(name, { sessionId: readString(args.sessionId, 'sessionId') })); + case 'browser_take_screenshot': { + return jsonResponse(await callBrowserUseApi(name, { sessionId: readString(args.sessionId, 'sessionId') })); + } + case 'browser_navigate': + return jsonResponse(await callBrowserUseApi(name, { + sessionId: readString(args.sessionId, 'sessionId'), + url: readString(args.url, 'url'), + })); + case 'browser_click': + return jsonResponse(await callBrowserUseApi(name, { + sessionId: readString(args.sessionId, 'sessionId'), + selector: readOptionalString(args.selector), + text: readOptionalString(args.text), + x: readNumber(args.x), + y: readNumber(args.y), + })); + case 'browser_type': + return jsonResponse(await callBrowserUseApi(name, { + sessionId: readString(args.sessionId, 'sessionId'), + selector: readOptionalString(args.selector), + text: readString(args.text, 'text'), + submit: args.submit === true, + })); + case 'browser_fill_form': { + const fields = Array.isArray(args.fields) + ? args.fields.map((field) => { + const record = field as Record; + return { + selector: readString(record.selector, 'field.selector'), + value: readString(record.value, 'field.value'), + }; + }) + : []; + return jsonResponse(await callBrowserUseApi(name, { + sessionId: readString(args.sessionId, 'sessionId'), + fields, + })); + } + case 'browser_press_key': + return jsonResponse(await callBrowserUseApi(name, { + sessionId: readString(args.sessionId, 'sessionId'), + key: readString(args.key, 'key'), + })); + case 'browser_select_option': + return jsonResponse(await callBrowserUseApi(name, { + sessionId: readString(args.sessionId, 'sessionId'), + selector: readString(args.selector, 'selector'), + values: Array.isArray(args.values) ? args.values.filter((value): value is string => typeof value === 'string') : [], + })); + case 'browser_wait_for': + return jsonResponse(await callBrowserUseApi(name, { + sessionId: readString(args.sessionId, 'sessionId'), + text: readOptionalString(args.text), + url: readOptionalString(args.url), + timeoutMs: readNumber(args.timeoutMs), + })); + case 'browser_tabs': + return jsonResponse(await callBrowserUseApi(name, { + sessionId: readString(args.sessionId, 'sessionId'), + action: args.action === 'new' || args.action === 'select' || args.action === 'close' || args.action === 'list' + ? args.action + : undefined, + index: readNumber(args.index), + url: readOptionalString(args.url), + })); + case 'browser_close_session': + return jsonResponse(await callBrowserUseApi(name, { sessionId: readString(args.sessionId, 'sessionId') })); + default: + throw new Error(`Unknown tool: ${name}`); + } +} + +async function handleMessage(message: JsonRpcRequest) { + if (message.method === 'initialize') { + return { + protocolVersion: '2024-11-05', + capabilities: { tools: {} }, + serverInfo: { name: 'cloudcli-browser-use', version: '1.0.0' }, + }; + } + + if (message.method === 'tools/list') { + return { tools }; + } + + if (message.method === 'tools/call') { + const params = message.params || {}; + const name = readString(params.name, 'name'); + const args = (params.arguments && typeof params.arguments === 'object' + ? params.arguments + : {}) as Record; + return callTool(name, args); + } + + if (message.method.startsWith('notifications/')) { + return undefined; + } + + throw new Error(`Unsupported method: ${message.method}`); +} + +function writeMessage(message: Record) { + const payload = JSON.stringify(message); + process.stdout.write(`Content-Length: ${Buffer.byteLength(payload, 'utf8')}\r\n\r\n${payload}`); +} + +function sendResult(id: string | number | null | undefined, result: unknown) { + if (id === undefined) { + return; + } + writeMessage({ jsonrpc: '2.0', id, result }); +} + +function sendError(id: string | number | null | undefined, error: unknown) { + if (id === undefined) { + return; + } + writeMessage({ + jsonrpc: '2.0', + id, + error: { + code: -32000, + message: error instanceof Error ? error.message : String(error), + }, + }); +} + +let buffer = Buffer.alloc(0); + +process.stdin.on('data', (chunk) => { + buffer = Buffer.concat([buffer, chunk]); + while (true) { + const headerEnd = buffer.indexOf('\r\n\r\n'); + if (headerEnd === -1) { + return; + } + + const header = buffer.slice(0, headerEnd).toString('utf8'); + const lengthMatch = /Content-Length:\s*(\d+)/i.exec(header); + if (!lengthMatch) { + buffer = buffer.slice(headerEnd + 4); + continue; + } + + const length = Number.parseInt(lengthMatch[1], 10); + const messageStart = headerEnd + 4; + const messageEnd = messageStart + length; + if (buffer.length < messageEnd) { + return; + } + + const rawMessage = buffer.slice(messageStart, messageEnd).toString('utf8'); + buffer = buffer.slice(messageEnd); + + void (async () => { + const request = JSON.parse(rawMessage) as JsonRpcRequest; + try { + const result = await handleMessage(request); + sendResult(request.id, result); + } catch (error) { + sendError(request.id, error); + } + })(); + } +}); diff --git a/server/cli.js b/server/cli.js index 9fa99ae3..e6daacc4 100755 --- a/server/cli.js +++ b/server/cli.js @@ -8,6 +8,7 @@ * (no args) - Start the server (default) * start - Start the server * sandbox - Manage Docker sandbox environments + * browser-use-mcp - Run Browser Use MCP stdio server * status - Show configuration and data locations * help - Show help information * version - Show version information @@ -605,6 +606,10 @@ async function startServer() { await import('./index.js'); } +async function startBrowserUseMcp() { + await import('./browser-use-mcp.js'); +} + // Parse CLI arguments function parseArgs(args) { const parsed = { command: 'start', options: {} }; @@ -658,6 +663,9 @@ async function main() { case 'sandbox': await sandboxCommand(remainingArgs || []); break; + case 'browser-use-mcp': + await startBrowserUseMcp(); + break; case 'status': case 'info': showStatus(); diff --git a/server/index.js b/server/index.js index 0a812920..3fcd438a 100755 --- a/server/index.js +++ b/server/index.js @@ -62,6 +62,7 @@ import geminiRoutes from './routes/gemini.js'; import pluginsRoutes from './routes/plugins.js'; import providerRoutes from './modules/providers/provider.routes.js'; import browserUseRoutes from './modules/browser-use/browser-use.routes.js'; +import browserUseMcpRoutes from './modules/browser-use/browser-use-mcp.routes.js'; import { browserUseService } from './modules/browser-use/browser-use.service.js'; import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js'; import { initializeDatabase, projectsDb, sessionsDb } from './modules/database/index.js'; @@ -195,6 +196,9 @@ app.use('/api/gemini', authenticateToken, geminiRoutes); // Plugins API Routes (protected) app.use('/api/plugins', authenticateToken, pluginsRoutes); +// Browser Use MCP bridge API (local token protected) +app.use('/api/browser-use-mcp', browserUseMcpRoutes); + // Browser Use API Routes (protected) app.use('/api/browser-use', authenticateToken, browserUseRoutes); diff --git a/server/modules/browser-use/browser-use-mcp.routes.ts b/server/modules/browser-use/browser-use-mcp.routes.ts new file mode 100644 index 00000000..335ffa18 --- /dev/null +++ b/server/modules/browser-use/browser-use-mcp.routes.ts @@ -0,0 +1,120 @@ +import express from 'express'; + +import { browserUseService } from '@/modules/browser-use/browser-use.service.js'; + +const router = express.Router(); + +function readBearerToken(header: unknown): string | null { + if (typeof header !== 'string') { + return null; + } + const match = /^Bearer\s+(.+)$/i.exec(header.trim()); + return match?.[1] || null; +} + +router.use((req, res, next) => { + const expected = browserUseService.getMcpToken(); + const token = readBearerToken(req.headers.authorization) || String(req.headers['x-browser-use-mcp-token'] || ''); + if (!token || token !== expected) { + res.status(401).json({ success: false, error: 'Invalid Browser Use MCP token.' }); + return; + } + next(); +}); + +router.post('/tools/:toolName', async (req, res) => { + try { + const input = (req.body && typeof req.body === 'object' ? req.body : {}) as Record; + const sessionId = typeof input.sessionId === 'string' ? input.sessionId : ''; + const toolName = req.params.toolName; + let result: unknown; + + switch (toolName) { + case 'browser_create_session': + result = await browserUseService.createAgentSession({ + profileName: typeof input.profileName === 'string' ? input.profileName : null, + }); + break; + case 'browser_list_sessions': + result = await browserUseService.listAgentSessions(); + break; + case 'browser_snapshot': + case 'browser_take_screenshot': + result = await browserUseService.agentSnapshot(sessionId); + break; + case 'browser_navigate': + result = await browserUseService.agentNavigate(sessionId, String(input.url || '')); + break; + case 'browser_click': + result = await browserUseService.agentClick(sessionId, { + selector: typeof input.selector === 'string' ? input.selector : undefined, + text: typeof input.text === 'string' ? input.text : undefined, + x: typeof input.x === 'number' ? input.x : undefined, + y: typeof input.y === 'number' ? input.y : undefined, + }); + break; + case 'browser_type': + result = await browserUseService.agentType(sessionId, { + selector: typeof input.selector === 'string' ? input.selector : undefined, + text: String(input.text || ''), + submit: input.submit === true, + }); + break; + case 'browser_fill_form': + result = await browserUseService.agentFillForm( + sessionId, + Array.isArray(input.fields) + ? input.fields.map((field) => { + const record = field as Record; + return { + selector: String(record.selector || ''), + value: String(record.value || ''), + }; + }) + : [], + ); + break; + case 'browser_press_key': + result = await browserUseService.agentPressKey(sessionId, String(input.key || '')); + break; + case 'browser_select_option': + result = await browserUseService.agentSelectOption( + sessionId, + String(input.selector || ''), + Array.isArray(input.values) ? input.values.filter((value): value is string => typeof value === 'string') : [], + ); + break; + case 'browser_wait_for': + result = await browserUseService.agentWaitFor(sessionId, { + text: typeof input.text === 'string' ? input.text : undefined, + url: typeof input.url === 'string' ? input.url : undefined, + timeoutMs: typeof input.timeoutMs === 'number' ? input.timeoutMs : undefined, + }); + break; + case 'browser_tabs': + result = await browserUseService.agentTabs(sessionId, { + action: input.action === 'new' || input.action === 'select' || input.action === 'close' || input.action === 'list' + ? input.action + : undefined, + index: typeof input.index === 'number' ? input.index : undefined, + url: typeof input.url === 'string' ? input.url : undefined, + }); + break; + case 'browser_close_session': + result = await browserUseService.agentStopSession(sessionId); + break; + default: + res.status(404).json({ success: false, error: `Unknown Browser Use MCP tool "${toolName}".` }); + return; + } + + res.json({ success: true, data: result }); + } catch (error) { + res.status(400).json({ + success: false, + error: error instanceof Error ? error.message : 'Browser Use MCP tool failed.', + }); + } +}); + +export default router; diff --git a/server/modules/browser-use/browser-use.routes.ts b/server/modules/browser-use/browser-use.routes.ts index c730dd53..cab7e592 100644 --- a/server/modules/browser-use/browser-use.routes.ts +++ b/server/modules/browser-use/browser-use.routes.ts @@ -56,6 +56,18 @@ router.put('/settings', async (req, res) => { } }); +router.post('/agent-tools/register', async (_req, res) => { + try { + const result = await browserUseService.registerAgentMcp(); + res.status(201).json({ success: true, data: result }); + } catch (error) { + res.status(400).json({ + success: false, + error: error instanceof Error ? error.message : 'Failed to register Browser Use MCP.', + }); + } +}); + router.post('/runtime/install', async (_req, res) => { try { const result = await browserUseService.installRuntime(); @@ -107,6 +119,30 @@ router.post('/sessions/:sessionId/navigate', async (req: AuthenticatedRequest, r } }); +router.post('/sessions/:sessionId/agent-access/grant', async (req: AuthenticatedRequest, res) => { + try { + const session = await browserUseService.grantAgentAccess(requireUser(req), readParam(req.params.sessionId)); + res.json({ success: true, data: { session } }); + } catch (error) { + res.status(400).json({ + success: false, + error: error instanceof Error ? error.message : 'Failed to grant agent access.', + }); + } +}); + +router.post('/sessions/:sessionId/agent-access/revoke', async (req: AuthenticatedRequest, res) => { + try { + const session = await browserUseService.revokeAgentAccess(requireUser(req), readParam(req.params.sessionId)); + res.json({ success: true, data: { session } }); + } catch (error) { + res.status(400).json({ + success: false, + error: error instanceof Error ? error.message : 'Failed to revoke agent access.', + }); + } +}); + router.post('/sessions/:sessionId/stop', async (req: AuthenticatedRequest, res) => { try { const result = await browserUseService.stopSession(requireUser(req), readParam(req.params.sessionId)); diff --git a/server/modules/browser-use/browser-use.service.ts b/server/modules/browser-use/browser-use.service.ts index bb53147f..e4ab8bec 100644 --- a/server/modules/browser-use/browser-use.service.ts +++ b/server/modules/browser-use/browser-use.service.ts @@ -1,18 +1,24 @@ import { createRequire } from 'node:module'; -import { randomUUID } from 'node:crypto'; +import { randomBytes, randomUUID } from 'node:crypto'; import { spawn } from 'node:child_process'; import dns from 'node:dns/promises'; import fs from 'node:fs'; +import os from 'node:os'; import net from 'node:net'; +import path from 'node:path'; import { appConfigDb } from '@/modules/database/repositories/app-config.js'; +import { providerMcpService } from '@/modules/providers/services/mcp.service.js'; +import { getModuleDir } from '@/utils/runtime-paths.js'; const require = createRequire(import.meta.url); +const __dirname = getModuleDir(import.meta.url); const IS_PLATFORM = process.env.VITE_IS_PLATFORM === 'true'; const MAX_SESSIONS_PER_OWNER = Number.parseInt(process.env.CLOUDCLI_BROWSER_USE_MAX_SESSIONS_PER_OWNER || '3', 10); const SESSION_TTL_MS = Number.parseInt(process.env.CLOUDCLI_BROWSER_USE_SESSION_TTL_MS || String(30 * 60 * 1000), 10); const ALLOW_PRIVATE_NETWORKS = process.env.CLOUDCLI_BROWSER_USE_ALLOW_PRIVATE_NETWORKS === '1'; const BROWSER_USE_SETTINGS_KEY = 'browser_use_settings'; +const BROWSER_USE_MCP_TOKEN_KEY = 'browser_use_mcp_token'; type BrowserUseRuntime = 'cloud' | 'local'; type BrowserUseSessionStatus = 'ready' | 'stopped' | 'unavailable'; @@ -20,6 +26,7 @@ type BrowserUseSessionStatus = 'ready' | 'stopped' | 'unavailable'; type BrowserUseSession = { id: string; ownerId: string; + createdBy: 'user' | 'agent'; runtime: BrowserUseRuntime; status: BrowserUseSessionStatus; url: string | null; @@ -29,12 +36,15 @@ type BrowserUseSession = { updatedAt: string; lastAction: string | null; message: string | null; + agentAccessEnabled: boolean; + profileName: string | null; }; type PublicBrowserUseSession = Omit; type RuntimeHandle = { browser?: any; + context?: any; page?: any; }; @@ -44,6 +54,7 @@ type BrowserUseOwner = { type BrowserUseSettings = { enabled: boolean; + agentToolsEnabled: boolean; }; type RuntimeReadiness = { @@ -62,7 +73,12 @@ let lastInstallMessage: string | null = null; const DEFAULT_SETTINGS: BrowserUseSettings = { enabled: false, + agentToolsEnabled: false, }; +const AGENT_OWNER_ID = 'agent'; +const PROFILE_ROOT = path.join(os.homedir(), '.cloudcli', 'browser-use', 'profiles'); +const MCP_SERVER_NAME = 'cloudcli-browser-use'; +const MCP_PROVIDERS = ['claude', 'codex', 'cursor', 'gemini', 'opencode']; function getRuntime(): BrowserUseRuntime { return IS_PLATFORM ? 'cloud' : 'local'; @@ -78,6 +94,7 @@ function readSettings(): BrowserUseSettings { const parsed = JSON.parse(raw) as Partial; return { enabled: parsed.enabled === true, + agentToolsEnabled: parsed.agentToolsEnabled === true, }; } catch (error: any) { console.warn('[Browser Use] Failed to read settings:', error?.message || error); @@ -88,12 +105,23 @@ function readSettings(): BrowserUseSettings { function writeSettings(settings: BrowserUseSettings): BrowserUseSettings { const normalized = { enabled: settings.enabled === true, + agentToolsEnabled: settings.agentToolsEnabled === true, }; appConfigDb.set(BROWSER_USE_SETTINGS_KEY, JSON.stringify(normalized)); return normalized; } +function getOrCreateMcpToken(): string { + const existing = appConfigDb.get(BROWSER_USE_MCP_TOKEN_KEY); + if (existing) { + return existing; + } + const token = randomBytes(32).toString('hex'); + appConfigDb.set(BROWSER_USE_MCP_TOKEN_KEY, token); + return token; +} + function getSetupMessage(settings: BrowserUseSettings, readiness: RuntimeReadiness): string { if (!settings.enabled) { return 'Browser Use is disabled in settings.'; @@ -118,6 +146,45 @@ function getPlaywright(): any | null { } } +function getMcpCommand(): { command: string; args: string[] } { + const serverDir = path.resolve(__dirname, '..', '..'); + const mcpScriptPath = path.join(serverDir, 'browser-use-mcp.js'); + if (fs.existsSync(mcpScriptPath)) { + return { + command: process.execPath, + args: [mcpScriptPath], + }; + } + + return { + command: 'cloudcli', + args: ['browser-use-mcp'], + }; +} + +function getMcpApiUrl(): string { + const port = process.env.SERVER_PORT || process.env.PORT || '3001'; + return `http://127.0.0.1:${port}/api/browser-use-mcp`; +} + +function normalizeProfileName(profileName?: string | null): string | null { + const normalized = String(profileName || '').trim(); + if (!normalized) { + return null; + } + + return normalized.slice(0, 80); +} + +function getProfilePath(profileName: string): string { + const safeName = profileName + .toLowerCase() + .replace(/[^a-z0-9._-]+/g, '-') + .replace(/^-+|-+$/g, '') + .slice(0, 80) || 'default'; + return path.join(PROFILE_ROOT, safeName); +} + function getRuntimeReadiness(): RuntimeReadiness { const playwright = getPlaywright(); const readiness: RuntimeReadiness = { @@ -333,6 +400,7 @@ function ownerSessions(ownerId: string): BrowserUseSession[] { async function closeHandle(sessionId: string): Promise { const handle = handles.get(sessionId); handles.delete(sessionId); + await handle?.context?.close?.().catch(() => undefined); await handle?.browser?.close().catch(() => undefined); } @@ -370,10 +438,24 @@ export const browserUseService = { async updateSettings(settings: Partial) { const current = readSettings(); - return writeSettings({ + const nextSettings = { ...current, enabled: typeof settings.enabled === 'boolean' ? settings.enabled : current.enabled, - }); + agentToolsEnabled: typeof settings.agentToolsEnabled === 'boolean' + ? settings.agentToolsEnabled + : current.agentToolsEnabled, + }; + if (!nextSettings.enabled) { + nextSettings.agentToolsEnabled = false; + } + + const next = writeSettings(nextSettings); + if (next.agentToolsEnabled) { + await this.registerAgentMcp(); + } else if (current.agentToolsEnabled) { + await this.unregisterAgentMcp(); + } + return next; }, async getStatus() { @@ -389,13 +471,53 @@ export const browserUseService = { chromiumInstalled: readiness.chromiumInstalled, installInProgress: readiness.installInProgress, sessionCount: sessions.size, - mcpRecommended: true, + agentToolsEnabled: settings.agentToolsEnabled, + mcpRecommended: !settings.agentToolsEnabled, message: available ? 'Browser Use runtime is available.' : getSetupMessage(settings, readiness), }; }, + async registerAgentMcp() { + const { command, args } = getMcpCommand(); + const results = await providerMcpService.addMcpServerToAllProviders({ + name: MCP_SERVER_NAME, + scope: 'user', + transport: 'stdio', + command, + args, + env: { + CLOUDCLI_BROWSER_USE_MCP_TOKEN: getOrCreateMcpToken(), + CLOUDCLI_BROWSER_USE_API_URL: getMcpApiUrl(), + }, + }); + return { name: MCP_SERVER_NAME, command, args, results }; + }, + + getMcpToken() { + return getOrCreateMcpToken(); + }, + + async unregisterAgentMcp() { + const results = await Promise.all(MCP_PROVIDERS.map(async (provider) => { + try { + const result = await providerMcpService.removeProviderMcpServer(provider, { + name: MCP_SERVER_NAME, + scope: 'user', + }); + return { provider, removed: result.removed }; + } catch (error) { + return { + provider, + removed: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + })); + return { name: MCP_SERVER_NAME, results }; + }, + async installRuntime() { const result = await installRuntime(); return { @@ -407,17 +529,22 @@ export const browserUseService = { async listSessions(owner: BrowserUseOwner) { const ownerId = getOwnerId(owner); await expireStaleSessions(); - return ownerSessions(ownerId).map(publicSession); + return [...sessions.values()] + .filter((session) => session.ownerId === ownerId || session.ownerId === AGENT_OWNER_ID || session.agentAccessEnabled) + .map(publicSession); }, - async createSession(owner: BrowserUseOwner) { + async createSession(owner: BrowserUseOwner, options?: { createdBy?: 'user' | 'agent'; profileName?: string | null; agentAccessEnabled?: boolean }) { const ownerId = getOwnerId(owner); await expireStaleSessions(); + const createdBy = options?.createdBy ?? 'user'; + const profileName = normalizeProfileName(options?.profileName); const now = new Date().toISOString(); const session: BrowserUseSession = { id: randomUUID(), ownerId, + createdBy, runtime: getRuntime(), status: 'unavailable', url: null, @@ -427,6 +554,8 @@ export const browserUseService = { updatedAt: now, lastAction: 'create', message: null, + agentAccessEnabled: options?.agentAccessEnabled ?? createdBy === 'agent', + profileName, }; const activeOwnerSessions = ownerSessions(ownerId).filter((item) => item.status === 'ready'); @@ -442,20 +571,97 @@ export const browserUseService = { return publicSession(session); } - const browser = await readiness.playwright.chromium.launch({ + let browser: any | undefined; + let context: any | undefined; + let page: any; + const launchOptions = { headless: true, args: ['--disable-dev-shm-usage'], - }); - const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); + }; + const contextOptions = { + viewport: { width: 1440, height: 900 }, + serviceWorkers: 'block', + }; + + if (profileName) { + fs.mkdirSync(PROFILE_ROOT, { recursive: true }); + context = await readiness.playwright.chromium.launchPersistentContext(getProfilePath(profileName), { + ...launchOptions, + ...contextOptions, + }); + page = context.pages()[0] || await context.newPage(); + } else { + browser = await readiness.playwright.chromium.launch(launchOptions); + context = await browser.newContext(contextOptions); + page = await context.newPage(); + } await attachRequestGuard(page); session.status = 'ready'; session.message = 'Browser session is ready.'; sessions.set(session.id, session); - handles.set(session.id, { browser, page }); + handles.set(session.id, { browser, context, page }); await captureSession(session, page); return publicSession(session); }, + async grantAgentAccess(owner: BrowserUseOwner, sessionId: string) { + const ownerId = getOwnerId(owner); + const session = sessions.get(sessionId); + if (!session || (session.ownerId !== ownerId && session.ownerId !== AGENT_OWNER_ID)) { + throw new Error('Browser session not found.'); + } + session.agentAccessEnabled = true; + session.updatedAt = new Date().toISOString(); + session.lastAction = 'agent_access:grant'; + return publicSession(session); + }, + + async revokeAgentAccess(owner: BrowserUseOwner, sessionId: string) { + const ownerId = getOwnerId(owner); + const session = sessions.get(sessionId); + if (!session || (session.ownerId !== ownerId && session.ownerId !== AGENT_OWNER_ID)) { + throw new Error('Browser session not found.'); + } + session.agentAccessEnabled = false; + session.updatedAt = new Date().toISOString(); + session.lastAction = 'agent_access:revoke'; + return publicSession(session); + }, + + async listAgentSessions() { + const settings = readSettings(); + if (!settings.enabled || !settings.agentToolsEnabled) { + return []; + } + await expireStaleSessions(); + return [...sessions.values()] + .filter((session) => session.agentAccessEnabled || session.ownerId === AGENT_OWNER_ID) + .map(publicSession); + }, + + async createAgentSession(options?: { profileName?: string | null }) { + const settings = readSettings(); + if (!settings.enabled || !settings.agentToolsEnabled) { + throw new Error('Browser Use agent tools are disabled.'); + } + return this.createSession( + { id: AGENT_OWNER_ID }, + { createdBy: 'agent', profileName: options?.profileName, agentAccessEnabled: true }, + ); + }, + + async getAgentSession(sessionId: string) { + const settings = readSettings(); + if (!settings.enabled || !settings.agentToolsEnabled) { + throw new Error('Browser Use agent tools are disabled.'); + } + const session = sessions.get(sessionId); + if (!session || (!session.agentAccessEnabled && session.ownerId !== AGENT_OWNER_ID)) { + throw new Error('Browser session is not shared with agents.'); + } + return session; + }, + async navigate(owner: BrowserUseOwner, sessionId: string, rawUrl: string) { const ownerId = getOwnerId(owner); await expireStaleSessions(); @@ -481,10 +687,184 @@ export const browserUseService = { return publicSession(session); }, + async agentNavigate(sessionId: string, rawUrl: string) { + await this.getAgentSession(sessionId); + return this.navigate({ id: AGENT_OWNER_ID }, sessionId, rawUrl).catch(async (error) => { + const session = await this.getAgentSession(sessionId); + if (session.ownerId !== AGENT_OWNER_ID) { + const url = await normalizeUrl(rawUrl); + const handle = handles.get(sessionId); + if (!handle?.page) { + throw new Error('Browser runtime handle is not available.'); + } + await handle.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 }); + session.lastAction = `navigate:${url}`; + await captureSession(session, handle.page); + return publicSession(session); + } + throw error; + }); + }, + + async agentSnapshot(sessionId: string) { + const session = await this.getAgentSession(sessionId); + const handle = handles.get(sessionId); + if (!handle?.page) { + throw new Error('Browser runtime handle is not available.'); + } + await captureSession(session, handle.page); + const text = await handle.page.locator('body').innerText({ timeout: 5_000 }).catch(() => ''); + return { + session: publicSession(session), + text: text.slice(0, 30_000), + }; + }, + + async agentClick(sessionId: string, input: { selector?: string; text?: string; x?: number; y?: number }) { + const session = await this.getAgentSession(sessionId); + const handle = handles.get(sessionId); + if (!handle?.page) { + throw new Error('Browser runtime handle is not available.'); + } + + if (input.selector) { + await handle.page.locator(input.selector).first().click({ timeout: 10_000 }); + } else if (input.text) { + await handle.page.getByText(input.text, { exact: false }).first().click({ timeout: 10_000 }); + } else if (typeof input.x === 'number' && typeof input.y === 'number') { + await handle.page.mouse.click(input.x, input.y); + } else { + throw new Error('Provide selector, text, or x/y coordinates.'); + } + + session.lastAction = 'click'; + await captureSession(session, handle.page); + return publicSession(session); + }, + + async agentType(sessionId: string, input: { selector?: string; text: string; submit?: boolean }) { + const session = await this.getAgentSession(sessionId); + const handle = handles.get(sessionId); + if (!handle?.page) { + throw new Error('Browser runtime handle is not available.'); + } + + if (input.selector) { + await handle.page.locator(input.selector).first().fill(input.text, { timeout: 10_000 }); + } else { + await handle.page.keyboard.type(input.text); + } + if (input.submit) { + await handle.page.keyboard.press('Enter'); + } + + session.lastAction = 'type'; + await captureSession(session, handle.page); + return publicSession(session); + }, + + async agentFillForm(sessionId: string, fields: Array<{ selector: string; value: string }>) { + const session = await this.getAgentSession(sessionId); + const handle = handles.get(sessionId); + if (!handle?.page) { + throw new Error('Browser runtime handle is not available.'); + } + for (const field of fields) { + await handle.page.locator(field.selector).first().fill(field.value, { timeout: 10_000 }); + } + session.lastAction = 'fill_form'; + await captureSession(session, handle.page); + return publicSession(session); + }, + + async agentPressKey(sessionId: string, key: string) { + const session = await this.getAgentSession(sessionId); + const handle = handles.get(sessionId); + if (!handle?.page) { + throw new Error('Browser runtime handle is not available.'); + } + await handle.page.keyboard.press(key); + session.lastAction = `press_key:${key}`; + await captureSession(session, handle.page); + return publicSession(session); + }, + + async agentSelectOption(sessionId: string, selector: string, values: string[]) { + const session = await this.getAgentSession(sessionId); + const handle = handles.get(sessionId); + if (!handle?.page) { + throw new Error('Browser runtime handle is not available.'); + } + await handle.page.locator(selector).first().selectOption(values, { timeout: 10_000 }); + session.lastAction = 'select_option'; + await captureSession(session, handle.page); + return publicSession(session); + }, + + async agentWaitFor(sessionId: string, input: { text?: string; url?: string; timeoutMs?: number }) { + const session = await this.getAgentSession(sessionId); + const handle = handles.get(sessionId); + if (!handle?.page) { + throw new Error('Browser runtime handle is not available.'); + } + const timeout = Math.max(250, Math.min(input.timeoutMs || 5_000, 30_000)); + if (input.text) { + await handle.page.getByText(input.text, { exact: false }).first().waitFor({ timeout }); + } else if (input.url) { + await handle.page.waitForURL(input.url, { timeout }); + } else { + await handle.page.waitForTimeout(timeout); + } + session.lastAction = 'wait_for'; + await captureSession(session, handle.page); + return publicSession(session); + }, + + async agentTabs(sessionId: string, input: { action?: 'list' | 'new' | 'select' | 'close'; index?: number; url?: string }) { + const session = await this.getAgentSession(sessionId); + const handle = handles.get(sessionId); + if (!handle?.context || !handle?.page) { + throw new Error('Browser runtime handle is not available.'); + } + const action = input.action || 'list'; + if (action === 'new') { + const page = await handle.context.newPage(); + handles.set(sessionId, { ...handle, page }); + await attachRequestGuard(page); + if (input.url) { + await this.agentNavigate(sessionId, input.url); + } + } else if (action === 'select') { + const page = handle.context.pages()[input.index || 0]; + if (!page) { + throw new Error('Tab not found.'); + } + handles.set(sessionId, { ...handle, page }); + } else if (action === 'close') { + const pages = handle.context.pages(); + const page = pages[input.index ?? pages.indexOf(handle.page)]; + if (!page) { + throw new Error('Tab not found.'); + } + await page.close(); + handles.set(sessionId, { ...handle, page: handle.context.pages()[0] || await handle.context.newPage() }); + } + const updatedHandle = handles.get(sessionId); + await captureSession(session, updatedHandle?.page || handle.page); + return { + session: publicSession(session), + tabs: handle.context.pages().map((page: any, index: number) => ({ + index, + url: page.url(), + active: page === (updatedHandle?.page || handle.page), + })), + }; + }, + async stopSession(owner: BrowserUseOwner, sessionId: string) { const ownerId = getOwnerId(owner); const session = sessions.get(sessionId); - if (!session || session.ownerId !== ownerId) { + if (!session || (session.ownerId !== ownerId && session.ownerId !== AGENT_OWNER_ID && !session.agentAccessEnabled)) { return { stopped: false }; } @@ -497,6 +877,11 @@ export const browserUseService = { return { stopped: true, session: publicSession(session) }; }, + async agentStopSession(sessionId: string) { + await this.getAgentSession(sessionId); + return this.stopSession({ id: AGENT_OWNER_ID }, sessionId); + }, + async stopAllSessions() { await Promise.all([...sessions.keys()].map(async (sessionId) => { await closeHandle(sessionId); diff --git a/src/components/browser-use/view/BrowserUsePanel.tsx b/src/components/browser-use/view/BrowserUsePanel.tsx index 22d1153b..41e25e8e 100644 --- a/src/components/browser-use/view/BrowserUsePanel.tsx +++ b/src/components/browser-use/view/BrowserUsePanel.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; -import { Download, ExternalLink, Globe, Loader2, MonitorPlay, Navigation, Pause, RefreshCw, Square } from 'lucide-react'; +import { Bot, Download, ExternalLink, Globe, Loader2, MonitorPlay, Navigation, Pause, RefreshCw, Share2, Square, X } from 'lucide-react'; import { Badge, Button } from '../../../shared/view/ui'; import { authenticatedFetch } from '../../../utils/api'; @@ -12,6 +12,7 @@ type BrowserUseStatus = { chromiumInstalled: boolean; installInProgress: boolean; sessionCount: number; + agentToolsEnabled: boolean; mcpRecommended: boolean; message: string; }; @@ -27,6 +28,9 @@ type BrowserUseSession = { updatedAt: string; lastAction: string | null; message: string | null; + agentAccessEnabled: boolean; + createdBy: 'user' | 'agent'; + profileName: string | null; }; type BrowserUsePanelProps = { @@ -112,6 +116,18 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) { await readJson(response); }); + const grantAgentAccess = () => runAction(async () => { + if (!selectedSession) return; + const response = await authenticatedFetch(`/api/browser-use/sessions/${selectedSession.id}/agent-access/grant`, { method: 'POST' }); + await readJson(response); + }); + + const revokeAgentAccess = () => runAction(async () => { + if (!selectedSession) return; + const response = await authenticatedFetch(`/api/browser-use/sessions/${selectedSession.id}/agent-access/revoke`, { method: 'POST' }); + await readJson(response); + }); + const installRuntime = () => runAction(async () => { setIsInstalling(true); try { @@ -138,7 +154,7 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) { )}

- Managed Playwright browser sessions with owner-scoped screenshots and navigation. + Create browser sessions, watch agent activity, and decide which sessions agents may control.

@@ -159,6 +175,11 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) {
Runtime
{status?.available ? 'Available' : 'Setup required'}

{status?.message || 'Loading Browser Use status...'}

+ {status?.enabled && ( +
+ Agent tools: {status.agentToolsEnabled ? 'enabled' : 'disabled in settings'} +
+ )} {canInstallRuntime && (
+
+ {session.createdBy === 'agent' && ( + agent + )} + {session.agentAccessEnabled && ( + + shared + + )} + {session.profileName && ( + profile: {session.profileName} + )} +
{session.url || session.message || session.id}
))} @@ -215,6 +254,17 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) { Go + {selectedSession?.agentAccessEnabled ? ( + + ) : ( + + )} - )} -
+
+ )}
@@ -212,17 +331,11 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) { {session.status}
- {session.createdBy === 'agent' && ( - agent - )} {session.agentAccessEnabled && ( shared )} - {session.profileName && ( - profile: {session.profileName} - )}
{session.url || session.message || session.id}
@@ -258,14 +371,18 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) { Give Agent Access )} - - +
{error && ( @@ -286,29 +403,25 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) { )}
-
- {selectedSession?.screenshotDataUrl ? ( - Browser session screenshot - ) : ( -
- -
- {selectedSession?.message || 'Create a browser session to start.'} -
-

- Install the Browser Use runtime from this panel or enable it from Settings. -

-
- )} -
+ {renderBrowserSurface()}
+ {isFullscreen && selectedSession && ( +
+
+
+
{selectedSession.title || selectedSession.url || 'Browser session'}
+ +
+ {renderBrowserSurface(true)} +
+
+ )} ); } diff --git a/src/components/settings/view/tabs/browser-use-settings/BrowserUseSettingsTab.tsx b/src/components/settings/view/tabs/browser-use-settings/BrowserUseSettingsTab.tsx index 8a2a1ef8..d19f4593 100644 --- a/src/components/settings/view/tabs/browser-use-settings/BrowserUseSettingsTab.tsx +++ b/src/components/settings/view/tabs/browser-use-settings/BrowserUseSettingsTab.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from 'react'; -import { Download, Loader2, MonitorPlay, RefreshCw } from 'lucide-react'; +import { Download, ExternalLink, Loader2 } from 'lucide-react'; import { Button } from '../../../../../shared/view/ui'; import { authenticatedFetch } from '../../../../../utils/api'; @@ -77,7 +77,7 @@ export default function BrowserUseSettingsTab() { } }; - const installRuntime = async () => { + const installBrowserBinaries = async () => { setIsInstalling(true); setError(null); try { @@ -85,13 +85,13 @@ export default function BrowserUseSettingsTab() { await readJson(response); await loadState(); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to install Browser Use runtime'); + setError(err instanceof Error ? err.message : 'Failed to install browser binaries'); } finally { setIsInstalling(false); } }; - const needsRuntime = Boolean(settings.enabled && status && (!status.playwrightInstalled || !status.chromiumInstalled)); + const needsBrowserBinaries = Boolean(settings.enabled && status && (!status.playwrightInstalled || !status.chromiumInstalled)); return (
@@ -100,6 +100,24 @@ export default function BrowserUseSettingsTab() { description="Manage local Playwright browser sessions used for captured browser screenshots and guarded navigation." > +
+
+
How Browser Use Works
+

+ Learn what agents can do with browser sessions, when to share access, and what the current limitations are. +

+
+ + Open Guide + + +
+ -
-
-
-
- - Runtime -
-

- {status?.message || (isLoading ? 'Checking Browser Use runtime...' : 'Runtime status unavailable.')} -

- {status && ( -
- - Playwright: {status.playwrightInstalled ? 'installed' : 'missing'} - - - Chromium: {status.chromiumInstalled ? 'installed' : 'missing'} - + {(needsBrowserBinaries || error) && ( +
+ {needsBrowserBinaries && ( +
+
+
Browser binaries required
+

+ {status?.message || 'Install the browser binaries needed to create Browser Use sessions.'} +

+
+ + Playwright: {status?.playwrightInstalled ? 'installed' : 'missing'} + + + Chromium: {status?.chromiumInstalled ? 'installed' : 'missing'} + +
- )} -
-
- - {needsRuntime && ( - - )} -
-
+
+ )} - {error && ( -
- {error} -
- )} -
+ {error && ( +
+ {error} +
+ )} +
+ )}
From 56532af33a6624c1ebc580cc82ec44c78e540dea Mon Sep 17 00:00:00 2001 From: Simos Mikelatos Date: Mon, 15 Jun 2026 21:22:49 +0000 Subject: [PATCH 10/58] feat: add browser use guide links --- src/components/browser-use/view/BrowserUsePanel.tsx | 11 ++++++++++- .../browser-use-settings/BrowserUseSettingsTab.tsx | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/browser-use/view/BrowserUsePanel.tsx b/src/components/browser-use/view/BrowserUsePanel.tsx index 73cc41d9..e0e6311c 100644 --- a/src/components/browser-use/view/BrowserUsePanel.tsx +++ b/src/components/browser-use/view/BrowserUsePanel.tsx @@ -266,6 +266,15 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) {

+ + Guide + +
)} diff --git a/src/components/settings/view/tabs/browser-use-settings/BrowserUseSettingsTab.tsx b/src/components/settings/view/tabs/browser-use-settings/BrowserUseSettingsTab.tsx index d19f4593..c109918f 100644 --- a/src/components/settings/view/tabs/browser-use-settings/BrowserUseSettingsTab.tsx +++ b/src/components/settings/view/tabs/browser-use-settings/BrowserUseSettingsTab.tsx @@ -173,7 +173,7 @@ export default function BrowserUseSettingsTab() { ) : ( )} - Install Binaries + {isInstalling || status?.installInProgress ? 'Installing…' : 'Install Binaries'} )} From a0d56429a7624b453a94b8b9450644646737a9ff Mon Sep 17 00:00:00 2001 From: Simos Mikelatos Date: Wed, 17 Jun 2026 15:35:55 +0000 Subject: [PATCH 11/58] fix browser use --- package-lock.json | 26 ++++++++++++ package.json | 3 +- server/browser-use-mcp.ts | 10 ++++- server/cli.js | 13 +++--- server/index.js | 12 +++++- .../browser-use/browser-use-mcp.routes.ts | 4 +- .../modules/browser-use/browser-use.routes.ts | 16 +++++--- .../browser-use/browser-use.service.ts | 41 +++++++++++++++---- 8 files changed, 100 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3faa74aa..28bdd8a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,6 +88,7 @@ "auto-changelog": "^2.5.0", "autoprefixer": "^10.4.16", "concurrently": "^8.2.2", + "cross-env": "^10.1.0", "electron": "^38.0.0", "electron-builder": "^26.15.3", "eslint": "^9.39.3", @@ -1748,6 +1749,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", @@ -8694,6 +8702,24 @@ "optional": true, "peer": true }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", diff --git a/package.json b/package.json index ae75f9c6..f378f79b 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "server:dev-watch": "tsx watch --tsconfig server/tsconfig.json server/index.js", "client": "vite", "desktop": "electron electron/main.js", - "desktop:dev": "ELECTRON_DEV_URL=http://127.0.0.1:5173 electron electron/main.js", + "desktop:dev": "cross-env ELECTRON_DEV_URL=http://127.0.0.1:5173 electron electron/main.js", "desktop:pack": "npm run build && electron-builder --dir", "desktop:dist:mac": "npm run build && electron-builder --mac dmg zip", "build": "npm run build:client && npm run build:server", @@ -194,6 +194,7 @@ "auto-changelog": "^2.5.0", "autoprefixer": "^10.4.16", "concurrently": "^8.2.2", + "cross-env": "^10.1.0", "electron": "^38.0.0", "electron-builder": "^26.15.3", "eslint": "^9.39.3", diff --git a/server/browser-use-mcp.ts b/server/browser-use-mcp.ts index 22a4c3e4..55c448ad 100644 --- a/server/browser-use-mcp.ts +++ b/server/browser-use-mcp.ts @@ -35,6 +35,7 @@ const readNumber = (value: unknown): number | undefined => const apiUrl = (process.env.CLOUDCLI_BROWSER_USE_API_URL || 'http://127.0.0.1:3001/api/browser-use-mcp').replace(/\/$/, ''); const apiToken = process.env.CLOUDCLI_BROWSER_USE_MCP_TOKEN || ''; +const API_TIMEOUT_MS = Number.parseInt(process.env.CLOUDCLI_BROWSER_USE_API_TIMEOUT_MS || '60000', 10); async function callBrowserUseApi(toolName: string, input: Record) { if (!apiToken) { @@ -48,6 +49,7 @@ async function callBrowserUseApi(toolName: string, input: Record { buffer = buffer.slice(messageEnd); void (async () => { - const request = JSON.parse(rawMessage) as JsonRpcRequest; + let request: JsonRpcRequest; + try { + request = JSON.parse(rawMessage) as JsonRpcRequest; + } catch (error) { + sendError(null, error); + return; + } try { const result = await handleMessage(request); sendResult(request.id, result); diff --git a/server/cli.js b/server/cli.js index e6daacc4..83c04411 100755 --- a/server/cli.js +++ b/server/cli.js @@ -155,12 +155,13 @@ Usage: cloudcli [command] [options] Commands: - start Start the CloudCLI server (default) - sandbox Manage Docker sandbox environments - status Show configuration and data locations - update Update to the latest version - help Show this help information - version Show version information + start Start the CloudCLI server (default) + sandbox Manage Docker sandbox environments + browser-use-mcp Run the Browser Use MCP stdio server + status Show configuration and data locations + update Update to the latest version + help Show this help information + version Show version information Options: -p, --port Set server port (default: 3001) diff --git a/server/index.js b/server/index.js index 3fcd438a..ce35c542 100755 --- a/server/index.js +++ b/server/index.js @@ -1714,8 +1714,16 @@ async function startServer() { await closeSessionsWatcher(); // Clean up plugin processes on shutdown const shutdownRuntimeServices = async () => { - await browserUseService.stopAllSessions(); - await stopAllPlugins(); + try { + await browserUseService.stopAllSessions(); + } catch (err) { + console.error('[Browser Use] Error stopping sessions during shutdown:', err?.message || err); + } + try { + await stopAllPlugins(); + } catch (err) { + console.error('[Plugins] Error stopping plugins during shutdown:', err?.message || err); + } process.exit(0); }; process.on('SIGTERM', () => void shutdownRuntimeServices()); diff --git a/server/modules/browser-use/browser-use-mcp.routes.ts b/server/modules/browser-use/browser-use-mcp.routes.ts index 335ffa18..2899fd74 100644 --- a/server/modules/browser-use/browser-use-mcp.routes.ts +++ b/server/modules/browser-use/browser-use-mcp.routes.ts @@ -8,8 +8,8 @@ function readBearerToken(header: unknown): string | null { if (typeof header !== 'string') { return null; } - const match = /^Bearer\s+(.+)$/i.exec(header.trim()); - return match?.[1] || null; + const match = /^Bearer\s+(\S.*)$/i.exec(header.trim()); + return match?.[1]?.trim() || null; } router.use((req, res, next) => { diff --git a/server/modules/browser-use/browser-use.routes.ts b/server/modules/browser-use/browser-use.routes.ts index 16f65d7e..167912cd 100644 --- a/server/modules/browser-use/browser-use.routes.ts +++ b/server/modules/browser-use/browser-use.routes.ts @@ -121,10 +121,12 @@ router.post('/sessions/:sessionId/navigate', async (req: AuthenticatedRequest, r router.post('/sessions/:sessionId/click', async (req: AuthenticatedRequest, res) => { try { - const session = await browserUseService.userClick(requireUser(req), readParam(req.params.sessionId), { - x: Number(req.body?.x), - y: Number(req.body?.y), - }); + const x = Number(req.body?.x); + const y = Number(req.body?.y); + if (!Number.isFinite(x) || !Number.isFinite(y)) { + throw new Error('Click requires numeric x and y coordinates.'); + } + const session = await browserUseService.userClick(requireUser(req), readParam(req.params.sessionId), { x, y }); res.json({ success: true, data: { session } }); } catch (error) { res.status(400).json({ @@ -136,7 +138,11 @@ router.post('/sessions/:sessionId/click', async (req: AuthenticatedRequest, res) router.post('/sessions/:sessionId/press-key', async (req: AuthenticatedRequest, res) => { try { - const session = await browserUseService.userPressKey(requireUser(req), readParam(req.params.sessionId), String(req.body?.key || '')); + const key = String(req.body?.key || '').trim(); + if (!key) { + throw new Error('A key is required.'); + } + const session = await browserUseService.userPressKey(requireUser(req), readParam(req.params.sessionId), key); res.json({ success: true, data: { session } }); } catch (error) { res.status(400).json({ diff --git a/server/modules/browser-use/browser-use.service.ts b/server/modules/browser-use/browser-use.service.ts index 06fe255b..7906b49e 100644 --- a/server/modules/browser-use/browser-use.service.ts +++ b/server/modules/browser-use/browser-use.service.ts @@ -7,7 +7,7 @@ import os from 'node:os'; import net from 'node:net'; import path from 'node:path'; -import { appConfigDb } from '@/modules/database/repositories/app-config.js'; +import { appConfigDb } from '@/modules/database/index.js'; import { providerMcpService } from '@/modules/providers/services/mcp.service.js'; import { getModuleDir } from '@/utils/runtime-paths.js'; @@ -220,6 +220,11 @@ function getRuntimeReadiness(): RuntimeReadiness { return readiness; } +const INSTALL_COMMAND_TIMEOUT_MS = Number.parseInt( + process.env.CLOUDCLI_BROWSER_USE_INSTALL_TIMEOUT_MS || String(10 * 60 * 1000), + 10, +); + function runCommand(command: string, args: string[]): Promise { return new Promise((resolve, reject) => { const child = spawn(command, args, { @@ -229,18 +234,36 @@ function runCommand(command: string, args: string[]): Promise { stdio: ['ignore', 'pipe', 'pipe'], }); const output: string[] = []; + let settled = false; + const finish = (fn: () => void) => { + if (settled) { + return; + } + settled = true; + clearTimeout(timer); + fn(); + }; + + // Guard against a stuck npm/playwright process hanging the install request forever. + const timer = setTimeout(() => { + child.kill('SIGKILL'); + finish(() => reject(new Error( + `${command} ${args.join(' ')} timed out after ${INSTALL_COMMAND_TIMEOUT_MS}ms.`, + ))); + }, INSTALL_COMMAND_TIMEOUT_MS); + timer.unref?.(); child.stdout.on('data', (chunk) => output.push(String(chunk))); child.stderr.on('data', (chunk) => output.push(String(chunk))); - child.on('error', reject); - child.on('close', (code) => { + child.on('error', (error) => finish(() => reject(error))); + child.on('close', (code) => finish(() => { if (code === 0) { resolve(); return; } reject(new Error(output.join('').trim() || `${command} ${args.join(' ')} exited with code ${code}`)); - }); + })); }); } @@ -386,8 +409,10 @@ async function assertAllowedBrowserRequest(rawUrl: string): Promise { await assertPublicHttpTarget(parsed); } -async function attachRequestGuard(page: any): Promise { - await page.route('**/*', async (route: any) => { +async function attachRequestGuard(context: any): Promise { + // Attach at the context level so the guard also covers popups, window.open targets, + // and any replacement pages created during the session lifecycle. + await context.route('**/*', async (route: any) => { try { await assertAllowedBrowserRequest(route.request().url()); await route.continue(); @@ -637,7 +662,7 @@ export const browserUseService = { context = await browser.newContext(contextOptions); page = await context.newPage(); } - await attachRequestGuard(page); + await attachRequestGuard(context); session.status = 'ready'; session.message = 'Browser session is ready.'; sessions.set(session.id, session); @@ -886,7 +911,7 @@ export const browserUseService = { if (action === 'new') { const page = await handle.context.newPage(); handles.set(sessionId, { ...handle, page }); - await attachRequestGuard(page); + // Request guard is attached at the context level, so new pages are already covered. if (input.url) { await this.agentNavigate(sessionId, input.url); } From 086df034b4aebbff2b1fba2057d98e03fa5d8c1a Mon Sep 17 00:00:00 2001 From: Simos Mikelatos Date: Wed, 17 Jun 2026 17:04:11 +0000 Subject: [PATCH 12/58] feat(browser-use): simplify agent session monitoring --- .../modules/browser-use/browser-use.routes.ts | 119 +---- .../browser-use/browser-use.service.ts | 194 ++------ .../tests/browser-use.service.test.ts | 15 +- server/modules/providers/index.ts | 1 + .../browser-use/view/BrowserUsePanel.tsx | 424 +++++++++--------- .../main-content/view/MainContent.tsx | 2 +- .../BrowserUseSettingsTab.tsx | 128 ++---- 7 files changed, 301 insertions(+), 582 deletions(-) diff --git a/server/modules/browser-use/browser-use.routes.ts b/server/modules/browser-use/browser-use.routes.ts index 167912cd..6eff6af6 100644 --- a/server/modules/browser-use/browser-use.routes.ts +++ b/server/modules/browser-use/browser-use.routes.ts @@ -4,20 +4,6 @@ import { browserUseService } from '@/modules/browser-use/browser-use.service.js' const router = express.Router(); -type AuthenticatedRequest = express.Request & { - user?: { - id?: string | number; - }; -}; - -function requireUser(req: AuthenticatedRequest): { id: string | number } { - const userId = req.user?.id; - if (userId === undefined || userId === null) { - throw new Error('Authenticated user is required.'); - } - return { id: userId }; -} - function readParam(value: string | string[] | undefined): string { return Array.isArray(value) ? value[0] || '' : value || ''; } @@ -56,18 +42,6 @@ router.put('/settings', async (req, res) => { } }); -router.post('/agent-tools/register', async (_req, res) => { - try { - const result = await browserUseService.registerAgentMcp(); - res.status(201).json({ success: true, data: result }); - } catch (error) { - res.status(400).json({ - success: false, - error: error instanceof Error ? error.message : 'Failed to register Browser Use MCP.', - }); - } -}); - router.post('/runtime/install', async (_req, res) => { try { const result = await browserUseService.installRuntime(); @@ -84,9 +58,9 @@ router.post('/runtime/install', async (_req, res) => { } }); -router.get('/sessions', async (req: AuthenticatedRequest, res) => { +router.get('/sessions', async (_req, res) => { try { - res.json({ success: true, data: { sessions: await browserUseService.listSessions(requireUser(req)) } }); + res.json({ success: true, data: { sessions: await browserUseService.listSessions() } }); } catch (error) { res.status(401).json({ success: false, @@ -95,90 +69,9 @@ router.get('/sessions', async (req: AuthenticatedRequest, res) => { } }); -router.post('/sessions', async (req: AuthenticatedRequest, res) => { +router.post('/sessions/:sessionId/stop', async (req, res) => { try { - const session = await browserUseService.createSession(requireUser(req)); - res.status(session.status === 'unavailable' ? 202 : 201).json({ success: true, data: { session } }); - } catch (error) { - res.status(400).json({ - success: false, - error: error instanceof Error ? error.message : 'Failed to create browser session.', - }); - } -}); - -router.post('/sessions/:sessionId/navigate', async (req: AuthenticatedRequest, res) => { - try { - const session = await browserUseService.navigate(requireUser(req), readParam(req.params.sessionId), String(req.body?.url || '')); - res.json({ success: true, data: { session } }); - } catch (error) { - res.status(400).json({ - success: false, - error: error instanceof Error ? error.message : 'Failed to navigate browser session.', - }); - } -}); - -router.post('/sessions/:sessionId/click', async (req: AuthenticatedRequest, res) => { - try { - const x = Number(req.body?.x); - const y = Number(req.body?.y); - if (!Number.isFinite(x) || !Number.isFinite(y)) { - throw new Error('Click requires numeric x and y coordinates.'); - } - const session = await browserUseService.userClick(requireUser(req), readParam(req.params.sessionId), { x, y }); - res.json({ success: true, data: { session } }); - } catch (error) { - res.status(400).json({ - success: false, - error: error instanceof Error ? error.message : 'Failed to click browser session.', - }); - } -}); - -router.post('/sessions/:sessionId/press-key', async (req: AuthenticatedRequest, res) => { - try { - const key = String(req.body?.key || '').trim(); - if (!key) { - throw new Error('A key is required.'); - } - const session = await browserUseService.userPressKey(requireUser(req), readParam(req.params.sessionId), key); - res.json({ success: true, data: { session } }); - } catch (error) { - res.status(400).json({ - success: false, - error: error instanceof Error ? error.message : 'Failed to send browser key input.', - }); - } -}); - -router.post('/sessions/:sessionId/agent-access/grant', async (req: AuthenticatedRequest, res) => { - try { - const session = await browserUseService.grantAgentAccess(requireUser(req), readParam(req.params.sessionId)); - res.json({ success: true, data: { session } }); - } catch (error) { - res.status(400).json({ - success: false, - error: error instanceof Error ? error.message : 'Failed to grant agent access.', - }); - } -}); - -router.post('/sessions/:sessionId/agent-access/revoke', async (req: AuthenticatedRequest, res) => { - try { - const session = await browserUseService.revokeAgentAccess(requireUser(req), readParam(req.params.sessionId)); - res.json({ success: true, data: { session } }); - } catch (error) { - res.status(400).json({ - success: false, - error: error instanceof Error ? error.message : 'Failed to revoke agent access.', - }); - } -}); - -router.post('/sessions/:sessionId/stop', async (req: AuthenticatedRequest, res) => { - try { - const result = await browserUseService.stopSession(requireUser(req), readParam(req.params.sessionId)); + const result = await browserUseService.stopSession(readParam(req.params.sessionId)); res.json({ success: true, data: result }); } catch (error) { res.status(400).json({ @@ -188,9 +81,9 @@ router.post('/sessions/:sessionId/stop', async (req: AuthenticatedRequest, res) } }); -router.delete('/sessions/:sessionId', async (req: AuthenticatedRequest, res) => { +router.delete('/sessions/:sessionId', async (req, res) => { try { - const result = await browserUseService.deleteSession(requireUser(req), readParam(req.params.sessionId)); + const result = await browserUseService.deleteSession(readParam(req.params.sessionId)); res.json({ success: true, data: result }); } catch (error) { res.status(400).json({ diff --git a/server/modules/browser-use/browser-use.service.ts b/server/modules/browser-use/browser-use.service.ts index 7906b49e..5d14f17c 100644 --- a/server/modules/browser-use/browser-use.service.ts +++ b/server/modules/browser-use/browser-use.service.ts @@ -8,7 +8,7 @@ import net from 'node:net'; import path from 'node:path'; import { appConfigDb } from '@/modules/database/index.js'; -import { providerMcpService } from '@/modules/providers/services/mcp.service.js'; +import { providerMcpService } from '@/modules/providers/index.js'; import { getModuleDir } from '@/utils/runtime-paths.js'; const require = createRequire(import.meta.url); @@ -26,7 +26,7 @@ type BrowserUseSessionStatus = 'ready' | 'stopped' | 'unavailable'; type BrowserUseSession = { id: string; ownerId: string; - createdBy: 'user' | 'agent'; + createdBy: 'agent'; runtime: BrowserUseRuntime; status: BrowserUseSessionStatus; url: string | null; @@ -36,7 +36,6 @@ type BrowserUseSession = { updatedAt: string; lastAction: string | null; message: string | null; - agentAccessEnabled: boolean; profileName: string | null; viewport: { width: number; @@ -45,7 +44,7 @@ type BrowserUseSession = { cursor: { x: number; y: number; - actor: 'agent' | 'user'; + actor: 'agent'; } | null; }; @@ -57,13 +56,8 @@ type RuntimeHandle = { page?: any; }; -type BrowserUseOwner = { - id: string | number; -}; - type BrowserUseSettings = { enabled: boolean; - agentToolsEnabled: boolean; }; type RuntimeReadiness = { @@ -82,7 +76,6 @@ let lastInstallMessage: string | null = null; const DEFAULT_SETTINGS: BrowserUseSettings = { enabled: false, - agentToolsEnabled: false, }; const AGENT_OWNER_ID = 'agent'; const PROFILE_ROOT = path.join(os.homedir(), '.cloudcli', 'browser-use', 'profiles'); @@ -103,7 +96,6 @@ function readSettings(): BrowserUseSettings { const parsed = JSON.parse(raw) as Partial; return { enabled: parsed.enabled === true, - agentToolsEnabled: parsed.agentToolsEnabled === true, }; } catch (error: any) { console.warn('[Browser Use] Failed to read settings:', error?.message || error); @@ -114,7 +106,6 @@ function readSettings(): BrowserUseSettings { function writeSettings(settings: BrowserUseSettings): BrowserUseSettings { const normalized = { enabled: settings.enabled === true, - agentToolsEnabled: settings.agentToolsEnabled === true, }; appConfigDb.set(BROWSER_USE_SETTINGS_KEY, JSON.stringify(normalized)); @@ -244,7 +235,6 @@ function runCommand(command: string, args: string[]): Promise { fn(); }; - // Guard against a stuck npm/playwright process hanging the install request forever. const timer = setTimeout(() => { child.kill('SIGKILL'); finish(() => reject(new Error( @@ -309,14 +299,6 @@ async function installRuntime(): Promise<{ success: boolean; message: string }> } } -function getOwnerId(owner: BrowserUseOwner): string { - if (owner.id === undefined || owner.id === null || String(owner.id).trim() === '') { - throw new Error('Authenticated user is required.'); - } - - return String(owner.id); -} - function isPrivateIpv4(address: string): boolean { const parts = address.split('.').map((part) => Number.parseInt(part, 10)); if (parts.length !== 4 || parts.some((part) => Number.isNaN(part) || part < 0 || part > 255)) { @@ -410,8 +392,6 @@ async function assertAllowedBrowserRequest(rawUrl: string): Promise { } async function attachRequestGuard(context: any): Promise { - // Attach at the context level so the guard also covers popups, window.open targets, - // and any replacement pages created during the session lifecycle. await context.route('**/*', async (route: any) => { try { await assertAllowedBrowserRequest(route.request().url()); @@ -431,10 +411,6 @@ function ownerSessions(ownerId: string): BrowserUseSession[] { return [...sessions.values()].filter((session) => session.ownerId === ownerId); } -function canAccessSession(ownerId: string, session: BrowserUseSession): boolean { - return session.ownerId === ownerId || session.ownerId === AGENT_OWNER_ID || session.agentAccessEnabled; -} - async function closeHandle(sessionId: string): Promise { const handle = handles.get(sessionId); handles.delete(sessionId); @@ -504,21 +480,15 @@ export const browserUseService = { async updateSettings(settings: Partial) { const current = readSettings(); const nextSettings = { - ...current, enabled: typeof settings.enabled === 'boolean' ? settings.enabled : current.enabled, - agentToolsEnabled: typeof settings.agentToolsEnabled === 'boolean' - ? settings.agentToolsEnabled - : current.agentToolsEnabled, }; - if (!nextSettings.enabled) { - nextSettings.agentToolsEnabled = false; - } const next = writeSettings(nextSettings); - if (next.agentToolsEnabled) { + if (next.enabled) { await this.registerAgentMcp(); - } else if (current.agentToolsEnabled) { + } else if (current.enabled) { await this.unregisterAgentMcp(); + await this.stopAllSessions(); } return next; }, @@ -536,8 +506,6 @@ export const browserUseService = { chromiumInstalled: readiness.chromiumInstalled, installInProgress: readiness.installInProgress, sessionCount: sessions.size, - agentToolsEnabled: settings.agentToolsEnabled, - mcpRecommended: !settings.agentToolsEnabled, message: available ? 'Browser Use runtime is available.' : getSetupMessage(settings, readiness), @@ -591,25 +559,27 @@ export const browserUseService = { }; }, - async listSessions(owner: BrowserUseOwner) { - const ownerId = getOwnerId(owner); + async listSessions() { await expireStaleSessions(); return [...sessions.values()] - .filter((session) => canAccessSession(ownerId, session)) + .filter((session) => session.ownerId === AGENT_OWNER_ID) .map(publicSession); }, - async createSession(owner: BrowserUseOwner, options?: { createdBy?: 'user' | 'agent'; profileName?: string | null; agentAccessEnabled?: boolean }) { - const ownerId = getOwnerId(owner); + async createAgentSession(options?: { profileName?: string | null }) { + const settings = readSettings(); + if (!settings.enabled) { + throw new Error('Browser Use agent tools are disabled.'); + } + await expireStaleSessions(); - const createdBy = options?.createdBy ?? 'user'; const profileName = normalizeProfileName(options?.profileName); const now = new Date().toISOString(); const session: BrowserUseSession = { id: randomUUID(), - ownerId, - createdBy, + ownerId: AGENT_OWNER_ID, + createdBy: 'agent', runtime: getRuntime(), status: 'unavailable', url: null, @@ -619,18 +589,16 @@ export const browserUseService = { updatedAt: now, lastAction: 'create', message: null, - agentAccessEnabled: options?.agentAccessEnabled ?? createdBy === 'agent', profileName, viewport: { width: 1440, height: 900 }, cursor: null, }; - const activeOwnerSessions = ownerSessions(ownerId).filter((item) => item.status === 'ready'); + const activeOwnerSessions = ownerSessions(AGENT_OWNER_ID).filter((item) => item.status === 'ready'); if (activeOwnerSessions.length >= MAX_SESSIONS_PER_OWNER) { - throw new Error(`Browser Use is limited to ${MAX_SESSIONS_PER_OWNER} active sessions per user.`); + throw new Error(`Browser Use is limited to ${MAX_SESSIONS_PER_OWNER} active agent sessions.`); } - const settings = readSettings(); const readiness = getRuntimeReadiness(); if (!settings.enabled || !readiness.playwrightInstalled || !readiness.chromiumInstalled || !readiness.playwright) { session.message = getSetupMessage(settings, readiness); @@ -671,70 +639,35 @@ export const browserUseService = { return publicSession(session); }, - async grantAgentAccess(owner: BrowserUseOwner, sessionId: string) { - const ownerId = getOwnerId(owner); - const session = sessions.get(sessionId); - if (!session || (session.ownerId !== ownerId && session.ownerId !== AGENT_OWNER_ID)) { - throw new Error('Browser session not found.'); - } - session.agentAccessEnabled = true; - session.updatedAt = new Date().toISOString(); - session.lastAction = 'agent_access:grant'; - return publicSession(session); - }, - - async revokeAgentAccess(owner: BrowserUseOwner, sessionId: string) { - const ownerId = getOwnerId(owner); - const session = sessions.get(sessionId); - if (!session || (session.ownerId !== ownerId && session.ownerId !== AGENT_OWNER_ID)) { - throw new Error('Browser session not found.'); - } - session.agentAccessEnabled = false; - session.updatedAt = new Date().toISOString(); - session.lastAction = 'agent_access:revoke'; - return publicSession(session); - }, - async listAgentSessions() { const settings = readSettings(); - if (!settings.enabled || !settings.agentToolsEnabled) { + if (!settings.enabled) { return []; } await expireStaleSessions(); return [...sessions.values()] - .filter((session) => session.agentAccessEnabled || session.ownerId === AGENT_OWNER_ID) + .filter((session) => session.ownerId === AGENT_OWNER_ID) .map(publicSession); }, - async createAgentSession(options?: { profileName?: string | null }) { - const settings = readSettings(); - if (!settings.enabled || !settings.agentToolsEnabled) { - throw new Error('Browser Use agent tools are disabled.'); - } - return this.createSession( - { id: AGENT_OWNER_ID }, - { createdBy: 'agent', profileName: options?.profileName, agentAccessEnabled: true }, - ); - }, - async getAgentSession(sessionId: string) { const settings = readSettings(); - if (!settings.enabled || !settings.agentToolsEnabled) { + if (!settings.enabled) { throw new Error('Browser Use agent tools are disabled.'); } const session = sessions.get(sessionId); - if (!session || (!session.agentAccessEnabled && session.ownerId !== AGENT_OWNER_ID)) { - throw new Error('Browser session is not shared with agents.'); + if (!session || session.ownerId !== AGENT_OWNER_ID) { + throw new Error('Browser session not found.'); } return session; }, - async navigate(owner: BrowserUseOwner, sessionId: string, rawUrl: string) { - const ownerId = getOwnerId(owner); + async agentNavigate(sessionId: string, rawUrl: string) { + await this.getAgentSession(sessionId); await expireStaleSessions(); const session = sessions.get(sessionId); - if (!session || !canAccessSession(ownerId, session)) { + if (!session || session.ownerId !== AGENT_OWNER_ID) { throw new Error('Browser session not found.'); } @@ -755,25 +688,6 @@ export const browserUseService = { return publicSession(session); }, - async agentNavigate(sessionId: string, rawUrl: string) { - await this.getAgentSession(sessionId); - return this.navigate({ id: AGENT_OWNER_ID }, sessionId, rawUrl).catch(async (error) => { - const session = await this.getAgentSession(sessionId); - if (session.ownerId !== AGENT_OWNER_ID) { - const url = await normalizeUrl(rawUrl); - const handle = handles.get(sessionId); - if (!handle?.page) { - throw new Error('Browser runtime handle is not available.'); - } - await handle.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 }); - session.lastAction = `navigate:${url}`; - await captureSession(session, handle.page); - return publicSession(session); - } - throw error; - }); - }, - async agentSnapshot(sessionId: string) { const session = await this.getAgentSession(sessionId); const handle = handles.get(sessionId); @@ -911,7 +825,6 @@ export const browserUseService = { if (action === 'new') { const page = await handle.context.newPage(); handles.set(sessionId, { ...handle, page }); - // Request guard is attached at the context level, so new pages are already covered. if (input.url) { await this.agentNavigate(sessionId, input.url); } @@ -942,10 +855,9 @@ export const browserUseService = { }; }, - async stopSession(owner: BrowserUseOwner, sessionId: string) { - const ownerId = getOwnerId(owner); + async stopSession(sessionId: string) { const session = sessions.get(sessionId); - if (!session || !canAccessSession(ownerId, session)) { + if (!session || session.ownerId !== AGENT_OWNER_ID) { return { stopped: false }; } @@ -958,10 +870,9 @@ export const browserUseService = { return { stopped: true, session: publicSession(session) }; }, - async deleteSession(owner: BrowserUseOwner, sessionId: string) { - const ownerId = getOwnerId(owner); + async deleteSession(sessionId: string) { const session = sessions.get(sessionId); - if (!session || !canAccessSession(ownerId, session)) { + if (!session || session.ownerId !== AGENT_OWNER_ID) { return { deleted: false }; } @@ -970,52 +881,9 @@ export const browserUseService = { return { deleted: true, sessionId }; }, - async userClick(owner: BrowserUseOwner, sessionId: string, input: { x: number; y: number }) { - const ownerId = getOwnerId(owner); - const session = sessions.get(sessionId); - if (!session || !canAccessSession(ownerId, session)) { - throw new Error('Browser session not found.'); - } - if (session.status !== 'ready') { - throw new Error(session.message || 'Browser session is not available.'); - } - - const handle = handles.get(sessionId); - if (!handle?.page) { - throw new Error('Browser runtime handle is not available.'); - } - - await handle.page.mouse.click(input.x, input.y); - session.lastAction = 'click'; - session.cursor = { x: input.x, y: input.y, actor: 'user' }; - await captureSession(session, handle.page); - return publicSession(session); - }, - - async userPressKey(owner: BrowserUseOwner, sessionId: string, key: string) { - const ownerId = getOwnerId(owner); - const session = sessions.get(sessionId); - if (!session || !canAccessSession(ownerId, session)) { - throw new Error('Browser session not found.'); - } - if (session.status !== 'ready') { - throw new Error(session.message || 'Browser session is not available.'); - } - - const handle = handles.get(sessionId); - if (!handle?.page) { - throw new Error('Browser runtime handle is not available.'); - } - - await handle.page.keyboard.press(key); - session.lastAction = `press_key:${key}`; - await captureSession(session, handle.page); - return publicSession(session); - }, - async agentStopSession(sessionId: string) { await this.getAgentSession(sessionId); - return this.stopSession({ id: AGENT_OWNER_ID }, sessionId); + return this.stopSession(sessionId); }, async stopAllSessions() { diff --git a/server/modules/browser-use/tests/browser-use.service.test.ts b/server/modules/browser-use/tests/browser-use.service.test.ts index 162e9439..3aefcd2d 100644 --- a/server/modules/browser-use/tests/browser-use.service.test.ts +++ b/server/modules/browser-use/tests/browser-use.service.test.ts @@ -14,17 +14,8 @@ test('browser use blocks private and local network addresses by default', () => assert.equal(isBlockedBrowserUseAddress('2001:4860:4860::8888'), false); }); -test('browser use sessions are listed only for their owner', async () => { - const ownerA = { id: `owner-a-${Date.now()}-${Math.random()}` }; - const ownerB = { id: `owner-b-${Date.now()}-${Math.random()}` }; +test('browser use monitor list starts empty without agent sessions', async () => { + const sessions = await browserUseService.listSessions(); - const ownerASession = await browserUseService.createSession(ownerA); - await browserUseService.createSession(ownerB); - - const ownerASessions = await browserUseService.listSessions(ownerA); - const ownerBSessions = await browserUseService.listSessions(ownerB); - - assert.equal(ownerASessions.some((session) => session.id === ownerASession.id), true); - assert.equal(ownerBSessions.some((session) => session.id === ownerASession.id), false); - assert.equal(Object.hasOwn(ownerASession, 'ownerId'), false); + assert.deepEqual(sessions, []); }); diff --git a/server/modules/providers/index.ts b/server/modules/providers/index.ts index 0d8d8edd..e057e200 100644 --- a/server/modules/providers/index.ts +++ b/server/modules/providers/index.ts @@ -1,5 +1,6 @@ export { sessionSynchronizerService } from './services/session-synchronizer.service.js'; export { providerSkillsService } from './services/skills.service.js'; +export { providerMcpService } from './services/mcp.service.js'; export { initializeSessionsWatcher } from './services/sessions-watcher.service.js'; export { closeSessionsWatcher } from './services/sessions-watcher.service.js'; diff --git a/src/components/browser-use/view/BrowserUsePanel.tsx b/src/components/browser-use/view/BrowserUsePanel.tsx index e0e6311c..5a7825ba 100644 --- a/src/components/browser-use/view/BrowserUsePanel.tsx +++ b/src/components/browser-use/view/BrowserUsePanel.tsx @@ -1,5 +1,5 @@ -import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent, type MouseEvent } from 'react'; -import { Bot, Download, Expand, ExternalLink, Globe, Loader2, MonitorPlay, Navigation, RefreshCw, Share2, Square, Trash2, X } from 'lucide-react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { Bot, Clock3, Download, Expand, ExternalLink, Loader2, MonitorPlay, RefreshCw, Settings, Square, Trash2, X } from 'lucide-react'; import { Badge, Button } from '../../../shared/view/ui'; import { authenticatedFetch } from '../../../utils/api'; @@ -11,8 +11,6 @@ type BrowserUseStatus = { chromiumInstalled: boolean; installInProgress: boolean; sessionCount: number; - agentToolsEnabled: boolean; - mcpRecommended: boolean; message: string; }; @@ -26,8 +24,7 @@ type BrowserUseSession = { updatedAt: string; lastAction: string | null; message: string | null; - agentAccessEnabled: boolean; - createdBy: 'user' | 'agent'; + createdBy: 'agent'; profileName: string | null; viewport: { width: number; @@ -36,12 +33,13 @@ type BrowserUseSession = { cursor: { x: number; y: number; - actor: 'agent' | 'user'; + actor: 'agent'; } | null; }; type BrowserUsePanelProps = { isVisible: boolean; + onShowSettings?: () => void; }; async function readJson(response: Response): Promise { @@ -52,16 +50,69 @@ async function readJson(response: Response): Promise { return data as T; } -export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) { +function formatRelativeTime(value: string | null): string { + if (!value) { + return 'Never'; + } + + const timestamp = Date.parse(value); + if (!Number.isFinite(timestamp)) { + return 'Unknown'; + } + + const elapsedSeconds = Math.max(0, Math.round((Date.now() - timestamp) / 1000)); + if (elapsedSeconds < 10) return 'Just now'; + if (elapsedSeconds < 60) return `${elapsedSeconds}s ago`; + const elapsedMinutes = Math.round(elapsedSeconds / 60); + if (elapsedMinutes < 60) return `${elapsedMinutes}m ago`; + const elapsedHours = Math.round(elapsedMinutes / 60); + if (elapsedHours < 24) return `${elapsedHours}h ago`; + return `${Math.round(elapsedHours / 24)}d ago`; +} + +function getDomain(url: string | null): string { + if (!url) { + return 'No page loaded'; + } + + try { + return new URL(url).hostname; + } catch { + return url; + } +} + +function formatAction(action: string | null): string { + if (!action) { + return 'Waiting'; + } + return action.replace(/_/g, ' ').replace(/:/g, ': '); +} + +function getStatusTone(status: BrowserUseSession['status']): string { + if (status === 'ready') { + return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'; + } + if (status === 'stopped') { + return 'border-border bg-muted text-muted-foreground'; + } + return 'border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300'; +} + +const PROMPTS = [ + 'Use Browser Use to open the staging checkout flow, try the main path, and summarize anything that looks broken.', + 'Use Browser Use to inspect the page at , capture what changed after each click, and report UI issues with screenshots.', +]; + +export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUsePanelProps) { const [status, setStatus] = useState(null); const [sessions, setSessions] = useState([]); const [selectedSessionId, setSelectedSessionId] = useState(null); - const [targetUrl, setTargetUrl] = useState('https://example.com'); + const [isRefreshing, setIsRefreshing] = useState(false); const [isBusy, setIsBusy] = useState(false); const [isInstalling, setIsInstalling] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); const [error, setError] = useState(null); - const viewerRef = useRef(null); const selectedSession = useMemo( () => sessions.find((session) => session.id === selectedSessionId) || sessions[0] || null, @@ -69,31 +120,35 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) { ); const refresh = useCallback(async () => { - const [statusResponse, sessionsResponse] = await Promise.all([ - authenticatedFetch('/api/browser-use/status'), - authenticatedFetch('/api/browser-use/sessions'), - ]); - const statusData = await readJson<{ data: BrowserUseStatus }>(statusResponse); - const sessionsData = await readJson<{ data: { sessions: BrowserUseSession[] } }>(sessionsResponse); - setStatus(statusData.data); - setSessions(sessionsData.data.sessions); - setSelectedSessionId((current) => ( - current && sessionsData.data.sessions.some((session) => session.id === current) - ? current - : sessionsData.data.sessions[0]?.id || null - )); + setIsRefreshing(true); + try { + const [statusResponse, sessionsResponse] = await Promise.all([ + authenticatedFetch('/api/browser-use/status'), + authenticatedFetch('/api/browser-use/sessions'), + ]); + const statusData = await readJson<{ data: BrowserUseStatus }>(statusResponse); + const sessionsData = await readJson<{ data: { sessions: BrowserUseSession[] } }>(sessionsResponse); + const nextSessions = sessionsData.data.sessions; + setStatus(statusData.data); + setSessions(nextSessions); + setSelectedSessionId((current) => ( + current && nextSessions.some((session) => session.id === current) + ? current + : nextSessions[0]?.id || null + )); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load Browser Use'); + } finally { + setIsRefreshing(false); + } }, []); useEffect(() => { if (!isVisible) return; - void refresh().catch((err) => setError(err instanceof Error ? err.message : 'Failed to load Browser Use')); + void refresh(); }, [isVisible, refresh]); - useEffect(() => { - if (!selectedSession?.url) return; - setTargetUrl(selectedSession.url); - }, [selectedSession?.id, selectedSession?.url]); - const runAction = useCallback(async (action: () => Promise) => { setIsBusy(true); setError(null); @@ -107,23 +162,6 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) { } }, [refresh]); - const createSession = () => runAction(async () => { - const response = await authenticatedFetch('/api/browser-use/sessions', { method: 'POST' }); - const data = await readJson<{ data: { session: BrowserUseSession } }>(response); - setSelectedSessionId(data.data.session.id); - }); - - const navigate = () => runAction(async () => { - if (!selectedSession) { - throw new Error('Create a browser session first.'); - } - const response = await authenticatedFetch(`/api/browser-use/sessions/${selectedSession.id}/navigate`, { - method: 'POST', - body: JSON.stringify({ url: targetUrl }), - }); - await readJson(response); - }); - const stopSession = () => runAction(async () => { if (!selectedSession) return; const response = await authenticatedFetch(`/api/browser-use/sessions/${selectedSession.id}/stop`, { method: 'POST' }); @@ -137,18 +175,6 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) { setIsFullscreen(false); }); - const grantAgentAccess = () => runAction(async () => { - if (!selectedSession) return; - const response = await authenticatedFetch(`/api/browser-use/sessions/${selectedSession.id}/agent-access/grant`, { method: 'POST' }); - await readJson(response); - }); - - const revokeAgentAccess = () => runAction(async () => { - if (!selectedSession) return; - const response = await authenticatedFetch(`/api/browser-use/sessions/${selectedSession.id}/agent-access/revoke`, { method: 'POST' }); - await readJson(response); - }); - const installBrowserBinaries = () => runAction(async () => { setIsInstalling(true); try { @@ -159,54 +185,16 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) { } }); - const clickViewer = useCallback((event: MouseEvent) => { - if (!selectedSession || selectedSession.status !== 'ready' || !selectedSession.viewport) { - return; - } - viewerRef.current?.focus(); - - const bounds = event.currentTarget.getBoundingClientRect(); - const scaleX = selectedSession.viewport.width / bounds.width; - const scaleY = selectedSession.viewport.height / bounds.height; - const x = Math.round((event.clientX - bounds.left) * scaleX); - const y = Math.round((event.clientY - bounds.top) * scaleY); - - void runAction(async () => { - const response = await authenticatedFetch(`/api/browser-use/sessions/${selectedSession.id}/click`, { - method: 'POST', - body: JSON.stringify({ x, y }), - }); - await readJson(response); - }); - }, [runAction, selectedSession]); - - const keyForEvent = useCallback((event: KeyboardEvent) => { - if (event.key === ' ') return 'Space'; - return event.key; - }, []); - - const pressViewerKey = useCallback((event: KeyboardEvent) => { - if (!selectedSession || selectedSession.status !== 'ready') { - return; - } - - const ignoredKeys = new Set(['Shift', 'Control', 'Alt', 'Meta', 'CapsLock']); - if (ignoredKeys.has(event.key)) { - return; - } - - event.preventDefault(); - const key = keyForEvent(event); - void runAction(async () => { - const response = await authenticatedFetch(`/api/browser-use/sessions/${selectedSession.id}/press-key`, { - method: 'POST', - body: JSON.stringify({ key }), - }); - await readJson(response); - }); - }, [keyForEvent, runAction, selectedSession]); - const needsBrowserBinaries = Boolean(status?.enabled && (!status.playwrightInstalled || !status.chromiumInstalled)); + const activeSessions = sessions.filter((session) => session.status === 'ready'); + const inactiveSessions = sessions.filter((session) => session.status !== 'ready'); + const statusLabel = !status?.enabled + ? 'Disabled' + : status.available + ? 'Ready' + : status.installInProgress || isInstalling + ? 'Installing' + : 'Setup required'; const cursorStyle = selectedSession?.cursor && selectedSession.viewport ? { @@ -215,20 +203,37 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) { } : null; - const renderBrowserSurface = (fullscreen = false) => ( -
( + + ); + + const renderBrowserSurface = (fullscreen = false) => ( +
{selectedSession?.screenshotDataUrl ? (
Browser session screenshot {cursorStyle && (
- {selectedSession?.message || 'Create a browser session to start.'} + {selectedSession?.message || 'No browser screenshot yet.'}

- Install browser binaries from this panel or enable Browser Use from Settings. + Agent-created browser sessions appear here after the agent starts using Browser Use.

)} @@ -260,48 +265,47 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) {

Browser Use

+ {statusLabel}

- Create browser sessions, watch agent activity, and decide which sessions agents may control. + Watch browser sessions created by AI agents and stop them when needed.

-
-
-