feat(desktop): polish desktop shell and shared WebUI surfaces (#4195)

* feat(desktop): add native host scaffold

* feat(webui): track turns and usage in gateway

* feat(webui): polish desktop chat experience

* feat(apps): add ArcGIS and Joplin logos

* feat(desktop): polish shell and shared surfaces

* fix(webui): avoid preview chips for glob references

* test: align CI expectations for token fallback

* feat(webui): preview prompt rail entries

* feat(webui): add prompt navigator drawer

* style(webui): refine prompt navigator placement

* style(webui): align prompt navigator with header actions

* style(webui): simplify prompt navigator header

* refactor(webui): clean thread resource refresh

* feat(desktop): add native reply notifications

* fix(webui): preserve desktop restart and replay state

* fix(desktop): harden gateway proxy startup

* fix(web): fall back when readability is unavailable

* fix(desktop): hide window instead of closing on macos

* fix(webui): unify desktop header actions

* fix(webui): simplify prompt history rows

* fix(desktop): log notification delivery failures

* chore(desktop): clean source package artifacts

* fix(cron): support one-time relative reminders

* fix(webui): reveal scroll button in place

* Revert "fix(cron): support one-time relative reminders"

This reverts commit 4c4661da120a3c7283e0768412bae48604e7390b.

* refactor(webui): extract token usage heatmap

* docs(desktop): clarify contributor guides

---------

Co-authored-by: chengyongru <2755839590@qq.com>
This commit is contained in:
Xubin Ren
2026-06-06 19:49:33 +08:00
committed by GitHub
co-authored by chengyongru
parent a1b9577224
commit ab9f49970d
103 changed files with 10483 additions and 1003 deletions
+129
View File
@@ -0,0 +1,129 @@
# nanobot Desktop
Mac-first desktop app for running nanobot locally with the same product UI as
the browser WebUI.
For users, the desktop app is a local wrapper around nanobot: it starts the
engine for you, keeps config and chat state in the platform app data directory,
and uses the shared WebUI for chat, settings, apps, skills, and workspace
selection.
For contributors, this folder is a native host shell. It reuses the root WebUI
build at `nanobot/web/dist`; it does not copy or fork `webui/src`. Electron owns
the local engine lifecycle, exposes `window.nanobotHost` to the renderer, serves
the `nanobot-app://` app protocol, and proxies `/api/*` plus `/webui/bootstrap`
to a private Unix socket `nanobot desktop-gateway` process.
## What To Read
- Using or trying the app from source: start with the development commands below.
- Changing desktop behavior: read [`docs/development.md`](docs/development.md).
- Adding native host capabilities: read [`docs/host-contract.md`](docs/host-contract.md).
- Keeping browser WebUI and desktop aligned: read [`docs/webui-sync.md`](docs/webui-sync.md).
## Development
This section is for contributors working from a source checkout.
```sh
cd desktop
bun run dev:webui
```
In another terminal:
```sh
cd desktop
bun run dev:app
```
`dev:app` points Electron at the Vite dev server so WebUI changes hot reload.
For source checkouts, the app uses `python3` by default and injects the repo
root into `PYTHONPATH`. Packaged builds look for a bundled interpreter at
`Resources/nanobot-engine/bin/python3`.
## Engine Bundle
Release builds prepare `resources/nanobot-engine/` from a macOS
`python-build-standalone` archive before running `electron-builder`.
By default the script discovers the latest `astral-sh/python-build-standalone`
CPython 3.12 `install_only` asset for the requested architecture.
```sh
cd desktop
bun run make:mac:arm64
bun run make:mac:x64
```
Useful overrides:
- `NANOBOT_DESKTOP_ARCH=arm64|x64`
- `NANOBOT_DESKTOP_PYTHON_VERSION=3.12`
- `PYTHON_STANDALONE_RELEASE=20260510`
- `PYTHON_STANDALONE_TARBALL=/path/to/archive.tar.gz`
- `PYTHON_STANDALONE_URL=https://.../cpython-...tar.gz`
- `NANOBOT_WHEELHOUSE=/path/to/wheels` to install from a locked wheelhouse
The script installs the current checkout's `nanobot-ai[api]` into the bundled
runtime and writes `nanobot-engine.json` for diagnostics.
## Updating Builds
The native host does not copy the WebUI source or fork the Python agent code. A
release bundle is assembled from the current repository state:
1. Build the shared WebUI:
```sh
bun run build --prefix webui
```
`electron-builder` packages the resulting `nanobot/web/dist` directory as
`Resources/nanobot-webui`.
2. Prepare the bundled Python engine:
```sh
cd desktop
NANOBOT_DESKTOP_ARCH=arm64 bun run prepare-engine
```
The script installs the current checkout's `nanobot-ai[api]` package into
`resources/nanobot-engine/`, so agent, provider, tool, WebSocket, and config
changes flow into the next desktop build automatically.
3. Build the desktop app and DMG:
```sh
bun run make:mac:arm64
bun run make:mac:x64
```
User data is not stored in the app bundle. Config, sessions, logs, workspace
state, and the default workspace remain under the platform app data directory,
so updating the app replaces code without overwriting local user state.
## Runtime Contract
- User data lives under Electron's platform app data directory. In development
this is usually `~/Library/Application Support/@nanobot/desktop/` on macOS;
packaged builds use the packaged app name.
- Fresh installs start the private engine directly. The Python desktop gateway
creates the first `config.json` with defaults, then shared WebUI settings own
provider, model, and credential setup.
- The gateway listens on a per-user Unix socket in the app data directory and uses a transient secret.
- The gateway starts with only the WebSocket local channel enabled and does not serve the WebUI static bundle.
- The renderer loads assets through `nanobot-app://app/...`; browser users cannot open the native UI from a localhost port.
- WebSocket traffic uses the generic `nanobot-host://engine/...` URL, is bridged over Electron IPC, and still uses the short-lived token minted by `/webui/bootstrap`.
- Host IPC only accepts the trusted app origin, and socket bridging only accepts the `nanobot-host://engine/...` scheme.
- Native WebUI responses include a restrictive Content Security Policy.
- WebUI talks only to the generic `window.nanobotHost` contract. Product-specific native behavior stays in this folder.
Generated release artifacts, node modules, and bundled runtimes remain ignored
so the tracked desktop package stays source-only.
See also:
- [`docs/development.md`](docs/development.md)
- [`docs/host-contract.md`](docs/host-contract.md)
- [`docs/webui-sync.md`](docs/webui-sync.md)
+594
View File
@@ -0,0 +1,594 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "@nanobot/desktop",
"devDependencies": {
"@types/node": "^22.10.5",
"electron": "^42.3.0",
"electron-builder": "^26.8.1",
"typescript": "^5.7.2",
},
},
},
"packages": {
"7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="],
"@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="],
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
"@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="],
"@electron/get": ["@electron/get@5.0.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^3.0.0", "graceful-fs": "^4.2.11", "progress": "^2.0.3", "semver": "^7.6.3", "sumchecker": "^3.0.1" }, "optionalDependencies": { "undici": "^7.24.4" } }, "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA=="],
"@electron/notarize": ["@electron/notarize@2.5.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" } }, "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A=="],
"@electron/osx-sign": ["@electron/osx-sign@1.3.3", "", { "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" } }, "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg=="],
"@electron/rebuild": ["@electron/rebuild@4.0.4", "", { "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" } }, "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg=="],
"@electron/universal": ["@electron/universal@2.0.3", "", { "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" } }, "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g=="],
"@electron/windows-sign": ["@electron/windows-sign@1.2.2", "", { "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" } }, "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ=="],
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
"@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="],
"@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="],
"@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="],
"@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="],
"@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="],
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
"@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="],
"@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="],
"@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="],
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/node": ["@types/node@22.19.19", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew=="],
"@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="],
"@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
"@types/verror": ["@types/verror@1.10.11", "", {}, "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg=="],
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="],
"abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
"ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"app-builder-bin": ["app-builder-bin@5.0.0-alpha.12", "", {}, "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w=="],
"app-builder-lib": ["app-builder-lib@26.8.1", "", { "dependencies": { "@develar/schema-utils": "~2.6.5", "@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.3", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "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.8.1", "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.0.3", "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", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.8.1", "electron-builder-squirrel-windows": "26.8.1" } }, "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="],
"astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="],
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
"async-exit-hook": ["async-exit-hook@2.0.1", "", {}, "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw=="],
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="],
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
"brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"builder-util": ["builder-util@26.8.1", "", { "dependencies": { "7zip-bin": "~5.2.0", "@types/debug": "^4.1.6", "app-builder-bin": "5.0.0-alpha.12", "builder-util-runtime": "9.5.1", "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" } }, "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw=="],
"builder-util-runtime": ["builder-util-runtime@9.5.1", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ=="],
"cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="],
"cacheable-request": ["cacheable-request@7.0.4", "", { "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" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="],
"ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
"compare-version": ["compare-version@0.1.2", "", {}, "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
"crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="],
"cross-dirname": ["cross-dirname@0.1.0", "", {}, "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
"defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="],
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="],
"dir-compare": ["dir-compare@4.2.0", "", { "dependencies": { "minimatch": "^3.0.5", "p-limit": "^3.1.0 " } }, "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ=="],
"dmg-builder": ["dmg-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" }, "optionalDependencies": { "dmg-license": "^1.0.11" } }, "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg=="],
"dmg-license": ["dmg-license@1.0.11", "", { "dependencies": { "@types/plist": "^3.0.1", "@types/verror": "^1.10.3", "ajv": "^6.10.0", "crc": "^3.8.0", "iconv-corefoundation": "^1.1.7", "plist": "^3.0.4", "smart-buffer": "^4.0.2", "verror": "^1.10.0" }, "os": "darwin", "bin": "bin/dmg-license.js" }, "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q=="],
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": "bin/cli.js" }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
"electron": ["electron@42.3.0", "", { "dependencies": { "@electron/get": "^5.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-9ZiLdRXk+WDxW1OgIUz8J2rIQ5TYU9o629gCOjU48Q3dQiOmym7osWsH5Ubs/Jh4uuFLn6m6SBD2rmRXLAPz9g=="],
"electron-builder": ["electron-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.8.1", "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" } }, "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw=="],
"electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "electron-winstaller": "5.4.0" } }, "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA=="],
"electron-publish": ["electron-publish@26.8.1", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w=="],
"electron-winstaller": ["electron-winstaller@5.4.0", "", { "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", "fs-extra": "^7.0.1", "lodash": "^4.17.21", "temp": "^0.9.0" }, "optionalDependencies": { "@electron/windows-sign": "^1.1.2" } }, "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
"err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": "cli.js" }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
"extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="],
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
"glob": ["glob@7.2.3", "", { "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" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"global-agent": ["global-agent@3.0.0", "", { "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" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="],
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"got": ["got@11.8.6", "", { "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" } }, "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
"hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
"http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"iconv-corefoundation": ["iconv-corefoundation@1.1.7", "", { "dependencies": { "cli-truncate": "^2.1.0", "node-addon-api": "^1.6.3" }, "os": "darwin" }, "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="],
"isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
"jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": "bin/cli.js" }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="],
"jiti": ["jiti@2.7.0", "", { "bin": "lib/jiti-cli.mjs" }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
"js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
"json5": ["json5@2.2.3", "", { "bin": "lib/cli.js" }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="],
"lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
"lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
"lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mime": ["mime@2.6.0", "", { "bin": "cli.js" }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="],
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
"minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": "bin/cmd.js" }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"node-abi": ["node-abi@4.31.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw=="],
"node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="],
"node-api-version": ["node-api-version@0.2.1", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q=="],
"node-gyp": ["node-gyp@12.3.0", "", { "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": "bin/node-gyp.js" }, "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg=="],
"nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": "bin/nopt.js" }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="],
"normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="],
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"pe-library": ["pe-library@0.4.1", "", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="],
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="],
"postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": "dist/cli.js" }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="],
"proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="],
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
"promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="],
"proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="],
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="],
"read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": "cli.js" }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="],
"resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],
"responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
"rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": "bin.js" }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="],
"roarr": ["roarr@2.15.4", "", { "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" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"sanitize-filename": ["sanitize-filename@1.6.4", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg=="],
"sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
"semver": ["semver@7.8.1", "", { "bin": "bin/semver.js" }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
"semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
"serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="],
"slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="],
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
"stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"tar": ["tar@7.5.15", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ=="],
"temp": ["temp@0.9.4", "", { "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" } }, "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA=="],
"temp-file": ["temp-file@3.4.0", "", { "dependencies": { "async-exit-hook": "^2.0.1", "fs-extra": "^10.0.0" } }, "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg=="],
"tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="],
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"tmp": ["tmp@0.2.7", "", {}, "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw=="],
"tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="],
"truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="],
"type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici": ["undici@7.27.0", "", {}, "sha512-+t2Z/GwkZQDtu00813aP66ygViGtPHKhhoFZpQKpKrE+9jIgES+Zw+mFNaDWOVRKiuJjuqKHzD3B1sfGg8+ZOQ=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="],
"verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="],
"which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"@electron/asar/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"@electron/fuses/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="],
"@electron/universal/fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="],
"@electron/universal/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
"@electron/windows-sign/fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="],
"@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "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" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="],
"app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
"app-builder-lib/semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"dir-compare/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"electron/@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="],
"electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="],
"filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
"glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"node-gyp/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"node-gyp/undici": ["undici@6.26.0", "", {}, "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A=="],
"node-gyp/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="],
"postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="],
"tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": "bin/semver" }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
"@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="],
"@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="],
"app-builder-lib/@electron/get/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"app-builder-lib/@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
"app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"dir-compare/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="],
"electron-winstaller/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
"electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"electron/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"filelist/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="],
"glob/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="],
"node-gyp/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="],
"@electron/asar/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"@electron/universal/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"app-builder-lib/@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"dir-compare/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
}
}
+116
View File
@@ -0,0 +1,116 @@
# Desktop Development Guide
This guide is for GitHub contributors who want to change the desktop app. If
you are using nanobot rather than developing it, the important bit is simpler:
desktop runs the local engine for you and shows the same chat, settings, apps,
skills, and workspace UI as the browser WebUI.
`desktop` is the native host for the shared nanobot WebUI. It is not a fork of
the WebUI, and it should not grow a second copy of product UI.
The healthy mental model is:
```text
nanobot core -> agent runtime, gateway, providers, tools, memory
webui -> shared product UI and runtime-aware UI
desktop -> native host, engine lifecycle, secure host capabilities
```
## Development Workflow
Use this when developing from a source checkout.
Run the shared WebUI dev server:
```sh
cd desktop
bun run dev:webui
```
Run the Electron host in another terminal:
```sh
cd desktop
bun run dev:app
```
In development, Electron loads `http://127.0.0.1:5173`, so changes under
`webui/src` hot reload. Changes under `desktop/src` require restarting
`dev:app`.
For source checkouts, the host starts the engine with local `python3` and
injects the repository root into `PYTHONPATH`. This means Python changes under
`nanobot/` are picked up from the current checkout.
## Where Code Goes
Use this table before adding a desktop feature:
| Change | Location |
| --- | --- |
| Agent behavior, tools, providers, memory, config schema | `nanobot/` |
| Shared chat UI, settings UI, reusable product UI | `webui/` |
| Runtime-aware UI rows, such as native engine status or open logs buttons | `webui/` |
| The implementation behind native capabilities | `desktop/src/main.ts` |
| The trusted renderer bridge contract | `desktop/src/preload.cts` and `desktop/docs/host-contract.md` |
| Electron window, app protocol, native menus, lifecycle, packaging | `desktop/src` and `desktop/package.json` |
| WebSocket-over-Unix-socket bridge | `desktop/src/unixWebSocket.ts` |
| Bundled Python runtime preparation | `desktop/scripts/prepare-engine.mjs` |
For example, if desktop Settings needs an "Open logs" button, the button belongs
in the shared WebUI settings page because it is product UI. The actual filesystem
operation belongs in the desktop host and is exposed through `window.nanobotHost`.
## Host Contract
The shared WebUI talks to desktop through `window.nanobotHost`. WebUI code may
check for host capabilities, but it must not import Electron, Node.js modules,
or desktop source files.
Prefer capability-driven UI:
```text
if host can open logs -> show Open logs
if host can restart engine -> show Restart engine
```
Avoid platform-driven UI:
```text
if desktop -> run Electron-specific logic in WebUI
```
This keeps the WebUI usable in browsers and leaves room for future native hosts
without rewriting product screens.
## Adding A Desktop Feature
Before implementing, answer these questions:
1. Is this product UI or a native capability?
2. Can the WebUI express it through a generic capability instead of a desktop flag?
3. Does the host API validate trusted origins and accepted URL schemes?
4. Does browser WebUI still work when `window.nanobotHost` is missing?
5. Does the engine behavior belong in nanobot core instead of Electron?
6. Does packaged mode use app data for user state instead of app resources?
## Anti-Patterns
- Do not copy or fork `webui/src` into `desktop/`.
- Do not import Electron or Node.js modules from `webui/src`.
- Do not add provider-specific onboarding screens to `desktop/`.
- Do not duplicate WebUI settings or login flows in Electron-owned HTML.
- Do not make `desktop/src/main.ts` own agent behavior.
- Do not commit `desktop/node_modules`, `desktop/build`, `desktop/dist`, DMGs,
or `desktop/resources/nanobot-engine`.
## Release Shape
Release builds assemble three existing parts:
1. the shared WebUI build from `nanobot/web/dist`,
2. the Python engine prepared under `desktop/resources/nanobot-engine`,
3. the Electron host compiled from `desktop/src`.
User config, logs, sessions, workspace state, and the default workspace live in
the platform app data directory, not inside the app bundle.
+94
View File
@@ -0,0 +1,94 @@
# Native Host Contract
This is a contributor reference for the boundary between the shared WebUI and
the native desktop host. Users should not need this contract to run the app, but
it explains why the desktop app can use native capabilities without turning the
WebUI into Electron-specific code.
`desktop` is a native host shell around the shared WebUI build. The renderer
must not import Electron directly. It receives a minimal bridge at
`window.nanobotHost`.
## Runtime API
```ts
type HostRuntimeInfo = {
surface: "native";
app_version: string;
engine_status: "starting" | "ready" | "restarting" | "stopped" | "crashed";
data_dir: string;
logs_dir: string;
config_path: string;
workspace_path: string;
python: string;
engine_transport?: "unix_socket";
};
type HostSocketEvent =
| { id: string; type: "open" }
| { id: string; type: "message"; data: string }
| { id: string; type: "error"; message: string }
| { id: string; type: "close"; code?: number; reason?: string };
type NanobotHost = {
getRuntimeInfo(): Promise<HostRuntimeInfo>;
restartEngine(): Promise<void>;
pickFolder(): Promise<string | null>;
openLogs(): Promise<void>;
exportDiagnostics(): Promise<string>;
checkForUpdates(): Promise<{ supported: boolean; message?: string }>;
openSocket(url: string): Promise<string>;
sendSocket(id: string, data: string): Promise<void>;
closeSocket(id: string): Promise<void>;
onSocketEvent(listener: (event: HostSocketEvent) => void): () => void;
onRuntimeStatus(listener: (status: HostRuntimeInfo["engine_status"]) => void): () => void;
};
```
## First Run
The desktop host starts the private engine immediately. If the native data
directory has no `config.json`, `nanobot desktop-gateway` creates one with
defaults before serving the shared WebUI. Provider, model, credential, and login
setup stay in WebUI settings instead of Electron-owned HTML.
## Socket Bridge
The engine listens on a per-user Unix socket under the app data directory.
`/webui/bootstrap` returns `runtime_surface: "native"` and a WebSocket URL in
the `nanobot-host://engine/...` scheme. WebUI never opens that URL directly in
the browser runtime; it hands the URL to `window.nanobotHost.openSocket`.
The native host then performs the WebSocket handshake against the Unix socket
and forwards events over Electron IPC.
## Host Security Boundary
The host bridge is intentionally narrower than a general Electron preload:
- IPC calls are accepted only from renderer frames loaded from `nanobot-app://app/...`.
- `openSocket` accepts only `nanobot-host://engine/...` URLs.
- External navigation is denied in the app window; safe web links are opened by
the operating system.
- Native WebUI responses carry a restrictive Content Security Policy and
`X-Content-Type-Options: nosniff`.
- The renderer runs with `nodeIntegration: false`, `contextIsolation: true`,
`sandbox: true`, and `webSecurity: true`.
Security-sensitive tool behavior still belongs in nanobot core. The host
protects the native app boundary; the engine protects file, network, and tool
permissions.
## Data Directory
The host stores config, workspace, sessions, logs, and transient socket files
under Electron's platform app data directory. In development on macOS this is
usually:
```text
~/Library/Application Support/@nanobot/desktop/
```
Packaged builds use the packaged app name.
The app bundle is replaceable. User data is not stored in the bundle.
+71
View File
@@ -0,0 +1,71 @@
# WebUI Sync Workflow
This workflow is for contributors keeping the desktop app and browser WebUI in
sync. Users should experience them as one product surface: desktop adds a native
host and local engine lifecycle, while chat, settings, apps, skills, and
workspace UI still come from the shared WebUI.
`desktop` consumes the shared WebUI build output. It must not copy, fork, or
vendor `webui/src`.
## Development
Run the WebUI dev server:
```sh
cd desktop
bun run dev:webui
```
Run the native host in another terminal:
```sh
cd desktop
bun run dev:app
```
The host loads `http://127.0.0.1:5173` in development, so React changes hot
reload. Main/preload changes still require restarting `dev:app`.
## Release Build
1. Build the shared WebUI:
```sh
bun run build --prefix webui
```
2. Prepare the bundled Python engine:
```sh
cd desktop
NANOBOT_DESKTOP_ARCH=arm64 bun run prepare-engine
```
3. Build the app:
```sh
bun run make:mac:arm64
bun run make:mac:x64
```
`electron-builder` packages `nanobot/web/dist` as `Resources/nanobot-webui`.
## Checklist
- WebUI source remains host-neutral: it may branch on generic runtime
capabilities, but it must not import Electron or desktop source files.
```sh
rg -n "from ['\\\"]electron|desktop/src|nanobotDesktop" webui/src
```
This command should print nothing.
- Native host behavior is implemented in `desktop/src`.
- Provider, model, credential, and login setup stay in shared WebUI settings.
Do not duplicate those flows in Electron-owned HTML.
- Shared UI behavior is implemented in `webui/src` through `window.nanobotHost`
and generic runtime capability checks.
- Do not copy React components from `webui/src` into this folder.
- Do not commit bundled runtimes, DMGs, or `node_modules`.
+55
View File
@@ -0,0 +1,55 @@
{
"name": "@nanobot/desktop",
"version": "0.2.1",
"private": true,
"type": "module",
"main": "build/main.js",
"scripts": {
"build": "tsc -p tsconfig.json",
"build:webui": "cd ../webui && bun run build",
"prepare-engine": "node scripts/prepare-engine.mjs",
"print-engine-url": "node scripts/prepare-engine.mjs --print-runtime-url",
"dev": "bun run dev:app",
"dev:app": "NANOBOT_DESKTOP_WEB_DEV_URL=http://127.0.0.1:5173 bun run build && electron .",
"dev:webui": "cd ../webui && bun run dev",
"start": "electron .",
"make:mac:arm64": "bun run build:webui && NANOBOT_DESKTOP_ARCH=arm64 bun run prepare-engine && bun run build && electron-builder --mac dmg --arm64",
"make:mac:x64": "bun run build:webui && NANOBOT_DESKTOP_ARCH=x64 bun run prepare-engine && bun run build && electron-builder --mac dmg --x64",
"dist:mac:arm64": "electron-builder --mac dmg --arm64",
"dist:mac:x64": "electron-builder --mac dmg --x64"
},
"devDependencies": {
"@types/node": "^22.10.5",
"electron": "^42.3.0",
"electron-builder": "^26.8.1",
"typescript": "^5.7.2"
},
"build": {
"appId": "wiki.nanobot.desktop",
"productName": "nanobot",
"asar": true,
"files": [
"build/**/*",
"package.json"
],
"extraResources": [
{
"from": "../nanobot/web/dist",
"to": "nanobot-webui"
},
{
"from": "resources/nanobot-engine",
"to": "nanobot-engine",
"filter": [
"**/*"
]
}
],
"mac": {
"category": "public.app-category.developer-tools",
"target": [
"dmg"
]
}
}
}
+265
View File
@@ -0,0 +1,265 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import {
cp,
lstat,
mkdir,
readdir,
readFile,
readlink,
rm,
stat,
symlink,
unlink,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, "..", "..");
const desktopRoot = path.resolve(__dirname, "..");
const engineDest = path.resolve(
process.env.NANOBOT_ENGINE_DEST ?? path.join(desktopRoot, "resources", "nanobot-engine"),
);
const pythonVersion = process.env.NANOBOT_DESKTOP_PYTHON_VERSION ?? "3.12";
const githubBase = "https://github.com/astral-sh/python-build-standalone";
function run(command, args, options = {}) {
const result = spawnSync(command, args, {
stdio: "inherit",
...options,
});
if (result.status !== 0) {
throw new Error(`${command} ${args.join(" ")} failed`);
}
}
async function download(url, destination) {
const response = await fetch(url, {
headers: {
"User-Agent": "nanobot/desktop-build",
},
});
if (!response.ok) {
throw new Error(`failed to download ${url}: HTTP ${response.status}`);
}
await writeFile(destination, Buffer.from(await response.arrayBuffer()));
}
async function fetchText(url) {
const response = await fetch(url, {
headers: {
"User-Agent": "nanobot/desktop-build",
"Accept": "text/html",
},
});
if (!response.ok) {
throw new Error(`failed to fetch ${url}: HTTP ${response.status}`);
}
return await response.text();
}
function targetTriple() {
const requested = process.env.NANOBOT_DESKTOP_ARCH ?? process.arch;
if (requested === "arm64" || requested === "aarch64") return "aarch64-apple-darwin";
if (requested === "x64" || requested === "x86_64") return "x86_64-apple-darwin";
throw new Error(`unsupported desktop engine arch: ${requested}`);
}
function latestReleaseTag(html) {
const match = html.match(/\/astral-sh\/python-build-standalone\/releases\/tag\/(\d{8})/);
if (!match) {
throw new Error("could not find latest python-build-standalone release tag");
}
return match[1];
}
async function defaultStandaloneUrl() {
const release =
process.env.PYTHON_STANDALONE_RELEASE
?? latestReleaseTag(await fetchText(`${githubBase}/releases`));
const triple = targetTriple();
const assetHtml = await fetchText(`${githubBase}/releases/expanded_assets/${release}`);
const escapedVersion = pythonVersion.replace(".", "\\.");
const assetPattern = new RegExp(
`cpython-${escapedVersion}\\.\\d+\\+${release}-${triple}-install_only\\.tar\\.gz`,
);
const asset = assetHtml.match(assetPattern)?.[0];
if (!asset) {
throw new Error(
`could not find a CPython ${pythonVersion} install_only asset for ${triple} in ${release}`,
);
}
return `${githubBase}/releases/download/${release}/${asset}`;
}
async function walk(dir, matches = []) {
for (const entry of await readdir(dir)) {
const fullPath = path.join(dir, entry);
const info = await stat(fullPath);
if (info.isDirectory()) {
await walk(fullPath, matches);
} else if (entry === "python3" || entry === "python") {
matches.push(fullPath);
}
}
return matches;
}
async function findStandaloneRoot(extractDir) {
const candidates = await walk(extractDir);
for (const candidate of candidates) {
const normalized = candidate.split(path.sep).join("/");
if (normalized.endsWith("/install/bin/python3")) {
return path.dirname(path.dirname(candidate));
}
}
for (const candidate of candidates) {
const parent = path.dirname(candidate);
if (path.basename(parent) === "bin") {
return path.dirname(parent);
}
}
throw new Error("could not find python-build-standalone bin/python3 in extracted archive");
}
function isInside(parent, child) {
const relative = path.relative(parent, child);
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
async function rewriteInternalSymlinks(root, sourceRoot) {
const entries = await readdir(root, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(root, entry.name);
const info = await lstat(fullPath);
if (info.isSymbolicLink()) {
const target = await readlink(fullPath);
if (!path.isAbsolute(target) || !isInside(sourceRoot, target)) {
continue;
}
const targetInBundle = path.join(engineDest, path.relative(sourceRoot, target));
const relativeTarget = path.relative(path.dirname(fullPath), targetInBundle);
await unlink(fullPath);
await symlink(relativeTarget, fullPath);
} else if (entry.isDirectory()) {
await rewriteInternalSymlinks(fullPath, sourceRoot);
}
}
}
async function assertNoExternalSymlinks(root) {
const entries = await readdir(root, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(root, entry.name);
const info = await lstat(fullPath);
if (info.isSymbolicLink()) {
const target = await readlink(fullPath);
if (path.isAbsolute(target) && !isInside(engineDest, target)) {
throw new Error(`external symlink left in engine bundle: ${fullPath} -> ${target}`);
}
} else if (entry.isDirectory()) {
await assertNoExternalSymlinks(fullPath);
}
}
}
async function tarSupportsZstd() {
const result = spawnSync("tar", ["--help"], { encoding: "utf8" });
return `${result.stdout}\n${result.stderr}`.includes("zstd");
}
async function extractArchive(archive, destination) {
await mkdir(destination, { recursive: true });
if (archive.endsWith(".tar.zst") && !(await tarSupportsZstd())) {
throw new Error("tar.zst archives require a tar build with zstd support");
}
run("tar", ["-xf", archive, "-C", destination]);
}
async function resolveArchive() {
const localArchive = process.env.PYTHON_STANDALONE_TARBALL;
if (localArchive) {
return { archive: path.resolve(localArchive), cleanupArchive: false };
}
const url = process.env.PYTHON_STANDALONE_URL ?? await defaultStandaloneUrl();
const suffix = url.endsWith(".tar.gz") ? ".tar.gz" : path.extname(url);
const downloadPath = path.join(tmpdir(), `nanobot-python-${Date.now()}${suffix}`);
console.log(`Downloading Python runtime from ${url}`);
await download(url, downloadPath);
return { archive: downloadPath, cleanupArchive: true };
}
async function installNanobot(pythonPath) {
run(pythonPath, ["-m", "ensurepip", "--upgrade"]);
run(pythonPath, ["-m", "pip", "install", "--upgrade", "pip"]);
const installArgs = ["-m", "pip", "install", "--upgrade"];
const wheelhouse = process.env.NANOBOT_WHEELHOUSE;
if (wheelhouse) {
installArgs.push("--no-index", "--find-links", path.resolve(wheelhouse));
}
installArgs.push(`${repoRoot}[api]`);
run(pythonPath, installArgs);
}
async function writeManifest(pythonPath) {
const version = spawnSync(pythonPath, ["--version"], { encoding: "utf8" });
const pyproject = await readFile(path.join(repoRoot, "pyproject.toml"), "utf8");
const match = pyproject.match(/^version\s*=\s*"([^"]+)"/m);
await writeFile(
path.join(engineDest, "nanobot-engine.json"),
JSON.stringify(
{
python: version.stdout.trim() || version.stderr.trim(),
nanobot_version: match?.[1] ?? "unknown",
prepared_at: new Date().toISOString(),
source: "python-build-standalone",
},
null,
2,
),
"utf8",
);
}
async function main() {
if (process.argv.includes("--print-runtime-url")) {
console.log(await defaultStandaloneUrl());
return;
}
const { archive, cleanupArchive } = await resolveArchive();
const extractDir = path.join(tmpdir(), `nanobot-engine-${Date.now()}`);
try {
await rm(extractDir, { recursive: true, force: true });
await extractArchive(archive, extractDir);
const standaloneRoot = await findStandaloneRoot(extractDir);
await rm(engineDest, { recursive: true, force: true });
await mkdir(path.dirname(engineDest), { recursive: true });
await cp(standaloneRoot, engineDest, { recursive: true });
await rewriteInternalSymlinks(engineDest, standaloneRoot);
await assertNoExternalSymlinks(engineDest);
const pythonPath = path.join(engineDest, "bin", "python3");
await installNanobot(pythonPath);
await writeManifest(pythonPath);
await writeFile(path.join(engineDest, ".gitkeep"), "", "utf8");
console.log(`Prepared nanobot desktop engine at ${engineDest}`);
} finally {
await rm(extractDir, { recursive: true, force: true });
if (cleanupArchive) {
await rm(archive, { force: true });
}
}
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
+783
View File
@@ -0,0 +1,783 @@
import { spawn, type ChildProcess } from "node:child_process";
import { randomBytes } from "node:crypto";
import { createWriteStream, existsSync } from "node:fs";
import { mkdir, rm, writeFile } from "node:fs/promises";
import http from "node:http";
import path from "node:path";
import { pathToFileURL } from "node:url";
import {
app,
BrowserWindow,
dialog,
ipcMain,
net as electronNet,
protocol,
session,
shell,
} from "electron";
import type { IpcMainInvokeEvent, WebContents } from "electron";
import { UnixWebSocketClient } from "./unixWebSocket.js";
import {
clearDesktopNotificationBadge,
handleDesktopNotificationFrame,
} from "./notifications.js";
type EngineStatus = "starting" | "ready" | "restarting" | "stopped" | "crashed";
type HostRuntime = {
configPath: string;
gateway: ChildProcess;
logsDir: string;
python: string;
secret: string;
socketPath: string;
status: EngineStatus;
workspacePath: string;
};
let runtime: HostRuntime | null = null;
let mainWindow: BrowserWindow | null = null;
let crashRestartAttempts = 0;
let isQuitting = false;
const hostSockets = new Map<string, UnixWebSocketClient>();
const APP_PROTOCOL = "nanobot-app:";
const APP_HOST = "app";
const HOST_SOCKET_PROTOCOL = "nanobot-host:";
const HOST_SOCKET_HOST = "engine";
const SAFE_EXTERNAL_PROTOCOLS = new Set(["https:", "http:", "mailto:"]);
const GATEWAY_REQUEST_TIMEOUT_MS = 12_000;
const GATEWAY_REQUEST_RETRIES = 2;
const GATEWAY_RETRY_DELAY_MS = 80;
protocol.registerSchemesAsPrivileged([
{
scheme: "nanobot-app",
privileges: {
standard: true,
secure: true,
supportFetchAPI: true,
corsEnabled: false,
},
},
]);
function repoRoot(): string {
return process.env.NANOBOT_DESKTOP_REPO_ROOT
? path.resolve(process.env.NANOBOT_DESKTOP_REPO_ROOT)
: path.resolve(app.getAppPath(), "..");
}
function bundledResourcePath(name: string): string {
return app.isPackaged
? path.join(process.resourcesPath, name)
: path.join(repoRoot(), "desktop", "resources", name);
}
function webDistPath(root: string): string {
if (process.env.NANOBOT_DESKTOP_WEB_DIST) {
return path.resolve(process.env.NANOBOT_DESKTOP_WEB_DIST);
}
const bundled = path.join(process.resourcesPath, "nanobot-webui");
if (app.isPackaged && existsSync(path.join(bundled, "index.html"))) {
return bundled;
}
return path.join(root, "nanobot", "web", "dist");
}
function webDevUrl(): string | null {
const value = process.env.NANOBOT_DESKTOP_WEB_DEV_URL?.trim();
return value ? value.replace(/\/+$/, "") : null;
}
function isTrustedAppUrl(rawUrl: string): boolean {
try {
const url = new URL(rawUrl);
return url.protocol === APP_PROTOCOL && url.host === APP_HOST;
} catch {
return false;
}
}
function assertTrustedIpc(event: IpcMainInvokeEvent): void {
const frameUrl = event.senderFrame?.url || event.sender.getURL();
if (!isTrustedAppUrl(frameUrl)) {
throw new Error("Blocked host API call from an untrusted renderer");
}
}
function parseHostSocketUrl(rawUrl: unknown): string {
if (typeof rawUrl !== "string") {
throw new Error("Host socket URL must be a string");
}
const url = new URL(rawUrl);
if (url.protocol !== HOST_SOCKET_PROTOCOL || url.host !== HOST_SOCKET_HOST) {
throw new Error("Host socket URL is not allowed");
}
if (url.username || url.password) {
throw new Error("Host socket URL credentials are not allowed");
}
return url.toString();
}
function openExternalIfSafe(rawUrl: string): void {
try {
const url = new URL(rawUrl);
if (SAFE_EXTERNAL_PROTOCOLS.has(url.protocol)) {
void shell.openExternal(url.toString());
}
} catch {
// Ignore malformed or unsupported external URLs.
}
}
function desktopContentSecurityPolicy(devUrl: string | null): string {
const connectSrc = ["'self'", "nanobot-host:"];
if (devUrl) {
const url = new URL(devUrl);
connectSrc.push(url.origin, url.origin.replace(/^http/, "ws"));
}
return [
"default-src 'self'",
"base-uri 'self'",
"object-src 'none'",
"frame-ancestors 'none'",
"form-action 'none'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob: https: nanobot-app:",
"font-src 'self' data:",
"media-src 'self' data: blob:",
"worker-src 'self' blob:",
`connect-src ${connectSrc.join(" ")}`,
].join("; ");
}
function withSecurityHeaders(response: Response, devUrl: string | null): Response {
const headers = new Headers(response.headers);
headers.set("Content-Security-Policy", desktopContentSecurityPolicy(devUrl));
headers.set("X-Content-Type-Options", "nosniff");
if (devUrl) {
headers.set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
headers.set("Pragma", "no-cache");
headers.set("Expires", "0");
}
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
}
function handleHostIpc(
channel: string,
handler: (event: IpcMainInvokeEvent, ...args: unknown[]) => unknown | Promise<unknown>,
): void {
ipcMain.handle(channel, async (event, ...args) => {
assertTrustedIpc(event);
return await handler(event, ...args);
});
}
function userDataPath(name: string): string {
return path.join(app.getPath("userData"), name);
}
function engineSocketPath(): string {
return userDataPath("engine.sock");
}
function pythonExecutable(): string {
if (process.env.NANOBOT_DESKTOP_PYTHON) {
return path.resolve(process.env.NANOBOT_DESKTOP_PYTHON);
}
const bundled = path.join(bundledResourcePath("nanobot-engine"), "bin", "python3");
if (existsSync(bundled)) return bundled;
return "python3";
}
function engineCwd(root: string): string {
return app.isPackaged ? app.getPath("userData") : root;
}
function engineEnv(root: string): NodeJS.ProcessEnv {
if (app.isPackaged) {
return { ...process.env };
}
return {
...process.env,
PYTHONPATH: [root, process.env.PYTHONPATH].filter(Boolean).join(path.delimiter),
};
}
async function ensureAppDirs(): Promise<{
configPath: string;
logsDir: string;
workspacePath: string;
}> {
const dataDir = app.getPath("userData");
const logsDir = userDataPath("logs");
const workspacePath = userDataPath("workspace");
await Promise.all([
mkdir(dataDir, { recursive: true }),
mkdir(logsDir, { recursive: true }),
mkdir(workspacePath, { recursive: true }),
]);
return {
configPath: userDataPath("config.json"),
logsDir,
workspacePath,
};
}
function appendGatewayLogs(gateway: ChildProcess, logsDir: string): void {
const logPath = path.join(logsDir, "engine.log");
const stream = createWriteStream(logPath, { flags: "a" });
gateway.stdout?.on("data", (chunk) => {
stream.write(chunk);
process.stdout.write(`[nanobot] ${chunk}`);
});
gateway.stderr?.on("data", (chunk) => {
stream.write(chunk);
process.stderr.write(`[nanobot] ${chunk}`);
});
gateway.once("exit", (code, signal) => {
stream.write(`\n[nanobot] engine exited code=${code ?? ""} signal=${signal ?? ""}\n`);
stream.end();
});
}
function notifyRuntimeStatus(status: EngineStatus): void {
if (runtime) runtime.status = status;
sendToRenderer(mainWindow?.webContents, "nanobot:runtime-status", status);
}
function sendToRenderer(
sender: WebContents | null | undefined,
channel: string,
payload: unknown,
): void {
if (!sender || sender.isDestroyed()) return;
sender.send(channel, payload);
}
function closeHostSockets(): void {
for (const [id, socket] of hostSockets) {
socket.close();
hostSockets.delete(id);
}
}
async function fetchGateway(
current: HostRuntime,
requestPath: string,
init: {
body?: ArrayBuffer;
headers?: Headers | Record<string, string>;
method: string;
},
): Promise<Response> {
let lastError: unknown;
for (let attempt = 0; attempt <= GATEWAY_REQUEST_RETRIES; attempt += 1) {
try {
return await fetchGatewayOnce(current, requestPath, init);
} catch (error) {
lastError = error;
if (!isTransientGatewayError(error) || attempt >= GATEWAY_REQUEST_RETRIES) {
break;
}
await new Promise((resolve) => setTimeout(resolve, GATEWAY_RETRY_DELAY_MS));
}
}
throw lastError instanceof Error ? lastError : new Error("gateway request failed");
}
async function fetchGatewayOnce(
current: HostRuntime,
requestPath: string,
init: {
body?: ArrayBuffer;
headers?: Headers | Record<string, string>;
method: string;
},
): Promise<Response> {
const body = init.body ? Buffer.from(init.body) : undefined;
const headers: http.OutgoingHttpHeaders = {};
if (init.headers instanceof Headers) {
init.headers.forEach((value, key) => {
headers[key] = value;
});
} else {
for (const [key, value] of Object.entries(init.headers ?? {})) {
headers[key] = value;
}
}
if (body) headers["content-length"] = String(body.length);
return await new Promise<Response>((resolve, reject) => {
let settled = false;
const fail = (error: Error) => {
if (settled) return;
settled = true;
reject(error);
};
const req = http.request(
{
socketPath: current.socketPath,
path: requestPath,
method: init.method,
headers,
},
(res) => {
const chunks: Buffer[] = [];
res.on("data", (chunk: Buffer) => chunks.push(chunk));
res.on("end", () => {
if (settled) return;
settled = true;
const responseHeaders = new Headers();
for (const [key, value] of Object.entries(res.headers)) {
if (Array.isArray(value)) {
for (const item of value) responseHeaders.append(key, item);
} else if (value !== undefined) {
responseHeaders.set(key, String(value));
}
}
resolve(
new Response(Buffer.concat(chunks), {
status: res.statusCode ?? 500,
statusText: res.statusMessage,
headers: responseHeaders,
}),
);
});
},
);
req.setTimeout(GATEWAY_REQUEST_TIMEOUT_MS, () => {
req.destroy(new Error(`gateway request timed out after ${GATEWAY_REQUEST_TIMEOUT_MS}ms`));
});
req.on("error", fail);
if (body) req.write(body);
req.end();
});
}
function isTransientGatewayError(error: unknown): boolean {
const code = typeof error === "object" && error !== null
? (error as { code?: unknown }).code
: undefined;
if (
code === "ECONNRESET" ||
code === "ECONNREFUSED" ||
code === "EPIPE" ||
code === "ETIMEDOUT"
) {
return true;
}
const message = error instanceof Error ? error.message : "";
return message.includes("socket hang up") || message.includes("timed out");
}
async function startGateway(): Promise<HostRuntime> {
const root = repoRoot();
const dirs = await ensureAppDirs();
const socketPath = engineSocketPath();
await rm(socketPath, { force: true });
const secret = randomBytes(32).toString("base64url");
const python = pythonExecutable();
const args = [
"-m",
"nanobot",
"desktop-gateway",
"--config",
dirs.configPath,
"--workspace",
dirs.workspacePath,
"--webui-socket",
socketPath,
"--token-issue-secret",
secret,
];
const gateway = spawn(python, args, {
cwd: engineCwd(root),
env: engineEnv(root),
stdio: ["ignore", "pipe", "pipe"],
});
appendGatewayLogs(gateway, dirs.logsDir);
gateway.once("exit", () => scheduleCrashRestart(gateway));
return {
configPath: dirs.configPath,
gateway,
logsDir: dirs.logsDir,
python,
secret,
socketPath,
status: "starting",
workspacePath: dirs.workspacePath,
};
}
async function bootstrapFromGateway(current: HostRuntime): Promise<Record<string, unknown>> {
const response = await fetchGateway(current, "/webui/bootstrap", {
method: "GET",
headers: {
"X-Nanobot-Auth": current.secret,
},
});
if (!response.ok) {
throw new Error(`engine bootstrap failed: HTTP ${response.status}`);
}
return await response.json() as Record<string, unknown>;
}
async function waitForGateway(current: HostRuntime): Promise<void> {
let lastError: unknown;
for (let attempt = 0; attempt < 160; attempt += 1) {
if (current.gateway.exitCode !== null) {
throw new Error(`engine gateway exited with code ${current.gateway.exitCode}`);
}
try {
await bootstrapFromGateway(current);
crashRestartAttempts = 0;
notifyRuntimeStatus("ready");
return;
} catch (error) {
lastError = error;
await new Promise((resolve) => setTimeout(resolve, 100));
}
}
throw lastError instanceof Error ? lastError : new Error("engine gateway did not become ready");
}
async function stopGateway(current: HostRuntime | null): Promise<void> {
if (!current || current.gateway.exitCode !== null) return;
current.status = "stopped";
closeHostSockets();
current.gateway.kill("SIGTERM");
await new Promise<void>((resolve) => {
const timer = setTimeout(() => {
if (current.gateway.exitCode === null) current.gateway.kill("SIGKILL");
resolve();
}, 2500);
current.gateway.once("exit", () => {
clearTimeout(timer);
resolve();
});
});
}
async function startRuntime(): Promise<void> {
notifyRuntimeStatus("starting");
runtime = await startGateway();
await waitForGateway(runtime);
}
async function restartRuntime(): Promise<void> {
const previous = runtime;
notifyRuntimeStatus("restarting");
await stopGateway(previous);
await startRuntime();
}
function scheduleCrashRestart(gateway: ChildProcess): void {
if (runtime?.gateway !== gateway || runtime.status === "restarting" || runtime.status === "stopped") {
return;
}
notifyRuntimeStatus("crashed");
if (crashRestartAttempts >= 3) return;
crashRestartAttempts += 1;
setTimeout(() => {
if (runtime?.gateway !== gateway || runtime.status === "stopped") return;
void startRuntime().catch((error) => {
console.error("failed to restart nanobot engine", error);
notifyRuntimeStatus("crashed");
});
}, 1000);
}
async function proxyToGateway(request: Request): Promise<Response> {
if (!runtime) {
return new Response("Engine unavailable", { status: 503 });
}
const requestUrl = new URL(request.url);
const headers = new Headers(request.headers);
headers.delete("host");
if (requestUrl.pathname === "/webui/bootstrap") {
headers.set("X-Nanobot-Auth", runtime.secret);
}
const init: {
body?: ArrayBuffer;
headers: Headers;
method: string;
} = {
method: request.method,
headers,
};
if (request.method !== "GET" && request.method !== "HEAD") {
init.body = await request.arrayBuffer();
}
let response: Response;
try {
response = await fetchGateway(
runtime,
`${requestUrl.pathname}${requestUrl.search}`,
init,
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`gateway proxy request failed: ${message}`);
return new Response("Engine unavailable", { status: 503 });
}
if (requestUrl.pathname !== "/webui/bootstrap" || !response.ok) {
return response;
}
const body = await response.json() as Record<string, unknown>;
const wsPath = typeof body.ws_path === "string" ? body.ws_path : "/";
const normalizedWsPath = wsPath.startsWith("/") ? wsPath : `/${wsPath}`;
return Response.json({
...body,
ws_url: `nanobot-host://engine${normalizedWsPath}`,
runtime_surface: "native",
});
}
function resolveStaticAsset(webDist: string, requestUrl: string): string | null {
const url = new URL(requestUrl);
const rawPath = decodeURIComponent(url.pathname);
const relativePath = rawPath === "/" ? "index.html" : rawPath.replace(/^\/+/, "");
const resolved = path.resolve(webDist, relativePath);
if (resolved !== webDist && !resolved.startsWith(`${webDist}${path.sep}`)) {
return null;
}
if (existsSync(resolved)) return resolved;
if (!path.extname(relativePath)) return path.join(webDist, "index.html");
return null;
}
function registerAppProtocol(webDist: string, devUrl: string | null): void {
protocol.handle("nanobot-app", async (request) => {
if (!isTrustedAppUrl(request.url)) {
return new Response("Forbidden", { status: 403 });
}
const requestUrl = new URL(request.url);
if (
requestUrl.pathname === "/webui/bootstrap"
|| requestUrl.pathname.startsWith("/api/")
) {
return proxyToGateway(request);
}
if (devUrl) {
const upstream = new URL(
`${requestUrl.pathname}${requestUrl.search}`,
devUrl,
);
const response = await electronNet.fetch(upstream.toString());
return withSecurityHeaders(response, devUrl);
}
const assetPath = resolveStaticAsset(webDist, request.url);
if (!assetPath) {
return new Response("Not Found", { status: 404 });
}
const response = await electronNet.fetch(pathToFileURL(assetPath).toString());
return withSecurityHeaders(response, devUrl);
});
}
function createWindow(): BrowserWindow {
const preload = path.join(app.getAppPath(), "build", "preload.cjs");
const win = new BrowserWindow({
width: 1180,
height: 820,
minWidth: 920,
minHeight: 640,
title: "nanobot",
titleBarStyle: "hiddenInset",
trafficLightPosition: { x: 14, y: 16 },
backgroundColor: process.platform === "darwin" ? "#00000000" : "#ffffff",
transparent: process.platform === "darwin",
...(process.platform === "darwin"
? {
vibrancy: "sidebar" as const,
visualEffectState: "active" as const,
}
: {}),
show: false,
webPreferences: {
preload,
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
webSecurity: true,
},
});
win.once("ready-to-show", () => win.show());
win.on("focus", clearDesktopNotificationBadge);
win.on("close", (event) => {
if (process.platform !== "darwin" || isQuitting) return;
event.preventDefault();
win.hide();
});
win.webContents.setWindowOpenHandler(({ url }) => {
openExternalIfSafe(url);
return { action: "deny" };
});
win.webContents.on("will-navigate", (event, url) => {
if (!isTrustedAppUrl(url)) {
event.preventDefault();
openExternalIfSafe(url);
}
});
win.webContents.on("preload-error", (_event, preloadPath, error) => {
console.error(`Preload failed: ${preloadPath}`, error);
});
win.on("closed", () => {
if (mainWindow === win) mainWindow = null;
closeHostSockets();
});
return win;
}
function runtimeInfo() {
return {
surface: "native" as const,
app_version: app.getVersion(),
engine_status: runtime?.status ?? "stopped",
data_dir: app.getPath("userData"),
logs_dir: runtime?.logsDir ?? userDataPath("logs"),
config_path: runtime?.configPath ?? userDataPath("config.json"),
workspace_path: runtime?.workspacePath ?? userDataPath("workspace"),
python: runtime?.python ?? pythonExecutable(),
engine_transport: "unix_socket",
};
}
function registerIpcHandlers(): void {
handleHostIpc("nanobot:get-runtime-info", () => runtimeInfo());
handleHostIpc("nanobot:restart-engine", async () => {
await restartRuntime();
});
handleHostIpc("nanobot:pick-folder", async () => {
const result = await dialog.showOpenDialog({
properties: ["openDirectory", "createDirectory"],
});
if (result.canceled || !result.filePaths[0]) return null;
return path.resolve(result.filePaths[0]);
});
handleHostIpc("nanobot:open-logs", async () => {
const logsDir = runtime?.logsDir ?? userDataPath("logs");
await mkdir(logsDir, { recursive: true });
const error = await shell.openPath(logsDir);
if (error) throw new Error(error);
});
handleHostIpc("nanobot:export-diagnostics", async () => {
const diagnosticsPath = path.join(
app.getPath("temp"),
`nanobot-diagnostics-${Date.now()}.json`,
);
await writeFile(
diagnosticsPath,
JSON.stringify(runtimeInfo(), null, 2),
"utf8",
);
shell.showItemInFolder(diagnosticsPath);
return diagnosticsPath;
});
handleHostIpc("nanobot:check-for-updates", () => ({
supported: false,
message: "Auto update is not configured for this build.",
}));
handleHostIpc("nanobot:ws-connect", (event, rawUrl) => {
if (!runtime) throw new Error("nanobot engine is not running");
const url = parseHostSocketUrl(rawUrl);
const id = randomBytes(12).toString("hex");
const client = new UnixWebSocketClient(runtime.socketPath, url, {
onOpen: () => sendToRenderer(event.sender, "nanobot:ws-event", { id, type: "open" }),
onMessage: (data) => {
handleDesktopNotificationFrame(data, { getWindow: () => mainWindow });
sendToRenderer(event.sender, "nanobot:ws-event", { id, type: "message", data });
},
onError: (message) => sendToRenderer(event.sender, "nanobot:ws-event", { id, type: "error", message }),
onClose: (code, reason) => {
hostSockets.delete(id);
sendToRenderer(event.sender, "nanobot:ws-event", { id, type: "close", code, reason });
},
});
hostSockets.set(id, client);
client.connect();
event.sender.once("destroyed", () => {
client.close();
hostSockets.delete(id);
});
return id;
});
handleHostIpc("nanobot:ws-send", (_event, id, data) => {
if (typeof id !== "string" || typeof data !== "string") {
throw new Error("Invalid host socket send arguments");
}
const socket = hostSockets.get(id);
if (!socket) throw new Error("Host socket not found");
socket.send(data);
});
handleHostIpc("nanobot:ws-close", (_event, id) => {
if (typeof id !== "string") {
throw new Error("Invalid host socket close argument");
}
hostSockets.get(id)?.close();
hostSockets.delete(id);
});
}
async function loadAppWindow(win: BrowserWindow): Promise<void> {
if (!runtime || runtime.status === "stopped" || runtime.status === "crashed") {
await startRuntime();
}
await win.loadURL("nanobot-app://app/index.html");
}
app.whenReady().then(async () => {
const root = repoRoot();
const webDist = webDistPath(root);
const devUrl = webDevUrl();
if (!devUrl && !existsSync(path.join(webDist, "index.html"))) {
throw new Error(`WebUI dist not found at ${webDist}. Run npm run build:webui first.`);
}
if (devUrl) {
await session.defaultSession.clearCache();
}
registerIpcHandlers();
registerAppProtocol(webDist, devUrl);
mainWindow = createWindow();
await loadAppWindow(mainWindow);
app.on("activate", () => {
if (mainWindow && !mainWindow.isDestroyed()) {
if (!mainWindow.isVisible()) mainWindow.show();
mainWindow.focus();
return;
}
if (BrowserWindow.getAllWindows().length === 0) {
mainWindow = createWindow();
void loadAppWindow(mainWindow);
}
});
}).catch((error) => {
console.error(error);
app.quit();
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit();
});
app.on("before-quit", () => {
isQuitting = true;
if (runtime) runtime.status = "stopped";
if (runtime?.gateway.exitCode === null) {
runtime.gateway.kill("SIGTERM");
}
});
+159
View File
@@ -0,0 +1,159 @@
import {
app,
BrowserWindow,
Notification,
} from "electron";
type NotificationSource = {
kind?: unknown;
label?: unknown;
};
type WsMessageFrame = {
chat_id?: unknown;
event?: unknown;
kind?: unknown;
source?: NotificationSource;
stream_id?: unknown;
text?: unknown;
};
interface DesktopNotifierOptions {
getWindow: () => BrowserWindow | null;
}
const MAX_NOTIFICATION_BODY_LENGTH = 180;
const MAX_NOTIFICATION_TITLE_LENGTH = 80;
let unreadNotificationCount = 0;
const streamTextBuffers = new Map<string, string>();
export function handleDesktopNotificationFrame(
data: string,
options: DesktopNotifierOptions,
): void {
const frame = parseWsMessageFrame(data);
const notificationFrame = frame ? notificationFrameFromWsFrame(frame) : null;
if (!notificationFrame) return;
if (!shouldNotify(options.getWindow())) return;
showDesktopNotification(notificationFrame, options);
}
export function clearDesktopNotificationBadge(): void {
unreadNotificationCount = 0;
app.setBadgeCount(0);
}
function parseWsMessageFrame(data: string): WsMessageFrame | null {
try {
const parsed = JSON.parse(data) as unknown;
return parsed && typeof parsed === "object"
? parsed as WsMessageFrame
: null;
} catch {
return null;
}
}
function isAssistantNotificationFrame(frame: WsMessageFrame): frame is WsMessageFrame & {
chat_id: string;
text: string;
} {
return (
frame.event === "message" &&
typeof frame.chat_id === "string" &&
typeof frame.text === "string" &&
frame.text.trim().length > 0 &&
frame.kind !== "tool_hint" &&
frame.kind !== "progress" &&
frame.kind !== "reasoning"
);
}
function notificationFrameFromWsFrame(frame: WsMessageFrame): WsMessageFrame & {
chat_id: string;
text: string;
} | null {
if (isAssistantNotificationFrame(frame)) return frame;
if (frame.event === "delta") {
if (typeof frame.chat_id === "string" && typeof frame.text === "string") {
const key = streamNotificationKey(frame);
streamTextBuffers.set(key, `${streamTextBuffers.get(key) ?? ""}${frame.text}`);
}
return null;
}
if (frame.event === "stream_end" && typeof frame.chat_id === "string") {
const key = streamNotificationKey(frame);
const text = typeof frame.text === "string"
? frame.text
: streamTextBuffers.get(key) ?? "";
streamTextBuffers.delete(key);
return text.trim().length > 0
? { ...frame, chat_id: frame.chat_id, text }
: null;
}
return null;
}
function streamNotificationKey(frame: WsMessageFrame): string {
const streamId = typeof frame.stream_id === "string" ? frame.stream_id : "";
return `${frame.chat_id ?? ""}\u0000${streamId}`;
}
function shouldNotify(win: BrowserWindow | null): boolean {
if (!Notification.isSupported()) return false;
if (!win || win.isDestroyed()) return false;
return !win.isFocused();
}
function showDesktopNotification(
frame: WsMessageFrame & { chat_id: string; text: string },
options: DesktopNotifierOptions,
): void {
const notification = new Notification({
title: notificationTitle(frame.source),
body: notificationBody(frame.text),
subtitle: "nanobot",
});
notification.on("failed", (_event, error) => {
console.warn(`[nanobot] Desktop notification failed: ${error}`);
});
notification.on("click", () => openChatFromNotification(frame.chat_id, options));
notification.show();
unreadNotificationCount += 1;
app.setBadgeCount(unreadNotificationCount);
}
function notificationTitle(source: NotificationSource | undefined): string {
if (source?.kind === "cron" && typeof source.label === "string") {
const label = source.label.trim();
if (label) return truncateText(label, MAX_NOTIFICATION_TITLE_LENGTH);
}
return "nanobot";
}
function notificationBody(text: string): string {
const compact = text.replace(/\s+/g, " ").trim();
return truncateText(compact, MAX_NOTIFICATION_BODY_LENGTH);
}
function truncateText(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
return `${text.slice(0, maxLength - 3)}...`;
}
function openChatFromNotification(chatId: string, options: DesktopNotifierOptions): void {
const win = options.getWindow();
if (!win || win.isDestroyed()) return;
if (win.isMinimized()) win.restore();
if (!win.isVisible()) win.show();
win.focus();
clearDesktopNotificationBadge();
const sessionKey = `websocket:${chatId}`;
const hash = `#/chat/${encodeURIComponent(sessionKey)}`;
void win.webContents.executeJavaScript(
`window.location.hash = ${JSON.stringify(hash)}`,
true,
).catch(() => {});
}
+55
View File
@@ -0,0 +1,55 @@
import { contextBridge, ipcRenderer } from "electron";
type HostRuntimeInfo = {
surface: "native";
app_version: string;
engine_status: "starting" | "ready" | "restarting" | "stopped" | "crashed";
data_dir: string;
logs_dir: string;
config_path: string;
workspace_path: string;
python: string;
engine_transport?: "unix_socket";
};
type HostSocketEvent =
| { id: string; type: "open" }
| { data: string; id: string; type: "message" }
| { id: string; message: string; type: "error" }
| { code?: number; id: string; reason?: string; type: "close" };
contextBridge.exposeInMainWorld("nanobotHost", {
getRuntimeInfo: (): Promise<HostRuntimeInfo> =>
ipcRenderer.invoke("nanobot:get-runtime-info"),
restartEngine: (): Promise<void> => ipcRenderer.invoke("nanobot:restart-engine"),
pickFolder: (): Promise<string | null> => ipcRenderer.invoke("nanobot:pick-folder"),
openLogs: (): Promise<void> => ipcRenderer.invoke("nanobot:open-logs"),
exportDiagnostics: (): Promise<string> =>
ipcRenderer.invoke("nanobot:export-diagnostics"),
checkForUpdates: (): Promise<{ supported: boolean; message?: string }> =>
ipcRenderer.invoke("nanobot:check-for-updates"),
openSocket: (url: string): Promise<string> =>
ipcRenderer.invoke("nanobot:ws-connect", url),
sendSocket: (id: string, data: string): Promise<void> =>
ipcRenderer.invoke("nanobot:ws-send", id, data),
closeSocket: (id: string): Promise<void> =>
ipcRenderer.invoke("nanobot:ws-close", id),
onSocketEvent: (
listener: (event: HostSocketEvent) => void,
): (() => void) => {
const handler = (_event: Electron.IpcRendererEvent, payload: HostSocketEvent) => {
listener(payload);
};
ipcRenderer.on("nanobot:ws-event", handler);
return () => ipcRenderer.removeListener("nanobot:ws-event", handler);
},
onRuntimeStatus: (
listener: (status: HostRuntimeInfo["engine_status"]) => void,
): (() => void) => {
const handler = (_event: Electron.IpcRendererEvent, status: HostRuntimeInfo["engine_status"]) => {
listener(status);
};
ipcRenderer.on("nanobot:runtime-status", handler);
return () => ipcRenderer.removeListener("nanobot:runtime-status", handler);
},
});
+208
View File
@@ -0,0 +1,208 @@
import net from "node:net";
import { randomBytes } from "node:crypto";
type UnixWebSocketHandlers = {
onClose: (code?: number, reason?: string) => void;
onError: (message: string) => void;
onMessage: (data: string) => void;
onOpen: () => void;
};
const OPCODE_CONTINUATION = 0x0;
const OPCODE_TEXT = 0x1;
const OPCODE_CLOSE = 0x8;
const OPCODE_PING = 0x9;
const OPCODE_PONG = 0xa;
export class UnixWebSocketClient {
private frameBuffer = Buffer.alloc(0);
private handshakeBuffer = Buffer.alloc(0);
private open = false;
private socket: net.Socket | null = null;
private fragmentedText: Buffer[] = [];
constructor(
private readonly socketPath: string,
private readonly url: string,
private readonly handlers: UnixWebSocketHandlers,
) {}
connect(): void {
const socket = net.createConnection(this.socketPath);
this.socket = socket;
socket.once("connect", () => this.writeHandshake());
socket.on("data", (chunk) => this.handleData(chunk));
socket.on("error", (error) => this.fail(error.message));
socket.on("close", () => {
if (this.open) {
this.open = false;
this.handlers.onClose();
}
});
}
send(data: string): void {
if (!this.open || !this.socket || this.socket.destroyed) {
throw new Error("host socket is not open");
}
this.socket.write(encodeFrame(OPCODE_TEXT, Buffer.from(data, "utf8")));
}
close(code = 1000, reason = ""): void {
const socket = this.socket;
if (!socket || socket.destroyed) return;
const reasonBuffer = Buffer.from(reason, "utf8");
const payload = Buffer.alloc(2 + reasonBuffer.length);
payload.writeUInt16BE(code, 0);
reasonBuffer.copy(payload, 2);
socket.write(encodeFrame(OPCODE_CLOSE, payload));
socket.end();
}
private writeHandshake(): void {
const socket = this.socket;
if (!socket) return;
const requestUrl = new URL(this.url);
const path = `${requestUrl.pathname || "/"}${requestUrl.search}`;
const key = randomBytes(16).toString("base64");
socket.write(
[
`GET ${path} HTTP/1.1`,
"Host: nanobot.host",
"Upgrade: websocket",
"Connection: Upgrade",
`Sec-WebSocket-Key: ${key}`,
"Sec-WebSocket-Version: 13",
"\r\n",
].join("\r\n"),
);
}
private handleData(chunk: Buffer): void {
if (!this.open) {
this.handshakeBuffer = Buffer.concat([this.handshakeBuffer, chunk]);
const headerEnd = this.handshakeBuffer.indexOf("\r\n\r\n");
if (headerEnd === -1) return;
const header = this.handshakeBuffer.subarray(0, headerEnd).toString("utf8");
const remainder = this.handshakeBuffer.subarray(headerEnd + 4);
this.handshakeBuffer = Buffer.alloc(0);
if (!header.startsWith("HTTP/1.1 101")) {
this.fail(`host socket upgrade failed: ${header.split("\r\n")[0]}`);
return;
}
this.open = true;
this.handlers.onOpen();
if (remainder.length > 0) this.handleFrames(remainder);
return;
}
this.handleFrames(chunk);
}
private handleFrames(chunk: Buffer): void {
this.frameBuffer = Buffer.concat([this.frameBuffer, chunk]);
while (this.frameBuffer.length >= 2) {
const first = this.frameBuffer[0];
const second = this.frameBuffer[1];
const fin = (first & 0x80) !== 0;
const opcode = first & 0x0f;
const masked = (second & 0x80) !== 0;
let length = second & 0x7f;
let offset = 2;
if (length === 126) {
if (this.frameBuffer.length < offset + 2) return;
length = this.frameBuffer.readUInt16BE(offset);
offset += 2;
} else if (length === 127) {
if (this.frameBuffer.length < offset + 8) return;
const bigLength = this.frameBuffer.readBigUInt64BE(offset);
if (bigLength > BigInt(Number.MAX_SAFE_INTEGER)) {
this.fail("host socket frame is too large");
return;
}
length = Number(bigLength);
offset += 8;
}
let mask: Buffer | null = null;
if (masked) {
if (this.frameBuffer.length < offset + 4) return;
mask = this.frameBuffer.subarray(offset, offset + 4);
offset += 4;
}
if (this.frameBuffer.length < offset + length) return;
const rawPayload = Buffer.from(this.frameBuffer.subarray(offset, offset + length));
this.frameBuffer = this.frameBuffer.subarray(offset + length);
const payload = mask ? unmask(rawPayload, mask) : rawPayload;
if (opcode === OPCODE_TEXT || opcode === OPCODE_CONTINUATION) {
this.handleTextFrame(opcode, payload, fin);
} else if (opcode === OPCODE_PING) {
this.socket?.write(encodeFrame(OPCODE_PONG, payload));
} else if (opcode === OPCODE_CLOSE) {
const code = payload.length >= 2 ? payload.readUInt16BE(0) : undefined;
const reason = payload.length > 2 ? payload.subarray(2).toString("utf8") : undefined;
this.open = false;
this.socket?.end();
this.handlers.onClose(code, reason);
return;
}
}
}
private handleTextFrame(opcode: number, payload: Buffer, fin: boolean): void {
if (opcode === OPCODE_TEXT && fin) {
this.handlers.onMessage(payload.toString("utf8"));
return;
}
if (opcode === OPCODE_TEXT) {
this.fragmentedText = [payload];
return;
}
if (this.fragmentedText.length === 0) return;
this.fragmentedText.push(payload);
if (fin) {
const data = Buffer.concat(this.fragmentedText).toString("utf8");
this.fragmentedText = [];
this.handlers.onMessage(data);
}
}
private fail(message: string): void {
this.handlers.onError(message);
this.socket?.destroy();
}
}
function encodeFrame(opcode: number, payload: Buffer): Buffer {
const length = payload.length;
const headerLength = length < 126 ? 2 : length <= 0xffff ? 4 : 10;
const header = Buffer.alloc(headerLength + 4);
header[0] = 0x80 | opcode;
if (length < 126) {
header[1] = 0x80 | length;
} else if (length <= 0xffff) {
header[1] = 0x80 | 126;
header.writeUInt16BE(length, 2);
} else {
header[1] = 0x80 | 127;
header.writeBigUInt64BE(BigInt(length), 2);
}
const maskOffset = headerLength;
const mask = randomBytes(4);
mask.copy(header, maskOffset);
const masked = Buffer.alloc(payload.length);
for (let i = 0; i < payload.length; i += 1) {
masked[i] = payload[i] ^ mask[i % 4];
}
return Buffer.concat([header, masked]);
}
function unmask(payload: Buffer, mask: Buffer): Buffer {
const out = Buffer.alloc(payload.length);
for (let i = 0; i < payload.length; i += 1) {
out[i] = payload[i] ^ mask[i % 4];
}
return out;
}
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022", "DOM"],
"types": ["node", "electron"],
"strict": true,
"skipLibCheck": true,
"noEmitOnError": true,
"outDir": "build",
"rootDir": "src",
"sourceMap": true
},
"include": ["src/**/*"]
}