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:
@@ -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)
|
||||
@@ -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=="],
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
@@ -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(() => {});
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user