diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34715c3..108e73a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,12 +12,19 @@ jobs: steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "24" + - uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Install dependencies - run: bun install + run: bun install --frozen-lockfile --ignore-scripts + + - name: Install desktop type dependencies + run: bun install --cwd desktop --frozen-lockfile --ignore-scripts - name: Run linter run: bun run lint:all @@ -26,7 +33,7 @@ jobs: run: bun run typecheck - name: Run tests - run: bun test + run: bun run test - name: Build - run: bun run build \ No newline at end of file + run: bun run build diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml new file mode 100644 index 0000000..611a927 --- /dev/null +++ b/.github/workflows/desktop.yml @@ -0,0 +1,174 @@ +name: Desktop + +on: + workflow_dispatch: + pull_request: + paths: + - .github/workflows/desktop.yml + - desktop/** + - src/** + - tests/** + - package.json + - bun.lock + - tsconfig.json + - eslint.config.js + push: + branches: [main, master] + paths: + - .github/workflows/desktop.yml + - desktop/** + - src/** + - tests/** + - package.json + - bun.lock + - tsconfig.json + - eslint.config.js + +permissions: + contents: read + +concurrency: + group: desktop-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + native: + name: Native smoke on ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [macos-15, windows-2022, ubuntu-24.04] + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + defaults: + run: + shell: bash + env: + CI: "true" + CSC_IDENTITY_AUTO_DISCOVERY: "false" + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + + - name: Put caches and build output in runner temporary storage + run: | + node --input-type=module <<'NODE' + import fs from "node:fs" + import path from "node:path" + const runnerTemp = process.env.RUNNER_TEMP + const root = path.join(runnerTemp, "copilot-bridge") + const environment = { + BUN_INSTALL_CACHE_DIR: path.join(root, "cache", "bun"), + ELECTRON_CACHE: path.join(root, "cache", "electron"), + ELECTRON_BUILDER_CACHE: path.join(root, "cache", "electron-builder"), + XDG_CACHE_HOME: path.join(root, "cache", "xdg"), + npm_config_cache: path.join(root, "cache", "npm"), + TMPDIR: runnerTemp, + TMP: runnerTemp, + TEMP: runnerTemp, + COPILOT_BRIDGE_PACKAGE_OUTPUT: path.join(root, "package"), + } + const envLines = Object.entries(environment).map(([name, value]) => `${name}=${value}`) + fs.appendFileSync(process.env.GITHUB_ENV, envLines.join("\n") + "\n") + const output = path.join(root, "dist") + fs.mkdirSync(output, { recursive: true }) + fs.symlinkSync(output, "desktop/dist", process.platform === "win32" ? "junction" : "dir") + NODE + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.3.12" + + - name: Install root dependencies without Git hooks + run: bun install --frozen-lockfile --ignore-scripts + + - name: Install desktop dependencies + working-directory: desktop + run: bun install --frozen-lockfile --ignore-scripts + + - name: Install Electron binary + run: node desktop/node_modules/electron/install.js + + - name: Install Linux native dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + dbus-x11 libasound2t64 libatk1.0-0t64 libatk-bridge2.0-0t64 \ + libcups2t64 libdrm2 libgbm1 libgtk-3-0t64 libnss3 \ + libx11-xcb1 libxcomposite1 libxdamage1 libxkbcommon0 libxrandr2 \ + libxss1 xauth xvfb + + - name: Lint desktop files + run: bun run lint --no-cache desktop + + - name: Typecheck proxy + run: bun run typecheck + + - name: Test proxy + run: bun test ./tests + + - name: Typecheck desktop + run: bun run desktop:typecheck + + - name: Test desktop + run: bun run desktop:test + + - name: Build native code and renderer + run: bun run desktop:build + + - name: Smoke test native app + if: runner.os != 'Linux' + env: + COPILOT_BRIDGE_SMOKE_OUTPUT: ${{ runner.temp }}/copilot-bridge/evidence/native + run: bun run desktop:smoke -- --default-fixture-profile + + - name: Smoke test native app on Linux + if: runner.os == 'Linux' + env: + COPILOT_BRIDGE_SMOKE_OUTPUT: ${{ runner.temp }}/copilot-bridge/evidence/native + run: dbus-run-session -- xvfb-run -a bun run desktop:smoke -- --default-fixture-profile + + - name: Package host architecture and smoke test installed executable + if: runner.os != 'Linux' + env: + COPILOT_BRIDGE_SMOKE_OUTPUT: ${{ runner.temp }}/copilot-bridge/evidence/packaged + run: node desktop/scripts/package-smoke.ts + + - name: Package host architecture and smoke test installed executable on Linux + if: runner.os == 'Linux' + env: + COPILOT_BRIDGE_SMOKE_OUTPUT: ${{ runner.temp }}/copilot-bridge/evidence/packaged + run: dbus-run-session -- xvfb-run -a node desktop/scripts/package-smoke.ts + + - name: Archive unsigned self-contained app + run: | + node --input-type=module <<'NODE' + import { execFileSync } from "node:child_process" + import path from "node:path" + const archive = path.join(process.env.RUNNER_TEMP, "copilot-bridge", "Copilot-Bridge-unsigned.tar.gz") + const tar = process.platform === "win32" + ? path.join(process.env.SystemRoot, "System32", "tar.exe") + : "tar" + execFileSync(tar, ["-czf", archive, "-C", process.env.COPILOT_BRIDGE_PACKAGE_OUTPUT, "."], { stdio: "inherit" }) + NODE + + - name: Upload native evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: copilot-bridge-${{ runner.os }}-${{ runner.arch }}-evidence + path: ${{ runner.temp }}/copilot-bridge/evidence + if-no-files-found: warn + retention-days: 14 + + - name: Upload unsigned app + uses: actions/upload-artifact@v4 + with: + name: copilot-bridge-${{ runner.os }}-${{ runner.arch }}-unsigned + path: ${{ runner.temp }}/copilot-bridge/Copilot-Bridge-unsigned.tar.gz + if-no-files-found: error + retention-days: 14 diff --git a/.gitignore b/.gitignore index e71e3a6..04cddd6 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,9 @@ node_modules/ # build output dist/ +/node_modules +/dist +/desktop/node_modules +/desktop/dist +/desktop/release .gstack/ diff --git a/README.md b/README.md index 4e18e90..2a87208 100644 --- a/README.md +++ b/README.md @@ -497,3 +497,7 @@ bun run start - `--rate-limit `: Enforces a minimum time interval between requests. For example, `copilot-proxy-api start --rate-limit 30` will ensure there's at least a 30-second gap between requests. - `--wait`: Use this with `--rate-limit`. It makes the server wait for the cooldown period to end instead of rejecting the request with an error. This is useful for clients that don't automatically retry on rate limit errors. - If you have a GitHub business or enterprise plan account with Copilot, use the `--account-type` flag (e.g., `--account-type business`). See the [official documentation](https://docs.github.com/en/enterprise-cloud@latest/copilot/managing-copilot/managing-github-copilot-in-your-organization/managing-access-to-github-copilot-in-your-organization/managing-github-copilot-access-to-your-organizations-network#configuring-copilot-subscription-based-network-routing-for-your-enterprise-or-organization) for more details. + +## Desktop app + +[Copilot Bridge](desktop/README.md) is the separately versioned desktop companion for macOS, Windows, and Linux. It provides GitHub sign-in, a keyed local gateway, models, usage, activity, and setup commands for coding agents. The desktop app starts at version 0.0.1. diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..98123b5 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,2 @@ +[test] +root = "./tests" diff --git a/desktop/ARCHITECTURE.md b/desktop/ARCHITECTURE.md new file mode 100644 index 0000000..b37015c --- /dev/null +++ b/desktop/ARCHITECTURE.md @@ -0,0 +1,40 @@ +# Desktop architecture + +Copilot Bridge gives a Copilot subscriber a local gateway and agent setup in one desktop app. The sandboxed renderer presents safe snapshots. Electron main owns settings, GitHub device login, encrypted persistence, clipboard actions, and the lifetime of one proxy child. That child owns the existing process-wide proxy state and protocol handlers. + +The app bundles Electron's Node runtime. Users do not install Bun or Node. The desktop package starts at 0.0.1 and does not change the CLI's release history. + +## Runtime choice + +Two independent designs compared running Hono inside Electron main with running it in a dedicated child. The child design preserves the current route and service signatures, contains CPU work and failures, and makes a stopped process a reliable boundary for account state. The embedded design required a broad state refactor through the existing CLI and handlers. An independent reviewer selected the child design. + +The child runs a desktop-specific HTTP composition around the existing inference routes. It binds to 127.0.0.1, requires a generated local key, rejects browser origins, and never mounts the CLI token or debug routes. It uses the Hono Node adapter because a direct Node experiment found the current srvx adapter can treat normal POST upload completion as cancellation. + +## Ownership + +| Area | Owner | +| --- | --- | +| Safe commands and snapshots | `shared/contracts.ts` | +| Agent setup text | `shared/connections.ts` | +| Account and desired gateway state | `main/controller.ts` | +| GitHub device flow | Native account code | +| Persistent secrets | Native vault using Electron safeStorage | +| Proxy singleton and response streams | Dedicated child | +| Native window, IPC, tray, and quit | Electron bootstrap | +| Presentation and local filters | React renderer | + +No renderer API accepts an arbitrary URL, file path, executable, or IPC channel. Upstream credentials do not enter snapshots. Local-key copies and complete agent configurations go directly from native code to the clipboard. IPC checks the originating window and frame, and validates commands at the controller boundary. + +## Lifecycle + +Start and stop converge to one desired state. Login cancellation and sign-out invalidate old asynchronous work before cleanup. Stop closes the listener and drains active work for a bounded period, then terminates the child if needed. A replacement starts only after the previous child exits. Parent disconnection also ends the child. Quit waits for cleanup. + +Settings and credentials have one writer. Credentials persist only when OS-backed encryption is available. Linux basic-text storage is treated as unavailable. Otherwise the app explicitly runs in session-only mode. Existing CLI token files and agent configuration files remain under their current owners. + +Activity is a bounded session history of completed-request metadata. The existing stream observer supplies final timing and token counts. It does not record prompts, generated text, tool arguments, headers, or raw upstream errors. This release adds no history database or telemetry. + +## Verification + +The [feature map](FEATURES.md) defines observable workflows. Local fixtures replace only upstream services; the renderer, native bridge, child, Hono routes, and loopback requests still run. Browser checks use gstack browse. Native smoke checks launch Electron and verify the actual window and listener. Packaged checks cover dependency assets and shutdown. Tests on this Mac establish macOS behavior. Windows and Linux require their native CI jobs before a cross-platform release claim. + +The decision trail and architecture candidates for this development run are under the project's mac-ssd `desktop-app` directory. diff --git a/desktop/FEATURES.md b/desktop/FEATURES.md new file mode 100644 index 0000000..1fce153 --- /dev/null +++ b/desktop/FEATURES.md @@ -0,0 +1,20 @@ +# Desktop feature map + +The desktop app has its own version, starting at 0.0.1. The existing CLI keeps its release history. + +| Feature | Expected behavior | Verification | +| --- | --- | --- | +| GitHub sign-in | Show a device code, open GitHub, obey polling limits, and handle approval, denial, expiry, cancellation, and network errors. | Local upstream fixtures and the rendered sign-in flow. Live sign-in is recorded separately. | +| Account storage | Keep credentials in the native process. Persist only encrypted credentials. Use a visible session-only mode when secure storage is unavailable. | Native storage tests and inspection of test data and renderer snapshots. | +| Local gateway | Start one authenticated listener at 127.0.0.1. Stop releases its port. Repeated actions, failed binds, sign-out, and quit leave no orphan listener. | Real local HTTP, a controlled child process, and the native UI. | +| Protocols | Preserve OpenAI Chat Completions, OpenAI Responses, Anthropic Messages, and their supported streaming behavior. | Existing proxy tests plus real HTTP through the desktop-owned Node runtime. | +| Models | Show the actual Copilot catalog, supported features, and model limits. Search and select a model for agent setup and test requests. | Fixture catalog in the UI and live discovery when credentials are available. | +| Usage | Display account quotas from GitHub. Distinguish unlimited, unavailable, and zero remaining. Keep fetch timestamps visible. | Controlled quota responses and UI refresh. | +| Agent setup | Preview and copy Claude Code, Codex, OpenCode, and HTTP configurations with POSIX or PowerShell setup. Copy the local key without exposing upstream credentials. | UI copy actions and requests using the resulting endpoint and key. Existing agent files are never overwritten. | +| Test request | Send the selected model a real request through the local gateway and show its response or an actionable error. | Controlled upstream response, completed activity, and live Copilot execution recorded separately. | +| Activity | Show bounded session metadata after streams finish. Distinguish failures and cancellations. Store no prompt, generated text, headers, or upstream error bodies in history. | JSON and streaming requests, cancellation, clearing, and redaction checks. | +| Settings | Validate ports and persist appearance, preferred model, start behavior, account type, and shell choice. Apply lifecycle changes through the native owner. | UI save, invalid-input checks, and app restart. | +| Desktop lifecycle | Offer native tray actions, a single app instance, safe close behavior, and complete quit cleanup. | Actual Electron launches. Platform-specific behavior requires the matching OS. | +| Packaging | Bundle the runtime for macOS, Windows, and Linux. Users need neither Node nor Bun. Include PDF dependencies and worker assets. | Local macOS launch and package checks. Native Windows/Linux CI must be distinguished from checks run on this Mac. | + +The first release supports one Copilot account. Other providers, account rotation, automatic edits to agent files, cloud sync, signing, publishing, and deployment are outside this development scope. diff --git a/desktop/README.md b/desktop/README.md new file mode 100644 index 0000000..e9b607d --- /dev/null +++ b/desktop/README.md @@ -0,0 +1,99 @@ +# Copilot Bridge + +Copilot Bridge is a desktop app for one GitHub Copilot account. It runs a local gateway and prepares terminal commands for Claude Code, Codex, OpenCode, and HTTP clients. The installed app includes Electron and Node. Users do not need Bun or Node. + +The desktop version is **0.0.1 build 1**. The CLI keeps its separate **0.10.32** release history. + +The app has five views. + +| View | Behavior | +| -------- | ------------------------------------------------------------------------------------------------ | +| Overview | Sign in, start or stop the gateway, inspect usage, and see recent requests. | +| Models | Search the Copilot catalog, inspect model capabilities, select a model, and send a test request. | +| Agents | Copy setup commands for a compatible model and either POSIX shells or PowerShell. | +| Activity | Filter or clear session request metadata, including timing, tokens, and outcomes. | +| Settings | Change the port, account type, appearance, preferred model, shell, and desktop behavior. | + +## Sign in and connect an agent + +1. Select **Sign in with GitHub** and copy the device code. +2. Open GitHub from the app and approve the code at `https://github.com/login/device`. +3. Check that the gateway runs in **Overview**. If it is stopped, select **Start gateway**. +4. Select a model in **Models**, then choose your agent and shell in **Agents**. +5. Copy the setup commands and run them in your terminal. + +The gateway binds to `127.0.0.1` and requires a generated local key. Copied setup commands contain that key. GitHub and Copilot tokens remain in the native process. The app does not overwrite your agent configuration files. Codex setup uses per-command configuration flags and requires a model that supports the Responses API. + +The app persists credentials only with OS-backed encryption. On Linux without a supported secret store, sign-in lasts for the current session. Tray support depends on the Linux desktop environment. Activity contains request metadata, not prompts, generated text, tool arguments, or headers. + +## Develop the app + +Development requires **Bun 1.3.12 or newer** and **Node 24 or newer**. Run these commands from the repository root. + +On this Mac, first configure `desktop/dist`, `desktop/node_modules`, and tool caches under the verified mac-ssd project root in [AGENTS.md](../AGENTS.md). `ai-storage path` resolves the project directory. Keep temporary files there with `TMPDIR`. + +```sh +bun install --frozen-lockfile --ignore-scripts +bun install --cwd desktop --frozen-lockfile --ignore-scripts +node desktop/node_modules/electron/install.js +bun run desktop:dev +``` + +`desktop:dev` builds and launches the native app. It uses the real GitHub device flow unless `--fixture` is supplied. + +```sh +bun run desktop:dev -- --fixture +``` + +The fixture browser preview runs the renderer, native controller, proxy child, and controlled upstream services. It prints its local URL, normally `http://127.0.0.1:5174`. + +```sh +bun run desktop:build +bun run desktop:preview +``` + +The preview and native fixture mode use isolated temporary profiles and test credentials. They need no GitHub approval. They do not prove live Copilot authentication or inference. + +`COPILOT_BRIDGE_PREVIEW_DATA` sets the preview's data directory. `ELECTRON_CACHE` and `ELECTRON_BUILDER_CACHE` set the Electron cache directories. + +## Verify the app + +The [feature map](FEATURES.md) defines the expected workflows. Root and desktop tests run separately because each suite owns its global fixtures. + +```sh +bun run typecheck +bun test ./tests +bun run desktop:typecheck +bun run desktop:test +bun run desktop:build +bun run desktop:smoke +node desktop/scripts/package-smoke.ts +``` + +The optional live check reuses the existing CLI credential in memory and sends one short Copilot request. It does not import that credential into the app or change the CLI sign-in. + +```sh +node desktop/dist/live-check.cjs --use-cli-sign-in +``` + +The native smoke launches the Electron window, exercises fixture UI workflows, makes local gateway requests, and checks shutdown. The package smoke builds an unsigned app directory for the host architecture and runs the same workflows through its executable. Each smoke run reports its evidence directory. `smoke.json`, screenshots, and `electron.log` record the result. + +`COPILOT_BRIDGE_PACKAGE_OUTPUT` sets the package directory. `COPILOT_BRIDGE_SMOKE_OUTPUT` sets the smoke evidence directory. Without these variables, the scripts use temporary storage. The package smoke does not copy artifacts into the source tree. On Linux, run the two smoke commands inside `xvfb-run -a` when no graphical session is available. + +The [desktop workflow](../.github/workflows/desktop.yml) runs native jobs on macOS, Windows, and Linux. It uploads fixture evidence and unsigned self-contained app archives. Each job builds only its host architecture. Signing and publishing are disabled. + +Local validation covers 13 macOS UI workflows in the development and packaged apps, encrypted-vault filesystem tests, shell-command tests, and the proxy and desktop suites. A live Copilot request through the controller and local gateway also passed using an existing CLI sign-in in memory. New GitHub device consent requires a user. Native Windows and Linux validation runs in CI. + +## Package an app + +After `bun run desktop:build`, `bun run desktop:package` uses [electron-builder.config.mjs](electron-builder.config.mjs). Pass `--x64` or `--arm64` to restrict the build to the host architecture and match the installed native PDF dependencies. + +| OS | Configured artifact targets | +| ------- | ----------------------------------- | +| macOS | DMG and ZIP for x64 and arm64. | +| Windows | NSIS installer for x64. | +| Linux | AppImage and DEB for x64 and arm64. | + +These are configured targets. The CI package smoke verifies unpacked app directories and does not create or validate installers. No signed or published release is included. + +The [architecture guide](ARCHITECTURE.md) describes the native process, sandboxed renderer, encrypted vault, and proxy child. This release has no other providers, account rotation, automatic edits to agent files, cloud sync, or telemetry. diff --git a/desktop/assets/icon.png b/desktop/assets/icon.png new file mode 100644 index 0000000..8a96d14 Binary files /dev/null and b/desktop/assets/icon.png differ diff --git a/desktop/assets/trayTemplate.png b/desktop/assets/trayTemplate.png new file mode 100644 index 0000000..6e1f275 Binary files /dev/null and b/desktop/assets/trayTemplate.png differ diff --git a/desktop/bun.lock b/desktop/bun.lock new file mode 100644 index 0000000..ebaf025 --- /dev/null +++ b/desktop/bun.lock @@ -0,0 +1,778 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "copilot-bridge", + "dependencies": { + "@hono/node-server": "^1.19.11", + "lucide-react": "^1.64.0", + "pdf-parse": "^2.4.5", + "react": "^19.3.1", + "react-dom": "^19.3.1", + "zod": "^4.3.6", + }, + "devDependencies": { + "@types/bun": "^1.3.11", + "@types/node": "^24.0.0", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^6.1.2", + "electron": "45.1.0", + "electron-builder": "^26.18.0", + "esbuild": "^0.28.2", + "typescript": "^5.9.3", + "vite": "^8.3.5", + }, + }, + }, + "packages": { + "@electron-internal/extract-zip": ["@electron-internal/extract-zip@1.0.8", "", {}, "sha512-8iajeLFbzNwM0m3AsQZSZgYREWglAImyGvvFSWU34FgMW8sHlzdwj9QKvv9dgN7jMP591EFUF/sLOC3Jl9YIjQ=="], + + "@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.1.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-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA=="], + + "@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.2.1", "", { "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-IS4pG3BysRrCLJ8xTAfvg+IQDbiUoAzdJy2SfjcYKnLDy5DpCdjnPdhS5Zf5UMfrkB24xu0KC+TqcOxg+eG+6w=="], + + "@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=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="], + + "@hono/node-server": ["@hono/node-server@1.19.17", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ=="], + + "@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=="], + + "@napi-rs/canvas": ["@napi-rs/canvas@0.1.80", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "0.1.80", "@napi-rs/canvas-darwin-arm64": "0.1.80", "@napi-rs/canvas-darwin-x64": "0.1.80", "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80", "@napi-rs/canvas-linux-arm64-gnu": "0.1.80", "@napi-rs/canvas-linux-arm64-musl": "0.1.80", "@napi-rs/canvas-linux-riscv64-gnu": "0.1.80", "@napi-rs/canvas-linux-x64-gnu": "0.1.80", "@napi-rs/canvas-linux-x64-musl": "0.1.80", "@napi-rs/canvas-win32-x64-msvc": "0.1.80" } }, "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww=="], + + "@napi-rs/canvas-android-arm64": ["@napi-rs/canvas-android-arm64@0.1.80", "", { "os": "android", "cpu": "arm64" }, "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ=="], + + "@napi-rs/canvas-darwin-arm64": ["@napi-rs/canvas-darwin-arm64@0.1.80", "", { "os": "darwin", "cpu": "arm64" }, "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ=="], + + "@napi-rs/canvas-darwin-x64": ["@napi-rs/canvas-darwin-x64@0.1.80", "", { "os": "darwin", "cpu": "x64" }, "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA=="], + + "@napi-rs/canvas-linux-arm-gnueabihf": ["@napi-rs/canvas-linux-arm-gnueabihf@0.1.80", "", { "os": "linux", "cpu": "arm" }, "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ=="], + + "@napi-rs/canvas-linux-arm64-gnu": ["@napi-rs/canvas-linux-arm64-gnu@0.1.80", "", { "os": "linux", "cpu": "arm64" }, "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw=="], + + "@napi-rs/canvas-linux-arm64-musl": ["@napi-rs/canvas-linux-arm64-musl@0.1.80", "", { "os": "linux", "cpu": "arm64" }, "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg=="], + + "@napi-rs/canvas-linux-riscv64-gnu": ["@napi-rs/canvas-linux-riscv64-gnu@0.1.80", "", { "os": "linux", "cpu": "none" }, "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw=="], + + "@napi-rs/canvas-linux-x64-gnu": ["@napi-rs/canvas-linux-x64-gnu@0.1.80", "", { "os": "linux", "cpu": "x64" }, "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA=="], + + "@napi-rs/canvas-linux-x64-musl": ["@napi-rs/canvas-linux-x64-musl@0.1.80", "", { "os": "linux", "cpu": "x64" }, "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg=="], + + "@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@0.1.80", "", { "os": "win32", "cpu": "x64" }, "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg=="], + + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@oxc-project/types": ["@oxc-project/types@0.156.0", "", {}, "sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw=="], + + "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.11.1", "", { "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-jaU9OaCcbZUdyXx9hMETZB9xoNmxTGjOGFqd3cOwKFkpKnyKd9WxF3uqj4PvX4cyOMP9bSdHg5YyML2qtx/d/g=="], + + "@peculiar/json-schema": ["@peculiar/json-schema@1.1.12", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w=="], + + "@peculiar/utils": ["@peculiar/utils@2.0.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ=="], + + "@peculiar/webcrypto": ["@peculiar/webcrypto@1.7.2", "", { "dependencies": { "@peculiar/asn1-schema": "^2.7.0", "@peculiar/json-schema": "^1.1.12", "@peculiar/utils": "^2.0.2", "tslib": "^2.8.1", "webcrypto-core": "^1.9.2" } }, "sha512-ktTSACRJ5WdpfjHjFT2EWJQQPacGv1cPCDfEbdmpAHOxdFiYqPe4lfnXGA3D8Dg9/0kHPYSvyRlgujIOVlTdvQ=="], + + "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.16", "", { "os": "android", "cpu": "arm" }, "sha512-ia3DwAs0OsMGS7+IConptSiyk9pEXkOx/AbJ2n/lAGxH3QkWQus1t8d7gnAT38FCU2sQu8lUbeR6AiZFv4JxaA=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.16", "", { "os": "android", "cpu": "arm64" }, "sha512-kx5wBiMB6E7TEvpl2B9gjLU004BkvEm2Dr941BFjx/SontPwSzNuQUIoVKxHHoSjpFIfHMmcguyLOtyn0hObRg=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qslZrXnKJU4cUpjoAmtAo/KQM9e2IRMlmpig6Zpwj+8KWxW8v+nkAf9216Bsek9lOC1jvg2yCjygCFo2f83msQ=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-FdF+y48ND13eAyM2mQn+85kvAqNlvRWROy2rhgA463X/0Gv4vGtazZtfaF1mRBLscYvtOlh4QnLjOWiFzX69OQ=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.16", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ddjqFM5e95AgLbUPtKIS7V1KTVlA0RUgCVr5mn+/IEhsdy3pr+Vuj/HZ7nlMN2FyxIwUktOH1LEPIQXO1v9y1Q=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.16", "", { "os": "linux", "cpu": "arm" }, "sha512-tHdKWbFkFBI687iim3LIyIvyL7j66VvQYkDPss5Stn/qyE2tWkR1YD0dXmXPFKKqT/o5ZEJVsaRsWS2meQwGOA=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-FJiLGAblPBMlBEdbOqw6kW6QcXDWAf/oJu7gYs9ZJZwmg+K+/zjFLxPTIY9izJUzxzc24FHvmseyA0IvJSl17w=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-hrJpblMsrQ58U9veWpbXzkhV5zStunds6HWLprqq1qauvgBr1E6P+24kLj6lSLI9vjRn/RYpjprhkHQPKGWpNg=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.16", "", { "os": "linux", "cpu": "ppc64" }, "sha512-qJd/dC6j6CwLG9/s/RP2nb1RZ0GIgj7Ioj7x/SYa3RzjVrlzrmH0fhIOpfvqQDeIsdP0loi+kHaMhJzY1PBIaA=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.16", "", { "os": "linux", "cpu": "s390x" }, "sha512-Vx7Xg5uUJuNcujJjI70OICaH0EYj80BtOLZTSN7uOYg8QOLlyKnaG0IW7Fn/J0IpJsaKzmDKMhOQv1JBM9m9vg=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.16", "", { "os": "linux", "cpu": "x64" }, "sha512-TNKhgE3uJTsTzatUSSYt0Kgo+Oa2uSlSjz4r1aEIea7HX6ULBym4fAlYUhG1i6pjNf2njfCUAfKwr6C3E9Xg1w=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.16", "", { "os": "linux", "cpu": "x64" }, "sha512-7chXdpjXZ8qG0+mIxECN3cT9kvhZ3/oZhMX6Pp876P7CDrIy8nJswva6Mq357JR7eYXfuskwZxMeV1SzoZdHrA=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.16", "", { "os": "none", "cpu": "arm64" }, "sha512-Kwjt+7uNTJK5/MDkGWGVO2KQPnj8yAV75dMpfwyHZrJSXWHrniOB0aUEWlVYf3H1hOqFLIStLdM5QXq6bnKmOQ=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-/LH6WfjDRXXJz/p5xl4TmU+jpcnwcxbKEQBW/GZE0JHfN0lGIyTtsUEGaQ/q/GikkGJ2yLKe6pwm7AnEWO8pdw=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.16", "", { "os": "win32", "cpu": "x64" }, "sha512-1lv1KvX0Z871zfk7vw+dnXMMnHNyDwMzWG8Kv3HxDXMOhzCX4SHc7HK319fInP0i+0p9D8brkfzL7pJOaGl+tw=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + + "@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/bun": ["@types/bun@1.4.4", "", { "dependencies": { "bun-types": "1.4.4" } }, "sha512-9wkRDXUXK124pEnAutE6n/l18j+i9GC5IAHK8TmX/2nlySETmky0OU6yL5U8pW1AoKh++AN9auDCr647BfurjQ=="], + + "@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@24.19.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ=="], + + "@types/react": ["@types/react@19.3.0", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="], + + "@types/react-dom": ["@types/react-dom@19.3.0", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="], + + "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.1.2", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "oxc-transform-react": "^0.152.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler", "oxc-transform-react"] }, "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw=="], + + "@xmldom/xmldom": ["@xmldom/xmldom@0.8.15", "", {}, "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA=="], + + "abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "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-lib": ["app-builder-lib@26.18.0", "", { "dependencies": { "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", "@electron/rebuild": "^4.2.1", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@noble/hashes": "^1.8.0", "@peculiar/webcrypto": "^1.7.1", "@types/fs-extra": "9.0.13", "ajv": "^8.18.0", "asn1js": "^3.0.10", "async-exit-hook": "^2.0.1", "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "26.18.0", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.2.5", "pkijs": "^3.4.0", "plist": "3.1.0", "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "unzipper": "^0.12.3", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.18.0", "electron-builder-squirrel-windows": "26.18.0" } }, "sha512-lx+KkuCSZHcbECf4R3IAcC9VOFk0+cMUwljO63s5y8yqS1Lw0sRGmjPaWsPwP3zv3ohZYbMhUvjXeiQXXZhJXA=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "asn1js": ["asn1js@3.1.0", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.2.0", "tslib": "^2.8.1" } }, "sha512-HWYi1THkOqguCiO+vwj09ZkQFD2kKEsejYHAYCtX0D80WVdoDnvRc9IukXX/jeb0T9NRWJnRtqwdbHltnFjwXA=="], + + "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=="], + + "aws4": ["aws4@1.13.2", "", {}, "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="], + + "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], + + "brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "builder-util": ["builder-util@26.16.0", "", { "dependencies": { "@types/debug": "^4.1.6", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "cross-spawn": "^7.0.6", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "js-yaml": "^4.1.0", "sanitize-filename": "^1.6.3", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0" } }, "sha512-RLyJhB7Si3YkzKR9ubQslWuXW3Vhs3CGe1i+SeixBZ0qTd1mk3XBmssvY22TlB6CS5blyko8Gu1JzpYk8UkYAg=="], + + "builder-util-runtime": ["builder-util-runtime@9.7.0", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw=="], + + "bun-types": ["bun-types@1.4.4", "", { "dependencies": { "@types/node": "*", "undici-types": "*" } }, "sha512-yjrJh5oGX7mMPKGd19YXyIjmEuj4G4zNC30bs7PM9Az7NQ3ndF3j6hbeRdvJTRQEhJ/FYQwW0a2moweEi2hMxQ=="], + + "bytestreamjs": ["bytestreamjs@2.0.1", "", {}, "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ=="], + + "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=="], + + "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.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "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=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "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-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "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.18.0", "", { "dependencies": { "app-builder-lib": "26.18.0", "builder-util": "26.16.0", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0" } }, "sha512-ozCGC54wi2C1RkS2v69y9lQxvoQ1Coh+iT8CmWiyY8y1ZuOKVvuNson2Hd+HEOs9+EZNNsOBbzKgQZ4SNqWd0w=="], + + "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=="], + + "duplexer2": ["duplexer2@0.1.4", "", { "dependencies": { "readable-stream": "^2.0.2" } }, "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA=="], + + "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], + + "electron": ["electron@45.1.0", "", { "dependencies": { "@electron-internal/extract-zip": "^1.0.1", "@electron/get": "^5.0.0", "@types/node": "^24.9.0" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-i7qKVAh/KR5v1pf56NhGGX2QYHcdjvLgS7koL6ui50c+zUDGLRunR95nXcTpHPURKOQ4GDBG8BmXiDgOQ8DD0A=="], + + "electron-builder": ["electron-builder@26.18.0", "", { "dependencies": { "app-builder-lib": "26.18.0", "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.18.0", "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-ASgIuQnOq4RuD8jMhS9hI0LS3EMRT1I/M5R6utqifB/BHN80IcCjBdt+DED+NmNPvmBgEtrlExXZitNdA7kXOg=="], + + "electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@26.18.0", "", { "dependencies": { "app-builder-lib": "26.18.0", "builder-util": "26.16.0", "electron-winstaller": "5.4.0" } }, "sha512-a2elgTipboXNIsxqS1HOmHMGp4Pd3LJgIZOdIFTOskBSawvxKyZWYWQ1d7KKA4CtINSchYCYEKzhxRqiKZev9Q=="], + + "electron-publish": ["electron-publish@26.18.0", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "aws4": "^1.13.2", "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-MagnqaZSALSWjcdKu5HEVhYUZD/2BDL3q7XzrqWwub6gf/UfD3ItMgmthVDv9w5y9E0gH1C3JWqKaXwf6f4ROg=="], + + "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=="], + + "esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="], + + "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=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.9", "", {}, "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "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.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], + + "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=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "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=="], + + "hono": ["hono@4.13.14", "", {}, "sha512-yhRLQ21U6qa9Hsu2N7Qcbh8tE+WKEcoErjIo56LeSGiGXGp9sA7UpE86Ui9uexb61KakiGwuHMn2QdV7w6i6kQ=="], + + "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.3.0", "", {}, "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA=="], + + "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=="], + + "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=="], + + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "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": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="], + + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + + "js-yaml": ["js-yaml@4.3.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "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=="], + + "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], + + "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=="], + + "lucide-react": ["lucide-react@1.64.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-v626F/bcLNvFom6vzztHILtG9tUgXXbgA5AGkd82YkV401+Ko/NHyrWafbPwGofa+6p/EgDJcvEkIwsMH9098w=="], + + "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": { "mime": "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.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], + + "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": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.20", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ=="], + + "node-abi": ["node-abi@4.40.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-GuvqvtrOk+AC27RidBxBsMzVNkwNl7PjHgc4NXbmdvSVLgIUjpaqcG1FjNRYnt2Yn9Ytq+Ln37XsBaglUcte/w=="], + + "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.4.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": { "node-gyp": "bin/node-gyp.js" } }, "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw=="], + + "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], + + "nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": { "nopt": "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=="], + + "pdf-parse": ["pdf-parse@2.4.5", "", { "dependencies": { "@napi-rs/canvas": "0.1.80", "pdfjs-dist": "5.4.296" }, "bin": { "pdf-parse": "bin/cli.mjs" } }, "sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg=="], + + "pdfjs-dist": ["pdfjs-dist@5.4.296", "", { "optionalDependencies": { "@napi-rs/canvas": "^0.1.80" } }, "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q=="], + + "pe-library": ["pe-library@0.4.1", "", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + + "pkijs": ["pkijs@3.4.3", "", { "dependencies": { "@noble/hashes": "1.8.0", "asn1js": "^3.0.6", "bytestreamjs": "^2.0.1", "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-V9kOzaItwDgqlryhXysQsAXUEKi5O7kqWUaP3dXI7MLvkPqWWMb4wVb1QAtmEDCKI+XIAINsZoNKMFF7sZb1eg=="], + + "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], + + "postcss": ["postcss@8.5.32", "", { "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" } }, "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g=="], + + "postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": { "postject": "dist/cli.js" } }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="], + + "proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="], + + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + + "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=="], + + "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], + + "pvutils": ["pvutils@1.2.0", "", {}, "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg=="], + + "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], + + "react": ["react@19.3.1", "", {}, "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw=="], + + "react-dom": ["react-dom@19.3.1", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.1" } }, "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ=="], + + "read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": { "read-binary-file-arch": "cli.js" } }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="], + + "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "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": { "rimraf": "./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=="], + + "rolldown": ["rolldown@1.2.16", "", { "dependencies": { "@oxc-project/types": "=0.156.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.16", "@rolldown/binding-android-arm64": "1.2.16", "@rolldown/binding-darwin-arm64": "1.2.16", "@rolldown/binding-darwin-x64": "1.2.16", "@rolldown/binding-freebsd-x64": "1.2.16", "@rolldown/binding-linux-arm-gnueabihf": "1.2.16", "@rolldown/binding-linux-arm64-gnu": "1.2.16", "@rolldown/binding-linux-arm64-musl": "1.2.16", "@rolldown/binding-linux-ppc64-gnu": "1.2.16", "@rolldown/binding-linux-s390x-gnu": "1.2.16", "@rolldown/binding-linux-x64-gnu": "1.2.16", "@rolldown/binding-linux-x64-musl": "1.2.16", "@rolldown/binding-openharmony-arm64": "1.2.16", "@rolldown/binding-win32-arm64-msvc": "1.2.16", "@rolldown/binding-win32-x64-msvc": "1.2.16" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw=="], + + "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "sanitize-filename": ["sanitize-filename@1.6.4", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg=="], + + "sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="], + + "scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="], + + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "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=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.2", "", {}, "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw=="], + + "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=="], + + "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "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.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="], + + "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=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "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.30.1", "", {}, "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg=="], + + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "unzipper": ["unzipper@0.12.5", "", { "dependencies": { "bluebird": "~3.7.2", "duplexer2": "~0.1.4", "fs-extra": "11.3.1", "graceful-fs": "^4.2.2", "node-int64": "^0.4.0" } }, "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A=="], + + "utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "vite": ["vite@8.3.5", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.30", "rolldown": "~1.2.14", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1 || ^0.8.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ=="], + + "webcrypto-core": ["webcrypto-core@1.9.2", "", { "dependencies": { "@peculiar/asn1-schema": "^2.7.0", "@peculiar/json-schema": "^1.1.12", "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q=="], + + "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.3", "", { "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-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "zod": ["zod@4.6.7", "", {}, "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg=="], + + "@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.4.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA=="], + + "@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.4.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA=="], + + "@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": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "bun-types/undici-types": ["undici-types@8.12.3", "", {}, "sha512-EXE4ydL3S76dFb27UFjNVZHTyv/TLdATj/KUe29ptdcg6qXL48J2tZ3126E5kICj5jn781OR+XlVH1N6Mz8jbA=="], + + "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-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.29.1", "", {}, "sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA=="], + + "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": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "unzipper/fs-extra": ["fs-extra@11.3.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g=="], + + "@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.21", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw=="], + + "@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.1.7", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g=="], + + "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": { "semver": "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.21", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw=="], + + "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=="], + + "filelist/minimatch/brace-expansion": ["brace-expansion@2.1.7", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@1.1.21", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw=="], + + "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=="], + } +} diff --git a/desktop/bunfig.toml b/desktop/bunfig.toml new file mode 100644 index 0000000..98123b5 --- /dev/null +++ b/desktop/bunfig.toml @@ -0,0 +1,2 @@ +[test] +root = "./tests" diff --git a/desktop/electron-builder.config.mjs b/desktop/electron-builder.config.mjs new file mode 100644 index 0000000..96fc2ba --- /dev/null +++ b/desktop/electron-builder.config.mjs @@ -0,0 +1,67 @@ +import { realpathSync } from "node:fs" +import path from "node:path" + +const root = import.meta.dirname + +export default { + appId: "com.voidsteed.copilotbridge", + productName: "Copilot Bridge", + buildVersion: "0.0.1.1", + directories: { + output: process.env.COPILOT_BRIDGE_PACKAGE_OUTPUT || "release", + buildResources: "assets", + }, + files: [ + { + from: realpathSync(path.join(root, "dist")), + to: "dist", + filter: ["main.cjs", "preload.cjs", "renderer/**"], + }, + "assets/**", + "package.json", + ], + extraResources: [ + { + from: realpathSync(path.join(root, "dist")), + to: "app.asar.unpacked/dist", + filter: ["engine.cjs"], + }, + ], + asar: true, + asarUnpack: [ + "node_modules/pdf-parse/**", + "node_modules/pdfjs-dist/**", + "node_modules/@napi-rs/**", + ], + npmRebuild: false, + publish: null, + mac: { + bundleVersion: "1", + category: "public.app-category.developer-tools", + icon: "assets/icon.png", + hardenedRuntime: true, + target: [ + { target: "dmg", arch: ["arm64", "x64"] }, + { target: "zip", arch: ["arm64", "x64"] }, + ], + }, + win: { + icon: "assets/icon.png", + executableName: "copilot-bridge", + target: [{ target: "nsis", arch: ["x64"] }], + }, + nsis: { + oneClick: false, + allowToChangeInstallationDirectory: true, + createDesktopShortcut: false, + }, + linux: { + icon: "assets/icon.png", + category: "Development", + executableName: "copilot-bridge", + target: [ + { target: "AppImage", arch: ["x64", "arm64"] }, + { target: "deb", arch: ["x64", "arm64"] }, + ], + }, +} diff --git a/desktop/engine/index.ts b/desktop/engine/index.ts new file mode 100644 index 0000000..fa040a9 --- /dev/null +++ b/desktop/engine/index.ts @@ -0,0 +1,110 @@ +import type { ModelsResponse } from "../../src/services/copilot/get-models" + +import { engineCommandSchema, type EngineEvent } from "./protocol" +import { mapUpstreamUrl, UpstreamError } from "./upstream" + +let initialized = false +let stopping = false +let runtime: + | { stop(): Promise; updateModels(models: ModelsResponse): void } + | undefined + +function send(event: EngineEvent): void { + if (process.connected) process.send?.(event, () => undefined) +} + +function startupErrorCode( + error: unknown, +): Extract["code"] { + if (error instanceof Error && "code" in error && error.code === "EADDRINUSE") + return "address-in-use" + if (error instanceof UpstreamError) + return error.kind === "unauthorized" ? "authentication" : "upstream" + return "startup" +} + +async function stop(): Promise { + if (stopping) return + stopping = true + const deadline = setTimeout(() => process.exit(0), 1200) + deadline.unref() + await runtime?.stop() + if (!process.connected) process.exit(0) + send({ kind: "stopped" }) +} + +process.on("disconnect", () => { + void stop() +}) +process.on("SIGTERM", () => { + void stop() +}) +process.on("SIGINT", () => { + void stop() +}) +process.on("uncaughtException", () => { + send({ kind: "error", code: "startup" }) + void stop() +}) +process.on("unhandledRejection", () => { + send({ kind: "error", code: "startup" }) + void stop() +}) + +process.on("message", (input: unknown) => { + const parsed = engineCommandSchema.safeParse(input) + if (!parsed.success) { + void stop() + return + } + if (parsed.data.kind === "exit") { + if (stopping) process.exit(0) + else void stop() + return + } + if (parsed.data.kind === "catalog") { + if (!runtime || stopping) return + runtime.updateModels(parsed.data.models) + send({ kind: "catalog-updated", id: parsed.data.id }) + return + } + if (parsed.data.kind === "stop") { + void stop() + return + } + if (initialized || stopping) return + initialized = true + const init = parsed.data + const originalFetch = globalThis.fetch + if (init.fixtureOrigin) { + const fixtureOrigin = init.fixtureOrigin + globalThis.fetch = Object.assign( + ( + input: Parameters[0], + options?: Parameters[1], + ) => { + const source = input instanceof Request ? input.url : String(input) + const destination = mapUpstreamUrl(source, fixtureOrigin) + return originalFetch( + input instanceof Request ? + new Request(destination, input) + : destination, + options, + ) + }, + originalFetch, + ) + } + void import("./runtime") + .then(({ startEngine }) => startEngine(init, send)) + .then((started) => { + runtime = started + if (stopping) return started.stop() + }) + .catch((error: unknown) => { + send({ kind: "error", code: startupErrorCode(error) }) + void stop() + }) +}) + +if (!process.send) process.exit(1) diff --git a/desktop/engine/protocol.ts b/desktop/engine/protocol.ts new file mode 100644 index 0000000..240fd0d --- /dev/null +++ b/desktop/engine/protocol.ts @@ -0,0 +1,50 @@ +import { z } from "zod" + +import { activitySchema, settingsSchema } from "../shared/contracts" +import { fixtureOriginSchema, upstreamCatalogSchema } from "./upstream" + +export const engineInitSchema = z + .object({ + kind: z.literal("init"), + githubToken: z.string().min(1).max(16_384), + gatewayKey: z.string().min(32).max(256), + port: settingsSchema.shape.port, + accountType: settingsSchema.shape.accountType, + fixtureOrigin: fixtureOriginSchema.optional(), + }) + .strict() + +export type EngineInit = z.infer + +export const engineCommandSchema = z.union([ + engineInitSchema, + z.object({ kind: z.literal("stop") }).strict(), + z.object({ kind: z.literal("exit") }).strict(), + z + .object({ + kind: z.literal("catalog"), + id: z.number().int().positive(), + models: upstreamCatalogSchema, + }) + .strict(), +]) + +export const engineEventSchema = z.discriminatedUnion("kind", [ + z.object({ kind: z.literal("ready") }).strict(), + z.object({ kind: z.literal("stopped") }).strict(), + z + .object({ + kind: z.literal("catalog-updated"), + id: z.number().int().positive(), + }) + .strict(), + z.object({ kind: z.literal("activity"), record: activitySchema }).strict(), + z + .object({ + kind: z.literal("error"), + code: z.enum(["address-in-use", "authentication", "upstream", "startup"]), + }) + .strict(), +]) + +export type EngineEvent = z.infer diff --git a/desktop/engine/runtime.ts b/desktop/engine/runtime.ts new file mode 100644 index 0000000..3e02f95 --- /dev/null +++ b/desktop/engine/runtime.ts @@ -0,0 +1,169 @@ +import { getRequestListener } from "@hono/node-server" +import { randomUUID } from "node:crypto" +import { createServer } from "node:http" + +import type { ModelsResponse } from "../../src/services/copilot/get-models" +import type { EngineEvent, EngineInit } from "./protocol" + +import { createDesktopProxy } from "../../src/desktop-proxy" +import { state } from "../../src/lib/state" +import { DEFAULT_VSCODE_VERSION } from "../../src/services/get-vscode-version" +import { createAccountClient } from "./upstream" + +const safePaths = new Set([ + "/", + "/models", + "/v1/models", + "/chat/completions", + "/v1/chat/completions", + "/responses", + "/v1/responses", + "/messages", + "/v1/messages", + "/messages/count_tokens", + "/v1/messages/count_tokens", + "/embeddings", + "/v1/embeddings", +]) + +function safeTokenCount(value: number | null): number | null { + return value !== null && Number.isFinite(value) && value >= 0 ? value : null +} + +export async function startEngine( + init: EngineInit, + send: (event: EngineEvent) => void, +) { + const abort = new AbortController() + const client = createAccountClient({ + githubToken: init.githubToken, + accountType: init.accountType, + fixtureOrigin: init.fixtureOrigin, + signal: abort.signal, + }) + state.githubToken = init.githubToken + state.accountType = init.accountType + state.localApiKeys = [init.gatewayKey] + state.vsCodeVersion = DEFAULT_VSCODE_VERSION + const initialToken = await client.token() + state.copilotToken = initialToken.token + state.models = await client.models(initialToken.token) + + let tokenTimer: ReturnType | undefined + const scheduleRefresh = (refreshSeconds: number) => { + tokenTimer = setTimeout( + () => { + void client + .token() + .then((token) => { + if (abort.signal.aborted) return + state.copilotToken = token.token + scheduleRefresh(token.refresh_in) + }) + .catch(() => { + if (!abort.signal.aborted) scheduleRefresh(90) + }) + }, + Math.max(30, Math.min(86_400, refreshSeconds - 60)) * 1000, + ) + tokenTimer.unref() + } + + const app = createDesktopProxy({ + port: init.port, + gatewayKey: init.gatewayKey, + onComplete(summary) { + const model = + state.models?.data.some((item) => item.id === summary.model) ? + summary.model + : null + send({ + kind: "activity", + record: { + ...summary, + id: randomUUID(), + model, + method: + ( + /^(?:GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS)$/.test( + summary.method, + ) + ) ? + summary.method + : "OTHER", + path: safePaths.has(summary.path) ? summary.path : "/unsupported", + inputTokens: safeTokenCount(summary.inputTokens), + outputTokens: safeTokenCount(summary.outputTokens), + cachedTokens: safeTokenCount(summary.cachedTokens), + }, + }) + }, + }) + + const listener = await createNodeListener({ + fetch: app.fetch, + port: init.port, + }) + scheduleRefresh(initialToken.refresh_in) + send({ kind: "ready" }) + let stopping: Promise | undefined + return { + updateModels(models: ModelsResponse): void { + state.models = models + }, + stop(): Promise { + stopping ??= (async () => { + abort.abort() + clearTimeout(tokenTimer) + await listener.stop() + state.githubToken = undefined + state.copilotToken = undefined + state.models = undefined + state.localApiKeys = [] + })() + return stopping + }, + } +} + +async function createNodeListener(options: { + fetch: Parameters[0] + port: number +}) { + const respond = getRequestListener(options.fetch) + const inFlight = new Set>() + const server = createServer((incoming, outgoing) => { + const response = Promise.resolve(respond(incoming, outgoing)) + .catch(() => { + outgoing.destroy() + }) + .finally(() => inFlight.delete(response)) + inFlight.add(response) + }) + server.headersTimeout = 10_000 + server.requestTimeout = 300_000 + await new Promise((resolve, reject) => { + server.once("error", reject) + server.listen(options.port, "127.0.0.1", () => { + server.removeListener("error", reject) + resolve() + }) + }) + let stopping: Promise | undefined + return { + stop(): Promise { + stopping ??= new Promise((resolve) => { + const force = setTimeout(() => server.closeAllConnections(), 750) + server.close(() => { + clearTimeout(force) + void Promise.race([ + Promise.allSettled(inFlight), + new Promise((complete) => setTimeout(complete, 200)), + ]).then(() => resolve()) + }) + server.closeIdleConnections() + }) + return stopping + }, + } +} diff --git a/desktop/engine/upstream.ts b/desktop/engine/upstream.ts new file mode 100644 index 0000000..6d0d237 --- /dev/null +++ b/desktop/engine/upstream.ts @@ -0,0 +1,274 @@ +import { z } from "zod" + +import type { Model } from "../../src/services/copilot/get-models" +import type { + DesktopModel, + DesktopSettings, + DesktopSnapshot, +} from "../shared/contracts" + +import { copilotHeaders, githubHeaders } from "../../src/lib/api-config" +import { DEFAULT_VSCODE_VERSION } from "../../src/services/get-vscode-version" + +const safeText = z.string().max(256) +const modelId = z + .string() + .min(1) + .max(200) + .regex(/^[\w.:-]+$/) +const tokenSchema = z.object({ + token: z.string().min(1).max(16_384), + refresh_in: z.number().positive().default(1500), + expires_at: z.number().optional(), +}) + +export const upstreamModelSchema = z.object({ + id: modelId, + name: safeText, + vendor: safeText.default("Unknown"), + object: z.string().default("model"), + version: z.string().default(""), + preview: z.boolean().default(false), + model_picker_enabled: z.boolean().default(true), + supported_endpoints: z.array(z.string().max(80)).max(30).optional(), + capabilities: z.object({ + family: z.string().default(""), + object: z.string().default("model_capabilities"), + tokenizer: z.string().default(""), + type: z.string().default("chat"), + limits: z + .object({ + max_context_window_tokens: z.number().nonnegative().optional(), + max_output_tokens: z.number().nonnegative().optional(), + max_prompt_tokens: z.number().nonnegative().optional(), + max_inputs: z.number().nonnegative().optional(), + }) + .default({}), + supports: z + .object({ + tool_calls: z.boolean().optional(), + parallel_tool_calls: z.boolean().optional(), + dimensions: z.boolean().optional(), + streaming: z.boolean().optional(), + structured_outputs: z.boolean().optional(), + vision: z.boolean().optional(), + adaptive_thinking: z.boolean().optional(), + reasoning_effort: z.array(z.string()).optional(), + }) + .default({}), + }), + policy: z + .object({ state: z.string(), terms: z.string().default("") }) + .optional(), +}) + +export const upstreamCatalogSchema = z.object({ + object: z.string().default("list"), + data: z.array(upstreamModelSchema).max(2000), +}) + +const quotaDetailSchema = z.object({ + entitlement: z.number().nonnegative().optional(), + percent_remaining: z.number().optional(), + quota_remaining: z.number().optional(), + remaining: z.number().optional(), + unlimited: z.boolean().default(false), +}) + +const upstreamUsageSchema = z.object({ + copilot_plan: safeText.default("Copilot"), + quota_reset_date: z.string().max(80).nullish(), + quota_snapshots: z.record(z.string().max(80), quotaDetailSchema).default({}), +}) + +export const fixtureOriginSchema = z.url().refine((value) => { + const url = new URL(value) + return ( + url.protocol === "http:" + && url.hostname === "127.0.0.1" + && url.pathname === "/" + && !url.search + && !url.hash + && !url.username + && !url.password + && Boolean(url.port) + ) +}, "Fixture origin must be a numeric loopback HTTP origin") + +const upstreamOrigins = new Set([ + "https://github.com", + "https://api.github.com", + "https://api.githubcopilot.com", + "https://api.business.githubcopilot.com", + "https://api.enterprise.githubcopilot.com", +]) + +export function mapUpstreamUrl(value: string, fixtureOrigin?: string): string { + if (!fixtureOrigin) return value + const url = new URL(value) + if (!upstreamOrigins.has(url.origin)) return value + return new URL(`${url.pathname}${url.search}`, fixtureOrigin).toString() +} + +export class UpstreamError extends Error { + readonly kind: "network" | "unauthorized" | "unavailable" | "invalid" + + constructor(kind: UpstreamError["kind"]) { + super(kind) + this.kind = kind + } +} + +export interface UpstreamOptions { + fixtureOrigin?: string + signal: AbortSignal +} + +export async function fetchUpstreamJson( + options: UpstreamOptions & { url: string; init?: RequestInit }, +): Promise { + try { + const response = await fetch( + mapUpstreamUrl(options.url, options.fixtureOrigin), + { + ...options.init, + redirect: "error", + signal: AbortSignal.any([options.signal, AbortSignal.timeout(12_000)]), + }, + ) + if (!response.ok) { + await response.body?.cancel() + throw new UpstreamError( + response.status === 401 || response.status === 403 ? + "unauthorized" + : "unavailable", + ) + } + const text = await response.text() + if (text.length > 4_000_000) throw new UpstreamError("invalid") + return JSON.parse(text) as unknown + } catch (error) { + if (error instanceof UpstreamError) throw error + options.signal.throwIfAborted() + throw new UpstreamError("network") + } +} + +export function createAccountClient( + options: UpstreamOptions & { + githubToken: string + accountType: DesktopSettings["accountType"] + }, +) { + const requestState = { + githubToken: options.githubToken, + accountType: options.accountType, + vsCodeVersion: DEFAULT_VSCODE_VERSION, + manualApprove: false, + rateLimitWait: false, + showToken: false, + localApiKeys: [], + responsesContextTrim: false, + } + const github = (path: string) => + fetchUpstreamJson({ + ...options, + url: `https://api.github.com${path}`, + init: { headers: githubHeaders(requestState) }, + }) + return { + async user() { + return z + .object({ + login: z + .string() + .min(1) + .max(80) + .regex(/^[\w-]+$/), + name: safeText.nullish().transform((name) => name ?? null), + }) + .parse(await github("/user")) + }, + async token() { + return tokenSchema.parse(await github("/copilot_internal/v2/token")) + }, + async models(copilotToken: string) { + const base = + options.accountType === "individual" ? + "https://api.githubcopilot.com" + : `https://api.${options.accountType}.githubcopilot.com` + return upstreamCatalogSchema.parse( + await fetchUpstreamJson({ + ...options, + url: `${base}/models`, + init: { headers: copilotHeaders({ ...requestState, copilotToken }) }, + }), + ) + }, + async usage(): Promise> { + const usage = upstreamUsageSchema.parse( + await github("/copilot_internal/user"), + ) + const quotas: NonNullable["quotas"] = [] + const labels: Record = { + chat: "Chat", + completions: "Completions", + premium_interactions: "Premium requests", + } + for (const [id, quota] of Object.entries(usage.quota_snapshots)) { + const label = labels[id] ?? id.replaceAll("_", " ") + if (quota.unlimited) { + quotas.push({ kind: "unlimited", id, label }) + continue + } + const remaining = quota.remaining ?? quota.quota_remaining + if (remaining === undefined || quota.entitlement === undefined) continue + quotas.push({ + kind: "limited", + id, + label, + remaining: Math.max(0, remaining), + limit: quota.entitlement, + percentRemaining: Math.max( + 0, + Math.min( + 100, + quota.percent_remaining + ?? (quota.entitlement > 0 ? + (remaining / quota.entitlement) * 100 + : 0), + ), + ), + }) + } + return { + plan: usage.copilot_plan, + resetAt: usage.quota_reset_date ?? null, + fetchedAt: new Date().toISOString(), + quotas, + } + }, + } +} + +export function toDesktopModel(model: Model): DesktopModel { + const supports: DesktopModel["supports"] = [] + if (model.capabilities.supports.tool_calls) supports.push("tools") + if (model.capabilities.supports.vision) supports.push("vision") + if ( + model.capabilities.supports.reasoning_effort?.length + || model.capabilities.supports.adaptive_thinking + ) + supports.push("reasoning") + if (model.capabilities.supports.streaming) supports.push("streaming") + return { + id: model.id, + name: model.name, + vendor: model.vendor, + contextWindow: model.capabilities.limits.max_context_window_tokens ?? null, + maxOutput: model.capabilities.limits.max_output_tokens ?? null, + supports, + endpoints: model.supported_endpoints ?? ["/chat/completions", "/responses"], + preview: model.preview, + } +} diff --git a/desktop/index.html b/desktop/index.html new file mode 100644 index 0000000..afd5a77 --- /dev/null +++ b/desktop/index.html @@ -0,0 +1,13 @@ + + + + + + + Copilot Bridge + + +
+ + + diff --git a/desktop/main/clipboard.ts b/desktop/main/clipboard.ts new file mode 100644 index 0000000..e8916b1 --- /dev/null +++ b/desktop/main/clipboard.ts @@ -0,0 +1,73 @@ +import type { DesktopCommand, DesktopModel } from "../shared/contracts" + +import { createConnectionSnippet, supportsAgent } from "../shared/connections" + +type ClipboardCommand = Extract< + DesktopCommand, + { kind: "copy" | "copy-config" } +> +type ClipboardResult = + | { kind: "text"; text: string } + | { kind: "error"; message: string } + +export function createClipboardText(options: { + command: ClipboardCommand + port: number + gatewayKey: string | null + deviceCode: string | null + models: Array +}): ClipboardResult { + const { command, gatewayKey, deviceCode } = options + if (command.kind === "copy") { + const target = command.target + switch (target) { + case "base-url": { + return { kind: "text", text: `http://127.0.0.1:${options.port}/v1` } + } + case "gateway-key": { + return gatewayKey ? + { kind: "text", text: gatewayKey } + : { kind: "error", message: "Sign in before copying a gateway key." } + } + case "device-code": { + return deviceCode ? + { kind: "text", text: deviceCode } + : { + kind: "error", + message: "A device code is available only during sign-in.", + } + } + default: { + const exhaustive: never = target + return exhaustive + } + } + } + const model = options.models.find((item) => item.id === command.modelId) + if (!gatewayKey) + return { + kind: "error", + message: "Sign in before copying an agent configuration.", + } + if (!model) + return { + kind: "error", + message: "This model is no longer available. Refresh the model catalog.", + } + if (!supportsAgent(model, command.target)) + return { + kind: "error", + message: + "This model does not support this agent's API. Choose another model.", + } + return { + kind: "text", + text: createConnectionSnippet({ + target: command.target, + model, + port: options.port, + apiKey: gatewayKey, + shell: command.shell, + }), + } +} diff --git a/desktop/main/controller.ts b/desktop/main/controller.ts new file mode 100644 index 0000000..344129d --- /dev/null +++ b/desktop/main/controller.ts @@ -0,0 +1,812 @@ +import { randomBytes } from "node:crypto" +import { z } from "zod" + +import type { ModelsResponse } from "../../src/services/copilot/get-models" +import type { + ControllerOptions, + DesktopController, + StoredCredentials, +} from "./types" + +import { + createAccountClient, + fixtureOriginSchema, + toDesktopModel, +} from "../engine/upstream" +import { + commandSchema, + type ActivityRecord, + type DesktopCommand, + type DesktopModel, + type DesktopSettings, + type DesktopSnapshot, +} from "../shared/contracts" +import { createClipboardText } from "./clipboard" +import { gatewayError, signInError } from "./failure-messages" +import { authorizeDevice, DEVICE_PAGE } from "./oauth" +import { executeProbe } from "./probe" +import { type ProxyProcess, startProxyProcess } from "./proxy-process" +import { createSettingsStore, persistSettings } from "./settings-store" +import { appendActivity, initialSnapshot } from "./snapshot" +const credentialsSchema = z + .object({ + githubToken: z.string().min(1).max(16384), + gatewayKey: z.string().min(32).max(256), + }) + .strict() +function newGatewayKey(): string { + return randomBytes(32).toString("base64url") +} +class Controller implements DesktopController { + private readonly options: ControllerOptions + private fixtureOrigin: string | undefined + private settingsStore: ReturnType + private state: DesktopSnapshot + private catalog: ModelsResponse | undefined + private credentials: StoredCredentials | null = null + private disposed: boolean = false + private accountGeneration: number = 0 + private accountAbort: AbortController | undefined + private refreshAbort: AbortController | undefined + private probeAbort: AbortController | undefined + private desiredGateway: boolean = false + private configurationRevision: number = 0 + private owned: + | { + process: ProxyProcess + revision: number + } + | undefined + private reconciliation: Promise | undefined + private storage: Promise = Promise.resolve() + private settingsWrites: Promise = Promise.resolve() + private disposal: Promise | undefined + private background: Set> = new Set>() + private listeners: Set<(snapshot: DesktopSnapshot) => void> = new Set< + (snapshot: DesktopSnapshot) => void + >() + constructor( + options: ControllerOptions, + settingsStore: ReturnType, + loaded: Awaited>, + ) { + this.options = options + this.settingsStore = settingsStore + this.fixtureOrigin = + this.options.fixtureOrigin === undefined ? + undefined + : fixtureOriginSchema.parse(this.options.fixtureOrigin) + this.state = initialSnapshot({ + version: options.version, + platform: options.platform, + fixture: this.fixtureOrigin !== undefined, + storageMode: options.vault.mode, + settings: loaded.settings, + recovered: loaded.recovered, + }) + } + snapshot = (): DesktopSnapshot => { + return structuredClone(this.state) + } + private publish(patch: Partial): void { + this.state = { ...this.state, ...patch, revision: this.state.revision + 1 } + if (this.disposed) return + for (const listener of this.listeners) { + try { + listener(this.snapshot()) + } catch { + continue + } + } + } + private notice(message: string, kind: "info" | "error" = "error"): void { + this.publish({ notice: { kind, message } }) + } + private own(task: Promise): void { + const guarded = task + .catch(() => { + if (!this.disposed) + this.notice("The action could not be completed. Try again.") + }) + .finally(() => this.background.delete(guarded)) + this.background.add(guarded) + } + private writeCredentials(task: () => Promise): Promise { + const next = this.storage.then(task) + this.storage = next.catch(() => undefined) + return next + } + private invalidateAccount(): number { + this.accountGeneration++ + this.accountAbort?.abort() + this.refreshAbort?.abort() + this.probeAbort?.abort() + return this.accountGeneration + } + private isAccountCurrent(generation: number, signal?: AbortSignal): boolean { + return ( + !this.disposed + && !signal?.aborted + && generation === this.accountGeneration + ) + } + + private canRunGateway(revision = this.configurationRevision): boolean { + return ( + this.desiredGateway + && !this.disposed + && this.state.account.kind === "signed-in" + && revision === this.configurationRevision + ) + } + + private recordActivity(process: ProxyProcess, record: ActivityRecord): void { + if ( + this.disposed + || !this.credentials + || this.state.account.kind !== "signed-in" + || this.owned?.process !== process + ) + return + this.publish(appendActivity(this.state, record)) + } + + private processExited(process: ProxyProcess, expected: boolean): void { + if (expected || this.disposed || this.owned?.process !== process) return + this.owned = undefined + this.desiredGateway = false + this.publish({ + gateway: { + kind: "error", + message: + "The gateway exited unexpectedly. Start it again to reconnect.", + }, + ...(this.state.probe.kind === "running" ? + { + probe: { + kind: "error", + message: + "The gateway stopped before the test request finished. Start it again.", + }, + } + : {}), + }) + this.probeAbort?.abort() + } + + private async reconcile(): Promise { + while (true) { + if (this.owned && !this.canRunGateway(this.owned.revision)) { + const previous = this.owned + this.publish({ gateway: { kind: "stopping" } }) + await previous.process.stop() + if (this.owned === previous) this.owned = undefined + this.publish({ gateway: { kind: "stopped" } }) + continue + } + if (!this.canRunGateway() || !this.credentials) return + if (this.owned) return + const revision = this.configurationRevision + const generation = this.accountGeneration + const credentials = this.credentials + this.publish({ gateway: { kind: "starting" } }) + if (!this.canRunGateway(revision)) continue + let process: ProxyProcess + try { + process = startProxyProcess({ + runtimePath: this.options.runtimePath, + runtimeExecPath: this.options.runtimeExecPath, + init: { + kind: "init", + githubToken: credentials.githubToken, + gatewayKey: credentials.gatewayKey, + port: this.state.settings.port, + accountType: this.state.settings.accountType, + fixtureOrigin: this.fixtureOrigin, + }, + onActivity: (record) => { + if (generation === this.accountGeneration) + this.recordActivity(process, record) + }, + onExit: (expected) => this.processExited(process, expected), + }) + this.owned = { process, revision } + await process.ready + await this.applyCurrentCatalog(process, revision) + if (this.canRunGateway(revision)) { + this.publish({ + gateway: { + kind: "running", + url: `http://127.0.0.1:${this.state.settings.port}/v1`, + startedAt: new Date().toISOString(), + }, + }) + return + } + } catch (error) { + if (!this.canRunGateway(revision)) continue + this.desiredGateway = false + const previous = this.owned + if (previous) { + await previous.process.stop() + if (this.owned === previous) this.owned = undefined + } + this.publish({ + gateway: { kind: "error", message: gatewayError(error) }, + }) + return + } + } + } + private async applyCurrentCatalog( + process: ProxyProcess, + revision: number, + ): Promise { + let catalog = this.catalog + while (catalog && this.canRunGateway(revision)) { + await process.updateModels(catalog) + if (catalog === this.catalog) return + catalog = this.catalog + } + } + + private convergeGateway(): Promise { + if (!this.reconciliation) { + const task = Promise.resolve() + .then(() => this.reconcile()) + .finally(() => { + if (this.reconciliation !== task) return + this.reconciliation = undefined + const unsettled = + this.owned ? + !this.canRunGateway(this.owned.revision) + : this.canRunGateway() && this.credentials !== null + if (unsettled) return this.convergeGateway() + }) + this.reconciliation = task + } + return this.reconciliation + } + private setGatewayDesired(running: boolean): Promise { + if (!running && this.desiredGateway) this.configurationRevision++ + this.desiredGateway = running + if (!running) { + this.probeAbort?.abort() + if (this.state.probe.kind === "running") + this.publish({ probe: { kind: "idle" } }) + if (!this.owned) this.publish({ gateway: { kind: "stopped" } }) + } + if ( + this.owned + && (!running || this.owned.revision !== this.configurationRevision) + ) + this.own(this.owned.process.stop()) + return this.convergeGateway() + } + private async refresh(): Promise { + if (!this.credentials || this.state.account.kind !== "signed-in") return + this.refreshAbort?.abort() + const abort = new AbortController() + this.refreshAbort = abort + const generation = this.accountGeneration + const client = createAccountClient({ + githubToken: this.credentials.githubToken, + accountType: this.state.settings.accountType, + fixtureOrigin: this.fixtureOrigin, + signal: abort.signal, + }) + const results = await Promise.allSettled([ + client + .token() + .then((token) => client.models(token.token)) + .then((models) => + this.synchronizeCatalog(models, generation, abort.signal), + ), + client.usage().then((usage) => { + if ( + !this.isAccountCurrent(generation, abort.signal) + || this.state.account.kind !== "signed-in" + ) + return + this.publish({ + usage, + account: { ...this.state.account, plan: usage.plan }, + }) + }), + ]) + if (!this.isAccountCurrent(generation, abort.signal)) return + if (results.some((result) => result.status === "rejected")) { + this.notice( + "Some account data could not be refreshed. Last successful values are still shown.", + ) + } + } + + private async synchronizeCatalog( + models: ModelsResponse, + generation: number, + signal: AbortSignal, + ): Promise { + while (this.isAccountCurrent(generation, signal)) { + const target = this.owned + if (!target || !this.canRunGateway(target.revision)) break + try { + await target.process.updateModels(models) + } catch (error) { + if (this.owned === target && this.canRunGateway(target.revision)) { + await this.setGatewayDesired(false) + throw error + } + continue + } + if (this.owned === target) break + } + if (!this.isAccountCurrent(generation, signal)) return + this.catalog = models + this.publish({ + models: models.data + .filter((model) => model.capabilities.type === "chat") + .map((model) => toDesktopModel(model)), + modelsFetchedAt: new Date().toISOString(), + }) + } + + private async finishAuthentication( + token: string, + generation: number, + signal: AbortSignal, + ): Promise { + const client = createAccountClient({ + githubToken: token, + accountType: this.state.settings.accountType, + fixtureOrigin: this.fixtureOrigin, + signal, + }) + const user = await client.user() + if (!this.isAccountCurrent(generation, signal)) return + const next = { + githubToken: token, + gatewayKey: this.credentials?.gatewayKey ?? newGatewayKey(), + } + await this.writeCredentials(async () => { + if (!this.isAccountCurrent(generation, signal)) return + await this.options.vault.save(next) + if (!this.isAccountCurrent(generation, signal)) + await this.options.vault.clear() + }) + if (!this.isAccountCurrent(generation, signal)) return + this.credentials = next + this.configurationRevision++ + this.publish({ + account: { + kind: "signed-in", + login: user.login, + name: user.name, + plan: "Copilot", + }, + notice: null, + }) + await this.refresh() + if (this.isAccountCurrent(generation, signal)) await this.convergeGateway() + } + private beginSignIn(): void { + if ( + this.state.account.kind === "signed-in" + || this.state.account.kind === "connecting" + || this.state.account.kind === "awaiting-authorization" + ) + return + const generation = this.invalidateAccount() + const abort = new AbortController() + this.accountAbort = abort + this.desiredGateway = this.state.settings.startOnLaunch + this.publish({ + account: { kind: "connecting" }, + notice: null, + probe: { kind: "idle" }, + }) + this.own( + authorizeDevice({ + fixtureOrigin: this.fixtureOrigin, + signal: abort.signal, + onCode: (code) => { + if (this.isAccountCurrent(generation, abort.signal)) { + this.publish({ + account: { kind: "awaiting-authorization", ...code }, + }) + } + }, + }) + .then((token) => + this.finishAuthentication(token, generation, abort.signal), + ) + .catch((error: unknown) => { + if (this.isAccountCurrent(generation, abort.signal)) { + this.publish({ + account: { kind: "error", message: signInError(error) }, + }) + } + }), + ) + } + private async signOut(): Promise { + this.invalidateAccount() + this.credentials = null + this.catalog = undefined + this.configurationRevision++ + const clearing = this.writeCredentials(() => this.options.vault.clear()) + const stopping = this.setGatewayDesired(false) + this.publish({ + account: { kind: "signed-out" }, + models: [], + modelsFetchedAt: null, + usage: null, + probe: { kind: "idle" }, + activity: [], + totals: { + requests: 0, + errors: 0, + inputTokens: 0, + outputTokens: 0, + cachedTokens: 0, + }, + notice: null, + }) + await Promise.all([clearing, stopping]) + } + private saveSettings( + change: (current: DesktopSettings) => DesktopSettings, + ): Promise { + const task = this.settingsWrites.then(async () => { + if (this.disposed) return + const previous = this.state.settings + const next = change(previous) + const saved = await persistSettings({ + previous, + next, + applyLoginItem: this.options.applyLoginItem?.bind(this.options), + save: (settings) => this.settingsStore.save(settings), + }) + if (saved !== "saved") { + this.notice( + saved === "unavailable" ? + "Launch at login is unavailable in this session." + : "Settings could not be saved. Check system permissions and try again.", + ) + return + } + if (this.disposal) return + const restart = + next.port !== previous.port || next.accountType !== previous.accountType + const accountChanged = next.accountType !== previous.accountType + if (accountChanged) { + this.refreshAbort?.abort() + this.catalog = undefined + } + this.publish({ + settings: next, + ...(accountChanged ? + { models: [], modelsFetchedAt: null, usage: null } + : {}), + }) + if (restart) { + this.configurationRevision++ + this.probeAbort?.abort() + if (this.state.probe.kind === "running") + this.publish({ probe: { kind: "idle" } }) + if (this.owned) this.own(this.owned.process.stop()) + await this.convergeGateway() + } + if (accountChanged) await this.refresh() + }) + this.settingsWrites = task.catch(() => undefined) + return task + } + private getModel(id: string): DesktopModel | undefined { + return this.state.models.find((model) => model.id === id) + } + private async runProbe(model: DesktopModel, prompt: string): Promise { + if (!this.credentials || this.state.gateway.kind !== "running") { + this.publish({ + probe: { + kind: "error", + message: "Start the gateway before sending a test request.", + }, + }) + return + } + this.probeAbort?.abort() + const abort = new AbortController() + this.probeAbort = abort + const generation = this.accountGeneration + const revision = this.configurationRevision + const baseUrl = this.state.gateway.url + const key = this.credentials.gatewayKey + const current = () => + !this.disposed + && !abort.signal.aborted + && generation === this.accountGeneration + && revision === this.configurationRevision + this.publish({ probe: { kind: "running", model: model.id } }) + try { + const result = await executeProbe({ + baseUrl, + gatewayKey: key, + model, + prompt, + signal: abort.signal, + }) + if (current()) this.publish({ probe: result }) + } catch { + if (!current()) return + this.publish({ + probe: { + kind: "error", + message: + "The test request failed. Check gateway activity and try another model or refresh your account.", + }, + }) + } + } + invoke = async (input: unknown): Promise => { + if (this.disposed) return this.snapshot() + const parsed = commandSchema.safeParse(input) + if (!parsed.success) { + this.notice("The app received an invalid command.") + return this.snapshot() + } + this.publish({ notice: null }) + try { + await this.dispatch(parsed.data) + } catch { + this.notice("The action could not be completed. Try again.") + } + return this.snapshot() + } + + private dispatch(command: DesktopCommand): void | Promise { + switch (command.kind) { + case "copy": + case "copy-config": { + return this.handleClipboardCommand(command) + } + case "start-gateway": + case "stop-gateway": + case "rotate-key": + case "run-probe": { + return this.handleGatewayCommand(command) + } + case "save-settings": + case "select-model": { + return this.handleSettingsCommand(command) + } + default: { + return this.handleAccountCommand(command) + } + } + } + + private async handleAccountCommand( + command: Extract< + DesktopCommand, + { + kind: + | "sign-in" + | "cancel-sign-in" + | "open-device-page" + | "sign-out" + | "refresh" + | "clear-activity" + | "dismiss-notice" + } + >, + ): Promise { + switch (command.kind) { + case "sign-in": { + this.beginSignIn() + return + } + case "cancel-sign-in": { + if ( + this.state.account.kind === "connecting" + || this.state.account.kind === "awaiting-authorization" + ) { + this.invalidateAccount() + this.credentials = null + this.desiredGateway = false + this.publish({ account: { kind: "signed-out" } }) + this.own(this.writeCredentials(() => this.options.vault.clear())) + } + return + } + case "open-device-page": { + if (this.state.account.kind === "awaiting-authorization") + await this.options.openExternal(DEVICE_PAGE) + return + } + case "sign-out": { + await this.signOut() + return + } + case "refresh": { + await this.refresh() + return + } + case "clear-activity": { + this.publish({ activity: [] }) + return + } + case "dismiss-notice": { + return + } + default: { + const exhaustive: never = command + return exhaustive + } + } + } + + private handleSettingsCommand( + command: Extract< + DesktopCommand, + { kind: "save-settings" | "select-model" } + >, + ): void | Promise { + if (command.kind === "save-settings") + return this.saveSettings(() => command.settings) + if (!this.getModel(command.modelId)) { + this.notice( + "This model is no longer available. Refresh the model catalog.", + ) + return + } + return this.saveSettings((current) => ({ + ...current, + preferredModel: command.modelId, + })) + } + + private async handleGatewayCommand( + command: Extract< + DesktopCommand, + { kind: "start-gateway" | "stop-gateway" | "rotate-key" | "run-probe" } + >, + ): Promise { + switch (command.kind) { + case "start-gateway": { + if (!this.credentials || this.state.account.kind !== "signed-in") + this.notice("Sign in to GitHub before starting the gateway.") + else await this.setGatewayDesired(true) + return + } + case "stop-gateway": { + await this.setGatewayDesired(false) + return + } + case "rotate-key": { + await this.rotateKey() + return + } + case "run-probe": { + const model = this.getModel(command.modelId) + if (!model) + this.publish({ + probe: { + kind: "error", + message: + "Choose an available model before sending a test request.", + }, + }) + else this.own(this.runProbe(model, command.prompt)) + return + } + default: { + const exhaustive: never = command + return exhaustive + } + } + } + + private async rotateKey(): Promise { + if (!this.credentials) { + this.notice("Sign in before changing the gateway key.") + return + } + const generation = this.accountGeneration + const next = { ...this.credentials, gatewayKey: newGatewayKey() } + await this.writeCredentials(async () => { + if (!this.disposed && generation === this.accountGeneration) + await this.options.vault.save(next) + }) + if (!this.isAccountCurrent(generation)) return + this.credentials = next + this.configurationRevision++ + this.probeAbort?.abort() + if (this.state.probe.kind === "running") + this.publish({ probe: { kind: "idle" } }) + if (this.owned) this.own(this.owned.process.stop()) + await this.convergeGateway() + this.notice( + "Gateway key changed. Copy fresh configurations for your agents.", + "info", + ) + } + + private async handleClipboardCommand( + command: Extract, + ): Promise { + const result = createClipboardText({ + command, + port: this.state.settings.port, + gatewayKey: this.credentials?.gatewayKey ?? null, + deviceCode: + this.state.account.kind === "awaiting-authorization" ? + this.state.account.code + : null, + models: this.state.models, + }) + if (result.kind === "error") this.notice(result.message) + else await this.options.writeClipboard(result.text) + } + async restore(): Promise { + try { + const stored = await this.options.vault.load() + if (stored) { + this.credentials = credentialsSchema.parse(stored) + const generation = this.invalidateAccount() + const abort = new AbortController() + this.accountAbort = abort + this.desiredGateway = this.state.settings.startOnLaunch + this.publish({ account: { kind: "connecting" } }) + this.own( + this.finishAuthentication( + this.credentials.githubToken, + generation, + abort.signal, + ).catch((error: unknown) => { + if (this.isAccountCurrent(generation, abort.signal)) { + this.credentials = null + this.publish({ + account: { kind: "error", message: signInError(error) }, + }) + } + }), + ) + } + } catch { + this.credentials = null + this.notice("Saved credentials could not be restored. Sign in again.") + } + } + subscribe = (listener: (snapshot: DesktopSnapshot) => void) => { + this.listeners.add(listener) + return () => this.listeners.delete(listener) + } + dispose = (): Promise => { + this.disposal ??= (async () => { + this.disposed = true + this.invalidateAccount() + this.listeners.clear() + await this.setGatewayDesired(false) + await Promise.allSettled([ + ...this.background, + this.settingsWrites, + this.storage, + ]) + this.credentials = null + })() + return this.disposal + } +} +export async function createController( + options: ControllerOptions, +): Promise { + const settingsStore = createSettingsStore( + options.dataDirectory, + options.platform === "win32" ? "powershell" : "posix", + ) + const loaded = await settingsStore.load() + const controller = new Controller(options, settingsStore, loaded) + await controller.restore() + return controller +} diff --git a/desktop/main/failure-messages.ts b/desktop/main/failure-messages.ts new file mode 100644 index 0000000..6481aa4 --- /dev/null +++ b/desktop/main/failure-messages.ts @@ -0,0 +1,32 @@ +import { UpstreamError } from "../engine/upstream" +import { DeviceFlowError } from "./oauth" +import { ProxyProcessError } from "./proxy-process" + +const gatewayMessages: Record = { + "address-in-use": + "This port is already in use. Choose another port in Settings.", + authentication: "Copilot could not verify this account. Sign in again.", + upstream: "Copilot is unavailable. Check your connection and try again.", + startup: "The gateway could not start. Try again.", + stopped: "The gateway stopped.", +} + +const deviceMessages: Record = { + expired: "Your device code expired. Start sign-in again.", + denied: + "GitHub authorization was denied. Start sign-in again when you are ready.", + failed: "GitHub could not authorize this device. Try signing in again.", +} + +export function gatewayError(error: unknown): string { + return error instanceof ProxyProcessError ? + gatewayMessages[error.kind] + : gatewayMessages.startup +} + +export function signInError(error: unknown): string { + if (error instanceof DeviceFlowError) return deviceMessages[error.kind] + return error instanceof UpstreamError && error.kind === "unauthorized" ? + "GitHub could not verify this account. Sign in again." + : "Could not reach GitHub. Check your connection and try signing in again." +} diff --git a/desktop/main/index.ts b/desktop/main/index.ts new file mode 100644 index 0000000..4c85b72 --- /dev/null +++ b/desktop/main/index.ts @@ -0,0 +1,355 @@ +import { + app, + BrowserWindow, + clipboard, + dialog, + ipcMain, + Menu, + nativeImage, + safeStorage, + session, + shell, + Tray, +} from "electron" +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import { pathToFileURL } from "node:url" + +import type { DesktopSnapshot } from "../shared/contracts" +import type { DesktopController } from "./types" + +import { IPC_CHANNELS } from "../shared/contracts" +import { isolateFixturePort } from "../testing/settings" +import { startFixtureUpstream } from "../testing/upstream" +import { createController } from "./controller" +import { selectDataDirectory } from "./profile" +import { createCredentialVault } from "./vault" + +app.setName("Copilot Bridge") +app.setAppUserModelId("com.voidsteed.copilotbridge") + +function argument(name: string): string | undefined { + const index = process.argv.indexOf(name) + return index === -1 ? undefined : process.argv[index + 1] +} + +const fixtureMode = process.argv.includes("--fixture") +const smokeOutput = argument("--smoke-output") +const dataDirectory = selectDataDirectory({ + fixture: fixtureMode, + explicit: argument("--data-dir"), +}) +if (dataDirectory) app.setPath("userData", path.resolve(dataDirectory)) + +let window: BrowserWindow | null = null +let tray: Tray | null = null +let controller: DesktopController | null = null +let stopFixture: (() => Promise) | undefined +let shuttingDown = false +let allowQuit = false +let exitStatus = 0 + +function markFailure(): void { + exitStatus = 1 +} + +function platform(): DesktopSnapshot["platform"] { + switch (process.platform) { + case "darwin": { + return "darwin" + } + case "win32": { + return "win32" + } + case "linux": { + return "linux" + } + default: { + throw new Error("Copilot Bridge supports macOS, Windows, and Linux.") + } + } +} + +function showWindow(): void { + if (!window || window.isDestroyed()) return + if (window.isMinimized()) window.restore() + window.show() + window.focus() +} + +async function applyLoginItem(enabled: boolean): Promise { + if (fixtureMode) return + if (!app.isPackaged) + throw new Error("Install the app before enabling launch at login.") + if (process.platform !== "linux") { + app.setLoginItemSettings({ openAtLogin: enabled }) + return + } + const directory = path.join( + process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), + "autostart", + ) + const filename = path.join(directory, "copilot-bridge.desktop") + if (!enabled) { + await fs.unlink(filename).catch((error: unknown) => { + if ( + !(error instanceof Error) + || !("code" in error) + || error.code !== "ENOENT" + ) + throw error + }) + return + } + const executable = (process.env.APPIMAGE || app.getPath("exe")).replaceAll( + /[\\"`$]/g, + String.raw`\$&`, + ) + await fs.mkdir(directory, { recursive: true }) + await fs.writeFile( + filename, + [ + "[Desktop Entry]", + "Type=Application", + "Name=Copilot Bridge", + `Exec="${executable}"`, + "Terminal=false", + "X-GNOME-Autostart-enabled=true", + "", + ].join("\n"), + { mode: 0o600 }, + ) +} + +function updateTray(snapshot: DesktopSnapshot): void { + if (!tray || tray.isDestroyed()) return + const running = snapshot.gateway.kind === "running" + tray.setToolTip( + `Copilot Bridge: ${running ? "Gateway running" : "Gateway stopped"}`, + ) + tray.setContextMenu( + Menu.buildFromTemplate([ + { label: "Open Copilot Bridge", click: showWindow }, + { type: "separator" }, + { + label: running ? "Stop gateway" : "Start gateway", + enabled: + snapshot.account.kind === "signed-in" + && snapshot.gateway.kind !== "starting" + && snapshot.gateway.kind !== "stopping", + click: () => { + void controller?.invoke({ + kind: running ? "stop-gateway" : "start-gateway", + }) + }, + }, + { type: "separator" }, + { label: "Quit Copilot Bridge", click: () => app.quit() }, + ]), + ) +} + +async function createNativeController(): Promise { + const fixture = fixtureMode ? await startFixtureUpstream() : null + if (fixture) stopFixture = () => fixture.close() + const storageAvailable = + !fixtureMode + && (await safeStorage.isAsyncEncryptionAvailable()) + && (process.platform !== "linux" + || safeStorage.getSelectedStorageBackend() !== "basic_text") + const directory = app.getPath("userData") + const runtimePath = + app.isPackaged ? + path.join( + process.resourcesPath, + "app.asar.unpacked", + "dist", + "engine.cjs", + ) + : path.join(__dirname, "engine.cjs") + const companion = await createController({ + dataDirectory: directory, + runtimePath, + runtimeExecPath: process.execPath, + fixtureOrigin: fixture?.origin, + platform: platform(), + version: app.getVersion(), + vault: createCredentialVault({ + directory, + encryption: + storageAvailable ? + { + encrypt: (value) => safeStorage.encryptStringAsync(value), + decrypt: (value) => safeStorage.decryptStringAsync(value), + } + : null, + }), + writeClipboard: (text) => clipboard.writeText(text), + async openExternal(url) { + if (url !== "https://github.com/login/device") + throw new Error("Only GitHub sign-in can be opened.") + if (!fixtureMode) await shell.openExternal(url) + }, + applyLoginItem, + }) + if (fixtureMode) await isolateFixturePort(companion) + return companion +} + +async function start(): Promise { + await app.whenReady() + controller = await createNativeController() + session.defaultSession.setPermissionRequestHandler( + (_contents, _permission, callback) => callback(false), + ) + const rendererFile = path.join(__dirname, "renderer", "index.html") + const rendererUrl = pathToFileURL(rendererFile).href + window = new BrowserWindow({ + width: 1180, + height: 820, + minWidth: 760, + minHeight: 600, + show: false, + title: "Copilot Bridge", + backgroundColor: "#f5f7f8", + ...(process.platform === "darwin" ? + { + titleBarStyle: "hiddenInset" as const, + trafficLightPosition: { x: 17, y: 17 }, + } + : {}), + icon: path.join(app.getAppPath(), "assets", "icon.png"), + webPreferences: { + preload: path.join(__dirname, "preload.cjs"), + sandbox: true, + contextIsolation: true, + nodeIntegration: false, + webSecurity: true, + spellcheck: false, + devTools: !app.isPackaged, + }, + }) + const appWindow = window + const companion = controller + function trusted(event: Electron.IpcMainInvokeEvent): boolean { + return ( + !appWindow.isDestroyed() + && event.sender === appWindow.webContents + && event.senderFrame === appWindow.webContents.mainFrame + && event.senderFrame.url === rendererUrl + ) + } + ipcMain.handle(IPC_CHANNELS.snapshot, (event) => { + if (!trusted(event)) throw new Error("Untrusted application frame.") + return companion.snapshot() + }) + ipcMain.handle(IPC_CHANNELS.command, (event, command: unknown) => { + if (!trusted(event)) throw new Error("Untrusted application frame.") + return companion.invoke(command) + }) + const unsubscribe = companion.subscribe((snapshot) => { + if (!appWindow.isDestroyed() && !appWindow.webContents.isDestroyed()) { + appWindow.webContents.send(IPC_CHANNELS.update, snapshot) + } + updateTray(snapshot) + }) + appWindow.once("closed", unsubscribe) + appWindow.webContents.setWindowOpenHandler(() => ({ action: "deny" })) + appWindow.webContents.on("will-navigate", (event, url) => { + if (url !== rendererUrl) event.preventDefault() + }) + appWindow.on("close", (event) => { + if ( + !shuttingDown + && companion.snapshot().settings.closeToTray + && tray + && !tray.isDestroyed() + ) { + event.preventDefault() + appWindow.hide() + } + }) + try { + const trayIcon = nativeImage + .createFromPath( + path.join( + app.getAppPath(), + "assets", + process.platform === "darwin" ? "trayTemplate.png" : "icon.png", + ), + ) + .resize({ width: 20, height: 20 }) + if (!trayIcon.isEmpty()) { + if (process.platform === "darwin") trayIcon.setTemplateImage(true) + tray = new Tray(trayIcon) + tray.on("click", showWindow) + updateTray(companion.snapshot()) + } + } catch { + tray = null + } + await appWindow.loadFile(rendererFile) + appWindow.show() + app.on("activate", showWindow) + + await smokeIfRequested(appWindow, companion) +} + +async function smokeIfRequested( + appWindow: BrowserWindow, + companion: DesktopController, +): Promise { + if (fixtureMode && smokeOutput) { + const { runNativeSmoke } = await import("./smoke") + try { + await runNativeSmoke({ + window: appWindow, + controller: companion, + outputDirectory: path.resolve(smokeOutput), + }) + } catch (error) { + await fs.mkdir(smokeOutput, { recursive: true }) + await fs.writeFile( + path.join(smokeOutput, "failure.txt"), + error instanceof Error ? error.message : "Native smoke failed", + ) + markFailure() + } + app.quit() + } +} + +if (!app.requestSingleInstanceLock()) { + app.quit() +} else { + app.on("second-instance", showWindow) + app.on("window-all-closed", () => { + if (!tray || !controller?.snapshot().settings.closeToTray) app.quit() + }) + app.on("before-quit", (event) => { + if (allowQuit) return + event.preventDefault() + if (shuttingDown) return + shuttingDown = true + void (async () => { + await controller?.dispose() + await stopFixture?.() + })() + .catch(() => { + markFailure() + }) + .finally(() => { + tray?.destroy() + allowQuit = true + app.exit(exitStatus) + }) + }) + void start().catch((error: unknown) => { + const message = + error instanceof Error ? error.message : "The app could not start." + if (!smokeOutput) dialog.showErrorBox("Copilot Bridge", message) + markFailure() + app.quit() + }) +} diff --git a/desktop/main/oauth.ts b/desktop/main/oauth.ts new file mode 100644 index 0000000..4e83d9e --- /dev/null +++ b/desktop/main/oauth.ts @@ -0,0 +1,149 @@ +import { z } from "zod" + +import { GITHUB_CLIENT_ID } from "../../src/lib/api-config" +import { fetchUpstreamJson, type UpstreamOptions } from "../engine/upstream" + +export const DEVICE_PAGE = "https://github.com/login/device" + +const deviceSchema = z.object({ + device_code: z.string().min(1).max(4096), + user_code: z + .string() + .min(1) + .max(80) + .regex(/^[A-Z0-9-]+$/i), + verification_uri: z.literal(DEVICE_PAGE), + expires_in: z.number().positive().max(86_400), + interval: z.number().positive().max(3600).default(5), +}) + +const pollSchema = z.union([ + z.object({ access_token: z.string().min(1).max(16_384) }), + z.object({ + error: z.string().max(80), + interval: z.number().positive().optional(), + }), +]) + +export class DeviceFlowError extends Error { + readonly kind: "expired" | "denied" | "failed" + + constructor(kind: DeviceFlowError["kind"]) { + super(kind) + this.kind = kind + } +} + +export function waitForPoll( + delayMs: number, + signal: AbortSignal, +): Promise { + signal.throwIfAborted() + return new Promise((resolve, reject) => { + const abort = () => { + clearTimeout(timer) + reject(new DOMException("Cancelled", "AbortError")) + } + const timer = setTimeout(() => { + signal.removeEventListener("abort", abort) + resolve() + }, delayMs) + signal.addEventListener("abort", abort, { once: true }) + }) +} + +export async function authorizeDevice( + options: UpstreamOptions & { + onCode(code: { + code: string + verificationUri: typeof DEVICE_PAGE + expiresAt: string + }): void + }, +): Promise { + const device = deviceSchema.parse( + await fetchUpstreamJson({ + ...options, + url: "https://github.com/login/device/code", + init: { + method: "POST", + headers: { + "content-type": "application/json", + accept: "application/json", + }, + body: JSON.stringify({ + client_id: GITHUB_CLIENT_ID, + scope: "read:user", + }), + }, + }), + ) + const expiresAt = Date.now() + device.expires_in * 1000 + options.signal.throwIfAborted() + options.onCode({ + code: device.user_code, + verificationUri: DEVICE_PAGE, + expiresAt: new Date(expiresAt).toISOString(), + }) + let intervalMs = device.interval * 1000 + + while (Date.now() < expiresAt) { + await waitForPoll( + Math.min(intervalMs, expiresAt - Date.now()), + options.signal, + ) + if (Date.now() >= expiresAt) throw new DeviceFlowError("expired") + const expiry = AbortSignal.timeout(Math.max(1, expiresAt - Date.now())) + let response: z.infer + try { + response = pollSchema.parse( + await fetchUpstreamJson({ + ...options, + signal: AbortSignal.any([options.signal, expiry]), + url: "https://github.com/login/oauth/access_token", + init: { + method: "POST", + headers: { + "content-type": "application/json", + accept: "application/json", + }, + body: JSON.stringify({ + client_id: GITHUB_CLIENT_ID, + device_code: device.device_code, + grant_type: "urn:ietf:params:oauth:grant-type:device_code", + }), + }, + }), + ) + } catch (error) { + options.signal.throwIfAborted() + if (Date.now() >= expiresAt || expiry.aborted) + throw new DeviceFlowError("expired") + throw error + } + options.signal.throwIfAborted() + if ("access_token" in response) return response.access_token + switch (response.error) { + case "authorization_pending": { + break + } + case "slow_down": { + intervalMs = Math.max( + intervalMs + 5000, + (response.interval ?? 0) * 1000, + ) + break + } + case "expired_token": { + throw new DeviceFlowError("expired") + } + case "access_denied": { + throw new DeviceFlowError("denied") + } + default: { + throw new DeviceFlowError("failed") + } + } + } + throw new DeviceFlowError("expired") +} diff --git a/desktop/main/preload.ts b/desktop/main/preload.ts new file mode 100644 index 0000000..9b88e66 --- /dev/null +++ b/desktop/main/preload.ts @@ -0,0 +1,31 @@ +import { contextBridge, ipcRenderer } from "electron" + +import type { DesktopBridge, DesktopCommand } from "../shared/contracts" + +import { IPC_CHANNELS, snapshotSchema } from "../shared/contracts" + +const bridge: DesktopBridge = { + async snapshot() { + const snapshot: unknown = await ipcRenderer.invoke(IPC_CHANNELS.snapshot) + return snapshotSchema.parse(snapshot) + }, + async invoke(command: DesktopCommand) { + const snapshot: unknown = await ipcRenderer.invoke( + IPC_CHANNELS.command, + command, + ) + return snapshotSchema.parse(snapshot) + }, + subscribe(listener) { + const handler = (_event: Electron.IpcRendererEvent, input: unknown) => { + const result = snapshotSchema.safeParse(input) + if (result.success) listener(result.data) + } + ipcRenderer.on(IPC_CHANNELS.update, handler) + return () => { + ipcRenderer.removeListener(IPC_CHANNELS.update, handler) + } + }, +} + +contextBridge.exposeInMainWorld("copilotBridge", bridge) diff --git a/desktop/main/probe.ts b/desktop/main/probe.ts new file mode 100644 index 0000000..3e3f383 --- /dev/null +++ b/desktop/main/probe.ts @@ -0,0 +1,91 @@ +import { z } from "zod" + +import type { DesktopModel, DesktopSnapshot } from "../shared/contracts" + +const probeReplySchema = z.object({ + output_text: z.string().optional(), + choices: z + .array( + z.object({ + message: z + .object({ content: z.string().nullable().optional() }) + .optional(), + }), + ) + .optional(), + output: z + .array( + z.object({ + content: z.array(z.object({ text: z.string().optional() })).optional(), + }), + ) + .optional(), +}) + +function extractProbeText(raw: unknown): string { + const reply = probeReplySchema.parse(raw) + const text = + reply.output_text + ?? reply.choices?.[0]?.message?.content + ?? reply.output + ?.flatMap( + (output) => output.content?.map((part) => part.text ?? "") ?? [], + ) + .join("") + ?? "" + if (!text) throw new Error("empty-response") + return text.slice(0, 16_000) +} + +export async function executeProbe(options: { + baseUrl: string + gatewayKey: string + model: DesktopModel + prompt: string + signal: AbortSignal +}): Promise> { + const { model, prompt } = options + const started = performance.now() + const usesChat = model.endpoints.includes("/chat/completions") + if (!usesChat && !model.endpoints.includes("/responses")) + throw new Error("unsupported") + const response = await fetch( + `${options.baseUrl}/${usesChat ? "chat/completions" : "responses"}`, + { + method: "POST", + redirect: "error", + headers: { + authorization: `Bearer ${options.gatewayKey}`, + "content-type": "application/json", + }, + body: JSON.stringify( + usesChat ? + { + model: model.id, + messages: [{ role: "user", content: prompt }], + stream: false, + max_tokens: 128, + } + : { + model: model.id, + input: prompt, + stream: false, + max_output_tokens: 128, + }, + ), + signal: AbortSignal.any([options.signal, AbortSignal.timeout(30_000)]), + }, + ) + if (!response.ok) { + await response.body?.cancel() + throw new Error("request-failed") + } + const raw = await response.text() + if (raw.length > 200_000) throw new Error("large-response") + return { + kind: "complete", + model: model.id, + text: extractProbeText(JSON.parse(raw) as unknown), + durationMs: performance.now() - started, + } +} diff --git a/desktop/main/profile.ts b/desktop/main/profile.ts new file mode 100644 index 0000000..4df09e0 --- /dev/null +++ b/desktop/main/profile.ts @@ -0,0 +1,13 @@ +import { mkdtempSync } from "node:fs" +import { tmpdir } from "node:os" +import path from "node:path" + +export function selectDataDirectory(options: { + fixture: boolean + explicit?: string +}): string | undefined { + if (options.explicit) return path.resolve(options.explicit) + if (options.fixture) + return mkdtempSync(path.join(tmpdir(), "copilot-bridge-fixture-")) + return undefined +} diff --git a/desktop/main/proxy-process.ts b/desktop/main/proxy-process.ts new file mode 100644 index 0000000..87cabc9 --- /dev/null +++ b/desktop/main/proxy-process.ts @@ -0,0 +1,241 @@ +import { fork, type ChildProcess } from "node:child_process" + +import type { ModelsResponse } from "../../src/services/copilot/get-models" +import type { ActivityRecord } from "../shared/contracts" + +import { + engineEventSchema, + engineInitSchema, + type EngineInit, + type EngineEvent, +} from "../engine/protocol" + +export class ProxyProcessError extends Error { + readonly kind: + | "address-in-use" + | "authentication" + | "upstream" + | "startup" + | "stopped" + + constructor(kind: ProxyProcessError["kind"]) { + super(kind) + this.kind = kind + } +} + +export interface ProxyProcess { + readonly ready: Promise + updateModels(models: ModelsResponse): Promise + stop(): Promise +} + +function childEnvironment(): NodeJS.ProcessEnv { + const environment: NodeJS.ProcessEnv = { ELECTRON_RUN_AS_NODE: "1" } + for (const key of [ + "PATH", + "SystemRoot", + "SYSTEMROOT", + "WINDIR", + "TEMP", + "TMP", + "TMPDIR", + "LANG", + "LC_ALL", + "TZ", + ]) { + if (process.env[key] !== undefined) environment[key] = process.env[key] + } + return environment +} + +export function startProxyProcess(options: { + runtimePath: string + runtimeExecPath?: string + init: EngineInit + onActivity(record: ActivityRecord): void + onExit(expected: boolean): void +}): ProxyProcess { + const init = engineInitSchema.parse(options.init) + const child = fork(options.runtimePath, [], { + execPath: options.runtimeExecPath ?? process.execPath, + execArgv: [], + env: childEnvironment(), + stdio: ["ignore", "ignore", "ignore", "ipc"], + serialization: "json", + }) + const ready = Promise.withResolvers() + const exited = Promise.withResolvers() + void ready.promise.catch(() => undefined) + let didExit = false + let expectedExit = false + let stopPromise: Promise | undefined + let stopTimer: ReturnType | undefined + let killTimer: ReturnType | undefined + const catalog = createCatalogChannel( + child, + ready.promise, + () => expectedExit || didExit, + ) + const startupTimer = setTimeout(() => { + ready.reject(new ProxyProcessError("startup")) + void stop() + }, 30_000) + + function finish(): void { + if (didExit) return + didExit = true + clearTimeout(startupTimer) + clearTimeout(stopTimer) + clearTimeout(killTimer) + ready.reject(new ProxyProcessError(expectedExit ? "stopped" : "startup")) + catalog.close() + exited.resolve(undefined) + options.onExit(expectedExit) + } + + function stop(): Promise { + if (stopPromise) return stopPromise + expectedExit = true + ready.reject(new ProxyProcessError("stopped")) + clearTimeout(startupTimer) + if (didExit) return Promise.resolve() + stopPromise = exited.promise + if (child.connected) child.send({ kind: "stop" }, () => undefined) + stopTimer = setTimeout(() => child.kill("SIGTERM"), 1500) + killTimer = setTimeout(() => child.kill("SIGKILL"), 3000) + return stopPromise + } + + child.once("spawn", () => { + child.send(init, (error) => { + if (error) { + ready.reject(new ProxyProcessError("startup")) + void stop() + } + }) + }) + child.on("message", (input: unknown) => + handleEngineMessage(input, { + ready: () => { + clearTimeout(startupTimer) + ready.resolve(undefined) + }, + activity: (record) => options.onActivity(record), + stopped: () => { + if (child.connected) child.send({ kind: "exit" }, () => undefined) + }, + catalog: (id) => catalog.acknowledge(id), + error: (code) => ready.reject(new ProxyProcessError(code)), + invalid: () => { + ready.reject(new ProxyProcessError("startup")) + void stop() + }, + }), + ) + child.once("error", () => { + ready.reject(new ProxyProcessError("startup")) + if (!child.pid) finish() + else void stop() + }) + child.once("exit", finish) + return { + ready: ready.promise, + stop, + updateModels: (models) => catalog.update(models), + } +} + +function createCatalogChannel( + child: ChildProcess, + ready: Promise, + stopping: () => boolean, +) { + let sequence = 0 + const updates = new Map< + number, + { + resolve(): void + reject(error: Error): void + timer: ReturnType + } + >() + return { + acknowledge(id: number): void { + const update = updates.get(id) + if (!update) return + clearTimeout(update.timer) + updates.delete(id) + update.resolve() + }, + close(): void { + for (const update of updates.values()) { + clearTimeout(update.timer) + update.reject(new ProxyProcessError("stopped")) + } + updates.clear() + }, + async update(models: ModelsResponse): Promise { + await ready + if (stopping() || !child.connected) throw new ProxyProcessError("stopped") + const id = ++sequence + await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + updates.delete(id) + reject(new ProxyProcessError("startup")) + }, 5000) + updates.set(id, { resolve, reject, timer }) + child.send({ kind: "catalog", id, models }, (error) => { + if (!error) return + clearTimeout(timer) + updates.delete(id) + reject(new ProxyProcessError("startup")) + }) + }) + }, + } +} + +function handleEngineMessage( + input: unknown, + handlers: { + ready(): void + activity(record: ActivityRecord): void + stopped(): void + catalog(id: number): void + error(code: Extract["code"]): void + invalid(): void + }, +): void { + const parsed = engineEventSchema.safeParse(input) + if (!parsed.success) { + handlers.invalid() + return + } + switch (parsed.data.kind) { + case "ready": { + handlers.ready() + return + } + case "activity": { + handlers.activity(parsed.data.record) + return + } + case "stopped": { + handlers.stopped() + return + } + case "catalog-updated": { + handlers.catalog(parsed.data.id) + return + } + case "error": { + handlers.error(parsed.data.code) + return + } + default: { + const exhaustive: never = parsed.data + return exhaustive + } + } +} diff --git a/desktop/main/settings-store.ts b/desktop/main/settings-store.ts new file mode 100644 index 0000000..8be9b51 --- /dev/null +++ b/desktop/main/settings-store.ts @@ -0,0 +1,73 @@ +import { randomUUID } from "node:crypto" +import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises" +import { join } from "node:path" + +import type { ControllerOptions } from "./types" + +import { type DesktopSettings, settingsSchema } from "../shared/contracts" + +export async function persistSettings( + options: Pick & { + previous: DesktopSettings + next: DesktopSettings + save(settings: DesktopSettings): Promise + }, +): Promise<"saved" | "unavailable" | "failed"> { + const launchChanged = + options.next.launchAtLogin !== options.previous.launchAtLogin + const apply = options.applyLoginItem + if (launchChanged && !apply) return "unavailable" + try { + if (launchChanged && apply) await apply(options.next.launchAtLogin) + await options.save(options.next) + return "saved" + } catch { + if (launchChanged && apply) { + try { + await apply(options.previous.launchAtLogin) + } catch { + return "failed" + } + } + return "failed" + } +} + +export function createSettingsStore( + dataDirectory: string, + defaultShell: DesktopSettings["shell"] = "posix", +) { + const path = join(dataDirectory, "settings.json") + const platformSettings = settingsSchema.extend({ + shell: settingsSchema.shape.shell.default(defaultShell), + }) + let writes = Promise.resolve() + return { + async load(): Promise<{ settings: DesktopSettings; recovered: boolean }> { + try { + const raw: unknown = JSON.parse(await readFile(path, "utf8")) + return { settings: platformSettings.parse(raw), recovered: false } + } catch (error) { + const missing = + error instanceof Error && "code" in error && error.code === "ENOENT" + return { settings: platformSettings.parse({}), recovered: !missing } + } + }, + save(settings: DesktopSettings): Promise { + const serialized = + JSON.stringify(settingsSchema.parse(settings), null, 2) + "\n" + const write = writes.then(async () => { + await mkdir(dataDirectory, { recursive: true, mode: 0o700 }) + const temporary = join(dataDirectory, `.settings-${randomUUID()}.tmp`) + try { + await writeFile(temporary, serialized, { mode: 0o600, flag: "wx" }) + await rename(temporary, path) + } finally { + await unlink(temporary).catch(() => undefined) + } + }) + writes = write.catch(() => undefined) + return write + }, + } +} diff --git a/desktop/main/smoke.ts b/desktop/main/smoke.ts new file mode 100644 index 0000000..a570d5b --- /dev/null +++ b/desktop/main/smoke.ts @@ -0,0 +1,428 @@ +import type { BrowserWindow } from "electron" + +import { clipboard } from "electron" +import assert from "node:assert/strict" +import fs from "node:fs/promises" +import path from "node:path" +import { z } from "zod" + +import type { DesktopController } from "./types" + +import { fixturePdf } from "../testing/pdf" +import { availablePort } from "../testing/settings" + +export async function runNativeSmoke(options: { + window: BrowserWindow + controller: DesktopController + outputDirectory: string +}): Promise { + const { window, controller, outputDirectory } = options + await fs.mkdir(outputDirectory, { recursive: true }) + const checks: Array = [] + const startedAt = new Date().toISOString() + + async function waitFor( + name: string, + predicate: () => boolean | Promise, + timeout = 30_000, + ): Promise { + const expires = Date.now() + timeout + while (Date.now() < expires) { + if (await predicate()) return + await new Promise((resolve) => setTimeout(resolve, 100)) + } + throw new Error(`Native smoke timed out: ${name}`) + } + + async function javascript(expression: string): Promise { + const result: unknown = await window.webContents.executeJavaScript( + expression, + true, + ) + return z.boolean().parse(result) + } + + async function click(selector: string): Promise { + await waitFor(`control ${selector}`, () => + javascript(`(() => { + const element = document.querySelector(${JSON.stringify(selector)}); + if (!(element instanceof HTMLElement) || element.hasAttribute('disabled') || !element.getClientRects().length) return false; + element.click(); return true; + })()`), + ) + } + + async function input(selector: string, value: string): Promise { + const applied = await javascript(`(() => { + const element = document.querySelector(${JSON.stringify(selector)}); + if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement)) return false; + const prototype = element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : element instanceof HTMLSelectElement ? HTMLSelectElement.prototype : HTMLInputElement.prototype; + Object.getOwnPropertyDescriptor(prototype, 'value').set.call(element, ${JSON.stringify(value)}); + element.dispatchEvent(new Event('input', { bubbles: true })); + element.dispatchEvent(new Event('change', { bubbles: true })); + return true; + })()`) + assert(applied, `Input ${selector} exists`) + } + + async function capture(name: string): Promise { + try { + assert(window.isVisible(), `Native window is visible for ${name}`) + let painted = false + window.webContents.beginFrameSubscription((image) => { + if (!image.isEmpty()) painted = true + }) + try { + window.webContents.invalidate() + await waitFor(`painted frame for ${name}`, () => painted, 10_000) + } finally { + window.webContents.endFrameSubscription() + } + const image = await window.webContents.capturePage() + assert(!image.isEmpty(), `Native screenshot ${name} is not empty`) + const png = image.toPNG() + assert(png.length > 0, `Native screenshot ${name} has PNG data`) + await fs.writeFile(path.join(outputDirectory, `${name}.png`), png) + } catch (error) { + const destroyed = window.isDestroyed() + const contents = destroyed ? undefined : window.webContents + await fs.writeFile( + path.join(outputDirectory, `${name}-capture-failure.json`), + JSON.stringify( + { + capture: name, + platform: process.platform, + electron: process.versions.electron, + chromium: process.versions.chrome, + error: + error instanceof Error ? + { name: error.name, message: error.message } + : { name: "UnknownError" }, + window: + destroyed ? + { destroyed: true } + : { + destroyed: false, + bounds: window.getBounds(), + contentBounds: window.getContentBounds(), + visible: window.isVisible(), + minimized: window.isMinimized(), + }, + renderer: + !contents || contents.isDestroyed() ? + { destroyed: true } + : { destroyed: false, loading: contents.isLoading() }, + }, + null, + 2, + ) + "\n", + ) + throw error + } + } + + try { + await waitFor("renderer bridge", () => + javascript( + "Boolean(window.copilotBridge && document.querySelector('[data-testid=sign-in]'))", + ), + ) + assert( + await javascript( + "typeof require === 'undefined' && typeof process === 'undefined'", + ), + ) + checks.push("Sandboxed native renderer loads without Node access") + await capture("native-signed-out") + + await click('[data-testid="sign-in"]') + await waitFor( + "device code", + () => controller.snapshot().account.kind === "awaiting-authorization", + ) + await capture("native-device-code") + await click('[data-testid="cancel-sign-in"]') + await waitFor( + "cancelled login", + () => controller.snapshot().account.kind === "signed-out", + ) + await new Promise((resolve) => setTimeout(resolve, 1200)) + assert.equal(controller.snapshot().account.kind, "signed-out") + checks.push( + "Rendered GitHub device sign-in cancels without late account restoration", + ) + + await click('[data-testid="sign-in"]') + await waitFor( + "approved login and gateway", + () => + controller.snapshot().account.kind === "signed-in" + && controller.snapshot().gateway.kind === "running", + ) + const connected = controller.snapshot() + assert.equal(connected.fixture, true) + assert(connected.models.length >= 5) + assert(connected.usage?.quotas.length) + const gateway = connected.gateway + assert.equal(gateway.kind, "running") + const originalBase = gateway.url + assert.equal((await fetch(`${originalBase}/models`)).status, 401) + checks.push( + "Approved fixture login loads the real catalog and quota path into the native window", + ) + await capture("native-overview") + + await click('[data-testid="nav-settings"]') + await click('button[aria-label="Copy local API key"]') + await waitFor( + "native clipboard key", + async () => (await clipboard.readText()).length >= 32, + ) + const gatewayKey = await clipboard.readText() + const headers = () => ({ + authorization: `Bearer ${gatewayKey}`, + "content-type": "application/json", + }) + assert.equal( + (await fetch(`${originalBase}/models`, { headers: headers() })).status, + 200, + ) + assert.equal( + ( + await fetch(`${originalBase.replace(/\/v1$/, "")}/token`, { + headers: headers(), + }) + ).status, + 404, + ) + assert.equal( + ( + await fetch(`${originalBase}/models`, { + headers: { ...headers(), origin: "https://example.com" }, + }) + ).status, + 403, + ) + checks.push( + "Native local key authenticates HTTP while token and browser-origin access are blocked", + ) + + await click('[data-testid="nav-overview"]') + await click('[data-testid="open-probe"]') + await input( + '[data-testid="probe-prompt"]', + "Reply with exactly: Copilot Bridge is connected.", + ) + await click('[data-testid="probe-send"]') + await waitFor( + "native test request", + () => controller.snapshot().probe.kind === "complete", + ) + const probe = controller.snapshot().probe + assert.equal(probe.kind, "complete") + assert.match(probe.text, /Copilot Bridge is connected/) + checks.push( + "Rendered test-request form sends through the owned gateway and shows a response", + ) + + const chatModel = connected.models.find((model) => + model.endpoints.includes("/chat/completions"), + ) + assert(chatModel) + const stream = await fetch(`${originalBase}/chat/completions`, { + method: "POST", + headers: headers(), + body: JSON.stringify({ + model: chatModel.id, + messages: [{ role: "user", content: "native streaming check" }], + stream: true, + }), + }) + assert.equal(stream.status, 200) + assert.match(await stream.text(), /\[DONE\]/) + await waitFor("completed stream activity", () => + controller + .snapshot() + .activity.some( + (entry) => entry.outputTokens === 9 && entry.inputTokens === 37, + ), + ) + checks.push( + "Actual Node child completes streaming POSTs and records final token usage", + ) + + const pdfResponse = await fetch(`${originalBase}/messages`, { + method: "POST", + headers: headers(), + body: JSON.stringify({ + model: chatModel.id, + max_tokens: 32, + messages: [ + { + role: "user", + content: [ + { + type: "document", + source: { + type: "base64", + media_type: "application/pdf", + data: fixturePdf().toString("base64"), + }, + }, + { type: "text", text: "Read the attached PDF." }, + ], + }, + ], + }), + }) + const pdfBody = await pdfResponse.text() + assert.equal( + pdfResponse.status, + 200, + `Packaged PDF extraction returned ${pdfResponse.status}: ${pdfBody.slice(0, 300)}`, + ) + checks.push( + "Native child loads the real PDF parser, native dependency, and worker assets", + ) + + await click('[data-testid="nav-models"]') + await input('[data-testid="model-search"]', "responses") + await waitFor("model search", () => + javascript( + "document.querySelectorAll('.model-table tbody tr, .models-table tbody tr').length <= 2", + ), + ) + await capture("native-models") + await click('[data-testid="nav-agents"]') + await click("#agent-tab-codex") + await click('[data-testid="copy-config"]') + await waitFor("native configuration clipboard", async () => + (await clipboard.readText()).includes('wire_api="responses"'), + ) + const copiedConfiguration = await clipboard.readText() + assert(copiedConfiguration.includes(gatewayKey)) + assert(!copiedConfiguration.includes("fixture_github_token")) + checks.push( + "Rendered agent setup copies an authenticated Responses configuration to the native clipboard", + ) + await capture("native-agent-setup") + + await click('[data-testid="nav-activity"]') + assert(controller.snapshot().activity.length >= 3) + await capture("native-activity") + const serializedActivity = JSON.stringify(controller.snapshot().activity) + assert(!serializedActivity.includes("fixture_github_token")) + assert(!serializedActivity.includes("fixture_copilot_token")) + assert(!serializedActivity.includes("native streaming check")) + await click('[data-testid="clear-activity"]') + await waitFor( + "cleared activity", + () => controller.snapshot().activity.length === 0, + ) + checks.push( + "Rendered activity reflects real requests, excludes content and credentials, and clears", + ) + + await click('[data-testid="nav-settings"]') + await input('[data-testid="settings-port"]', "0") + await waitFor("invalid port", () => + javascript( + "document.querySelector('[data-testid=save-settings]').disabled === true", + ), + ) + const nextPort = await availablePort() + await input('[data-testid="settings-port"]', String(nextPort)) + await javascript( + "Array.from(document.querySelectorAll('button[role=radio]')).find(button => button.textContent.trim() === 'Dark').click(); true", + ) + await click('[data-testid="save-settings"]') + await waitFor( + "settings and restart", + () => + controller.snapshot().settings.port === nextPort + && controller.snapshot().settings.theme === "dark" + && controller.snapshot().gateway.kind === "running", + ) + await assert.rejects(() => + fetch(`${originalBase}/models`, { + headers: headers(), + signal: AbortSignal.timeout(2000), + }), + ) + await capture("native-settings-dark") + checks.push( + "Rendered settings reject an invalid port and restart on a saved port without retaining the old listener", + ) + + const beforeReload = controller.snapshot().revision + window.webContents.reload() + await waitFor("native renderer reload", () => + javascript( + "document.documentElement.dataset.theme === 'dark' && Boolean(document.querySelector('[data-testid=nav-settings]'))", + ), + ) + assert(controller.snapshot().revision >= beforeReload) + checks.push( + "Window reload restores the saved appearance and current native state", + ) + + await click('[data-testid="nav-overview"]') + await click('[data-testid="stop-gateway"]') + await waitFor( + "stopped gateway", + () => controller.snapshot().gateway.kind === "stopped", + ) + await assert.rejects(() => + fetch(`http://127.0.0.1:${nextPort}/v1/models`, { + signal: AbortSignal.timeout(2000), + }), + ) + checks.push("Rendered Stop action releases the real loopback port") + await click('[data-testid="start-gateway"]') + await waitFor( + "gateway restarted", + () => controller.snapshot().gateway.kind === "running", + ) + await click('[data-testid="nav-settings"]') + await click('[data-testid="sign-out"]') + await waitFor( + "signed-out cleanup", + () => + controller.snapshot().account.kind === "signed-out" + && controller.snapshot().gateway.kind === "stopped", + ) + await assert.rejects(() => + fetch(`http://127.0.0.1:${nextPort}/v1/models`, { + signal: AbortSignal.timeout(2000), + }), + ) + checks.push( + "Rendered Sign out stops the child and clears account/model state", + ) + assert.equal(controller.snapshot().models.length, 0) + await fs.writeFile( + path.join(outputDirectory, "smoke.json"), + JSON.stringify( + { + passed: true, + mode: "fixture", + platform: process.platform, + electron: process.versions.electron, + node: process.versions.node, + startedAt, + completedAt: new Date().toISOString(), + checks, + limitations: [ + "Controlled upstream services; no live Copilot inference or GitHub consent", + "Session-only fixture vault; OS credential encryption requires a live user session", + ], + }, + null, + 2, + ) + "\n", + ) + } catch (error) { + await capture("native-failure").catch(() => undefined) + throw error + } +} diff --git a/desktop/main/snapshot.ts b/desktop/main/snapshot.ts new file mode 100644 index 0000000..01dddcb --- /dev/null +++ b/desktop/main/snapshot.ts @@ -0,0 +1,57 @@ +import type { ActivityRecord, DesktopSnapshot } from "../shared/contracts" + +export function appendActivity( + snapshot: Pick, + record: ActivityRecord, +): Pick { + return { + activity: [record, ...snapshot.activity].slice(0, 200), + totals: { + requests: snapshot.totals.requests + 1, + errors: snapshot.totals.errors + (record.outcome === "failed" ? 1 : 0), + inputTokens: snapshot.totals.inputTokens + (record.inputTokens ?? 0), + outputTokens: snapshot.totals.outputTokens + (record.outputTokens ?? 0), + cachedTokens: snapshot.totals.cachedTokens + (record.cachedTokens ?? 0), + }, + } +} + +export function initialSnapshot( + options: Pick< + DesktopSnapshot, + "version" | "platform" | "fixture" | "storageMode" | "settings" + > & { + recovered: boolean + }, +): DesktopSnapshot { + return { + revision: 0, + version: options.version, + platform: options.platform, + fixture: options.fixture, + storageMode: options.storageMode, + account: { kind: "signed-out" }, + gateway: { kind: "stopped" }, + settings: options.settings, + models: [], + modelsFetchedAt: null, + usage: null, + activity: [], + totals: { + requests: 0, + errors: 0, + inputTokens: 0, + outputTokens: 0, + cachedTokens: 0, + }, + probe: { kind: "idle" }, + notice: + options.recovered ? + { + kind: "error", + message: + "Saved settings could not be read. Default settings are active.", + } + : null, + } +} diff --git a/desktop/main/types.ts b/desktop/main/types.ts new file mode 100644 index 0000000..4ecfc7a --- /dev/null +++ b/desktop/main/types.ts @@ -0,0 +1,33 @@ +import type { DesktopSnapshot } from "../shared/contracts" + +export interface StoredCredentials { + githubToken: string + gatewayKey: string +} + +export interface CredentialVault { + readonly mode: DesktopSnapshot["storageMode"] + load(): Promise + save(credentials: StoredCredentials): Promise + clear(): Promise +} + +export interface ControllerOptions { + dataDirectory: string + runtimePath: string + runtimeExecPath?: string + fixtureOrigin?: string + platform: DesktopSnapshot["platform"] + version: string + vault: CredentialVault + writeClipboard(text: string): void | Promise + openExternal(url: string): Promise + applyLoginItem?(enabled: boolean): Promise +} + +export interface DesktopController { + snapshot(): DesktopSnapshot + invoke(input: unknown): Promise + subscribe(listener: (snapshot: DesktopSnapshot) => void): () => void + dispose(): Promise +} diff --git a/desktop/main/vault.ts b/desktop/main/vault.ts new file mode 100644 index 0000000..ce9239f --- /dev/null +++ b/desktop/main/vault.ts @@ -0,0 +1,102 @@ +import { randomUUID } from "node:crypto" +import fs from "node:fs/promises" +import path from "node:path" +import { z } from "zod" + +import type { CredentialVault, StoredCredentials } from "./types" + +const credentialsSchema = z + .object({ + githubToken: z.string().min(1), + gatewayKey: z.string().min(32), + }) + .strict() + +interface Encryption { + encrypt(value: string): Promise + decrypt(value: Buffer): Promise<{ result: string; shouldReEncrypt: boolean }> +} + +async function clearSavedCredentials(filename: string): Promise { + await fs.unlink(filename).catch((error: unknown) => { + if ( + !(error instanceof Error) + || !("code" in error) + || error.code !== "ENOENT" + ) { + throw new Error("Saved sign-in could not be removed.") + } + }) +} + +export function createCredentialVault(options: { + directory: string + encryption: Encryption | null +}): CredentialVault { + const filename = path.join(options.directory, "credentials.enc") + const encryption = options.encryption + if (!encryption) { + let credentials: StoredCredentials | null = null + return { + mode: "session-only", + load: () => Promise.resolve(credentials && { ...credentials }), + save(value) { + credentials = credentialsSchema.parse(value) + return Promise.resolve() + }, + async clear() { + credentials = null + await clearSavedCredentials(filename) + }, + } + } + + const save = async (credentials: StoredCredentials): Promise => { + const value = credentialsSchema.parse(credentials) + const ciphertext = await encryption.encrypt(JSON.stringify(value)) + const temporary = `${filename}.${randomUUID()}.tmp` + await fs.mkdir(options.directory, { recursive: true, mode: 0o700 }) + try { + await fs.writeFile(temporary, ciphertext, { mode: 0o600, flag: "wx" }) + await fs.rename(temporary, filename) + } finally { + await fs.unlink(temporary).catch((error: unknown) => { + if ( + !(error instanceof Error) + || !("code" in error) + || error.code !== "ENOENT" + ) + throw error + }) + } + } + + return { + mode: "encrypted", + async load() { + let bytes: Buffer + try { + bytes = await fs.readFile(filename) + } catch (error) { + if ( + error instanceof Error + && "code" in error + && error.code === "ENOENT" + ) + return null + throw new Error("Saved sign-in could not be read. Sign in again.") + } + try { + const decrypted = await encryption.decrypt(bytes) + const value: unknown = JSON.parse(decrypted.result) + if (decrypted.shouldReEncrypt) + await save(credentialsSchema.parse(value)) + return credentialsSchema.parse(value) + } catch { + throw new Error("Saved sign-in could not be unlocked. Sign in again.") + } + }, + save, + clear: () => clearSavedCredentials(filename), + } +} diff --git a/desktop/package.json b/desktop/package.json new file mode 100644 index 0000000..9f36116 --- /dev/null +++ b/desktop/package.json @@ -0,0 +1,39 @@ +{ + "name": "copilot-bridge", + "version": "0.0.1", + "private": true, + "description": "A desktop home for your GitHub Copilot gateway.", + "license": "MIT", + "author": "Yujun Liu", + "type": "module", + "main": "dist/main.cjs", + "scripts": { + "build": "node scripts/build.mjs", + "dev": "node scripts/dev.ts", + "package": "electron-builder --config electron-builder.config.mjs", + "preview": "node scripts/preview.mjs", + "smoke": "node scripts/smoke.ts", + "test": "bun test ./tests", + "typecheck": "tsc --noEmit -p tsconfig.json" + }, + "dependencies": { + "@hono/node-server": "^1.19.11", + "lucide-react": "^1.64.0", + "pdf-parse": "^2.4.5", + "react": "^19.3.1", + "react-dom": "^19.3.1", + "zod": "^4.3.6" + }, + "devDependencies": { + "@types/bun": "^1.3.11", + "@types/node": "^24.0.0", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^6.1.2", + "electron": "45.1.0", + "electron-builder": "^26.18.0", + "esbuild": "^0.28.2", + "typescript": "^5.9.3", + "vite": "^8.3.5" + } +} diff --git a/desktop/renderer/ActivityView.tsx b/desktop/renderer/ActivityView.tsx new file mode 100644 index 0000000..423da8b --- /dev/null +++ b/desktop/renderer/ActivityView.tsx @@ -0,0 +1,288 @@ +import { Activity, ChevronRight, Search, Trash2 } from "lucide-react" +import { Fragment, useState } from "react" + +import type { ActivityRecord } from "../shared/contracts" +import type { ViewProps } from "./App" + +import { + type BadgeTone, + Badge, + Button, + EmptyState, + formatDuration, + formatNumber, + formatTime, + formatTokens, +} from "./components" + +const outcomeTones: Record = { + completed: "success", + failed: "error", + cancelled: "warning", +} + +function statusLabel(record: ActivityRecord): string | number { + if (record.outcome === "cancelled") return "Cancelled" + return record.status > 0 ? record.status : "Failed" +} + +export function ActivityView({ + snapshot, + run, + pending, + onTest, +}: ViewProps & { onTest: () => void }) { + const [modelFilter, setModelFilter] = useState("") + const [outcomeFilter, setOutcomeFilter] = useState("") + const models = Array.from( + new Set( + snapshot.activity.flatMap((record) => + record.model ? [record.model] : [], + ), + ), + ).sort() + const filtered = snapshot.activity.filter( + (record) => + (!modelFilter || record.model === modelFilter) + && (!outcomeFilter || record.outcome === outcomeFilter), + ) + + return ( +
+
+
+

Session activity

+

{formatNumber(snapshot.totals.requests)} requests this session

+
+ +
+
+
+
+ + + + +
+ + {formatNumber(filtered.length)}{" "} + {filtered.length === 1 ? "request" : "requests"} + +
+ {snapshot.activity.length === 0 && ( + Send a test request} + > + Completed requests appear here while Copilot Bridge is open. + + )} + {snapshot.activity.length > 0 && filtered.length === 0 && ( + { + setModelFilter("") + setOutcomeFilter("") + }} + > + Clear filters + + } + > + Try another model or status. + + )} + {filtered.length > 0 && } +
+

+ Session history stores request metadata. Prompts and responses are not + saved. +

+
+ ) +} + +export function RequestTable({ + records, + compact = false, +}: { + records: Array + compact?: boolean +}) { + const [expandedId, setExpandedId] = useState(null) + + return ( +
+ + + + + + {!compact && } + + + + + + + {records.map((record) => { + const expanded = record.id === expandedId + const totalTokens = + record.inputTokens !== null && record.outputTokens !== null ? + record.inputTokens + record.outputTokens + : null + return ( + + + + + {!compact && ( + + )} + + + + + {expanded && ( + + + + )} + + ) + })} + +
TimeModelEndpointStatusTokensDuration
+ + + + {record.model ?? "Unknown model"} + + + + {record.path} + + + + {statusLabel(record)} + + + {formatTokens(totalTokens)} + + {formatDuration(record.durationMs)} +
+
+
+
Request ID
+
+ {record.id} +
+
+
+
Endpoint
+
+ + {record.method} {record.path} + +
+
+
+
Outcome
+
{record.outcome}
+
+
+
HTTP status
+
+ {record.status > 0 ? record.status : "No response"} +
+
+
+
Input tokens
+
{formatTokens(record.inputTokens)}
+
+
+
Output tokens
+
{formatTokens(record.outputTokens)}
+
+
+
Cached tokens
+
{formatTokens(record.cachedTokens)}
+
+
+
Duration
+
{formatDuration(record.durationMs)}
+
+
+
Started
+
{new Date(record.startedAt).toLocaleString()}
+
+
+
+
+ ) +} diff --git a/desktop/renderer/AgentsView.tsx b/desktop/renderer/AgentsView.tsx new file mode 100644 index 0000000..0f6c16a --- /dev/null +++ b/desktop/renderer/AgentsView.tsx @@ -0,0 +1,282 @@ +import { Box, Code2, Copy, KeyRound, LockKeyhole, Terminal } from "lucide-react" +import { useState } from "react" + +import type { AgentTarget, DesktopSettings } from "../shared/contracts" +import type { ViewProps } from "./App" + +import { + AGENTS, + createConnectionSnippet, + supportsAgent, +} from "../shared/connections" +import { + Badge, + Button, + EmptyState, + gatewayStatus, + ModelSelect, + nextTabIndex, +} from "./components" + +export function AgentsView({ snapshot, run, pending }: ViewProps) { + const [target, setTarget] = useState("claude-code") + const [modelId, setModelId] = useState(snapshot.settings.preferredModel) + const [shell, setShell] = useState( + snapshot.settings.shell, + ) + const models = snapshot.models.filter((model) => supportsAgent(model, target)) + const model = + models.find((entry) => entry.id === modelId) + ?? models.find((entry) => entry.id === snapshot.settings.preferredModel) + ?? models.at(0) + const agent = AGENTS.find((entry) => entry.id === target) + const snippet = + model ? + createConnectionSnippet({ + target, + model, + port: snapshot.settings.port, + apiKey: "", + shell, + }) + : "" + const signedIn = snapshot.account.kind === "signed-in" + const status = gatewayStatus(snapshot.gateway) + + return ( +
+
+
+

Connect an agent

+

Use your Copilot models in your terminal.

+
+ + {snapshot.gateway.kind === "running" ? + "Gateway ready" + : `Gateway ${status.label.toLowerCase()}`} + +
+
+
+ {AGENTS.map((entry) => ( + + ))} +
+
+
+
+

{agent?.name}

+ {agent?.protocol} +
+
+ {snapshot.models.length === 0 && ( + { + void run({ kind: signedIn ? "refresh" : "sign-in" }) + }} + > + {signedIn ? "Refresh models" : "Sign in with GitHub"} + + } + > + Your account's model catalog is used to build the configuration. + + )} + {snapshot.models.length > 0 && models.length === 0 && ( + { + void run({ kind: "refresh" }) + }} + busy={pending.has("refresh")} + > + Refresh models + + } + > + {target === "codex" ? + "Codex requires a model that supports the Responses API." + : "This agent requires a model that supports Chat Completions."} + + )} + {models.length > 0 && ( + <> +
+ { + setModelId(value) + void run({ kind: "select-model", modelId: value }) + }} + /> + +
+ {target === "codex" && ( +

+ Showing models with Responses API support. +

+ )} + {target === "opencode" && ( +

+ Showing models with Chat Completions support. +

+ )} +
+
+ + + + +
+
+                  {snippet}
+                
+
+
+

+

+ +
+
+ 1 +

+ {target === "curl" ? + "Paste and run this command in your terminal." + : `Paste these commands in your terminal to start ${agent?.name}.` + } +

+
+ {snapshot.gateway.kind !== "running" && ( +
+ 2 +

Start the gateway before sending requests.

+ +
+ )} + + )} +
+
+

+

+
+ ) +} diff --git a/desktop/renderer/App.tsx b/desktop/renderer/App.tsx new file mode 100644 index 0000000..899a498 --- /dev/null +++ b/desktop/renderer/App.tsx @@ -0,0 +1,513 @@ +import { + Activity, + Box, + Check, + ChevronRight, + Copy, + LayoutDashboard, + LoaderCircle, + LogIn, + Network, + RefreshCw, + Settings2, + ShieldCheck, + Terminal, + TriangleAlert, + X, + type LucideIcon, +} from "lucide-react" +import { useEffect, useState } from "react" + +import type { DesktopCommand, DesktopSnapshot } from "../shared/contracts" + +import { ActivityView } from "./ActivityView" +import { AgentsView } from "./AgentsView" +import { useDesktop, type RunCommand } from "./bridge" +import { Badge, Button, Dialog, EmptyState, gatewayStatus } from "./components" +import { Github } from "./Github" +import { ModelsView } from "./ModelsView" +import { OverviewView } from "./OverviewView" +import { SettingsView } from "./SettingsView" + +type View = "overview" | "models" | "agents" | "activity" | "settings" + +const navigation: Array<{ id: View; label: string; icon: LucideIcon }> = [ + { id: "overview", label: "Overview", icon: LayoutDashboard }, + { id: "models", label: "Models", icon: Box }, + { id: "agents", label: "Agents", icon: Terminal }, + { id: "activity", label: "Activity", icon: Activity }, + { id: "settings", label: "Settings", icon: Settings2 }, +] + +export type PendingCommands = ReadonlySet + +export interface ViewProps { + snapshot: DesktopSnapshot + run: RunCommand + pending: PendingCommands +} + +export function App() { + const desktop = useDesktop() + const [view, setView] = useState("overview") + const [testRequest, setTestRequest] = useState<{ + open: boolean + modelId: string + }>({ open: false, modelId: "" }) + const snapshot = + desktop.state.kind === "ready" ? desktop.state.snapshot : null + + useEffect(() => { + const theme = snapshot?.settings.theme ?? "light" + const media = globalThis.matchMedia("(prefers-color-scheme: dark)") + const applyTheme = () => { + const systemTheme = media.matches ? "dark" : "light" + document.documentElement.dataset.theme = + theme === "system" ? systemTheme : theme + } + applyTheme() + media.addEventListener("change", applyTheme) + return () => { + media.removeEventListener("change", applyTheme) + } + }, [snapshot?.settings.theme]) + + const status = snapshot ? gatewayStatus(snapshot.gateway) : null + const signedIn = snapshot?.account.kind === "signed-in" + const account = snapshot?.account + const signInBusy = + account?.kind === "connecting" + || account?.kind === "awaiting-authorization" + || desktop.pending.has("sign-in") + + const openTest = (modelId = "") => { + setTestRequest({ open: true, modelId }) + setView("overview") + } + + return ( +
+ + +
+
+
+

{navigation.find((item) => item.id === view)?.label}

+
+
+ {status && ( + + Gateway {status.label.toLowerCase()} + + )} + {signedIn ? + + } +
+
+ +
+
+ {snapshot?.fixture && ( +
+
+ )} + {snapshot?.storageMode === "session-only" && ( +
+
+ )} + {desktop.connectionError && ( +
+
+ )} + {desktop.actionError && ( +
+
+ )} + {snapshot?.notice && ( +
+
+ )} + {account?.kind === "error" && ( +
+
+ )} + + {desktop.state.kind === "loading" && ( +
+ +

Connecting to Copilot Bridge…

+
+ )} + {desktop.state.kind === "unavailable" && ( + { + void desktop.refreshSnapshot() + }} + > + Try again + + } + > + {desktop.state.message} + + )} + {snapshot && ( + <> + {view === "overview" && ( + { + setTestRequest((current) => ({ ...current, open })) + }} + onNavigate={setView} + /> + )} + {view === "models" && ( + { + setView("agents") + }} + onProbe={openTest} + /> + )} + {view === "agents" && ( + + )} + {view === "activity" && ( + { + openTest() + }} + /> + )} + {view === "settings" && ( + + )} + + )} +
+
+
+ + {desktop.confirmation && ( +
+
+ )} + {snapshot + && (snapshot.account.kind === "connecting" + || snapshot.account.kind === "awaiting-authorization") && ( + + )} +
+ ) +} + +function SignInDialog({ snapshot, run, pending }: ViewProps) { + const [now, setNow] = useState(Date.now) + useEffect(() => { + const timer = globalThis.setInterval(() => { + setNow(Date.now()) + }, 1000) + return () => { + globalThis.clearInterval(timer) + } + }, []) + const account = snapshot.account + const cancel = () => { + void run({ kind: "cancel-sign-in" }) + } + if ( + account.kind !== "connecting" + && account.kind !== "awaiting-authorization" + ) + return null + const remaining = + account.kind === "awaiting-authorization" ? + Math.max(0, Math.ceil((Date.parse(account.expiresAt) - now) / 1000)) + : 0 + const expired = account.kind === "awaiting-authorization" && remaining <= 0 + + return ( + + + {account.kind === "awaiting-authorization" && ( + + )} + + } + > +
+
+ {account.kind === "connecting" ? +
+ +

Requesting your device code…

+
+ : <> +

+ Enter this code on GitHub to connect your Copilot account. +

+
{account.code}
+ +
+ {!expired && ( +
+ {!expired && ( +

+ Expires in {Math.floor(remaining / 60)}: + {String(remaining % 60).padStart(2, "0")} +

+ )} + + } +
+ ) +} + +function Sidebar({ + snapshot, + view, + onNavigate, + signInBusy, + run, +}: { + snapshot: DesktopSnapshot | null + view: View + onNavigate: (view: View) => void + signInBusy: boolean + run: RunCommand +}) { + const account = snapshot?.account + return ( + + ) +} diff --git a/desktop/renderer/DESIGN.md b/desktop/renderer/DESIGN.md new file mode 100644 index 0000000..e890407 --- /dev/null +++ b/desktop/renderer/DESIGN.md @@ -0,0 +1,73 @@ +# Copilot Bridge renderer + +This document explains the renderer design. The native controller remains the sole owner of account state, credentials, settings, and gateway lifecycle. + +## Worklist + +- [x] `how` over the affected subsystem. +- [x] `architect` for parallel design exploration. + Parent architecture exploration selected a dedicated proxy child. Renderer alternatives below were compared locally within the assigned renderer scope. +- [x] Write the throughput checkpoint as four todo items. +- [x] Delegate code-writing to a subagent using your configured feature model. + This renderer is the parent's delegated implementation. Nested delegation is outside this assignment. +- [ ] Verify on the matching surface. + Parent owns the shared browser and native verification sessions. +- [ ] Rebase into small, ordered commits. Stack follow-ups. + Parent owns integration. This assignment makes no commits. +- [x] If the design is contested, `interrogate` before shipping. + Skip because the parent supplied an agreed design direction. +- [ ] Run **Opening a PR**. + Parent owns delivery. + +## Throughput checkpoint + +- Blocking first steps. Read the shared contract and agree on the connection snippet signature before rendering commands. +- Independent workstreams. The parent owns bootstrap, build, preview, and shared snippets. Another worker owns the controller. This worker owns only the renderer and HTML entry. +- Shared mutable state. Renderer filters and drafts stay local. Every persistent operation is a validated command to the controller. +- Smallest safe decomposition. One renderer owner keeps styling, interactions, and accessibility coherent. Parent performs independent review and UI verification. + +## Data shape and ownership + +`DesktopSnapshot` is the authoritative domain shape. A discriminated transport state separates loading, unavailable, and ready views. Every incoming snapshot is parsed with `snapshotSchema`. A revision comparison rejects snapshots older than the currently rendered one. A pending-command set prevents repeated identical actions. Device authorization uses the account union rather than a second sign-in state machine. + +The renderer exposes no token or local-key field. It previews configurations with the literal `` and sends copy commands to native code. + +## Design alternatives + +| Layout | Strength | Cost | Decision | +| --- | --- | --- | --- | +| Equal summary cards | Familiar scanning | Hides the actual provider-to-agent relationship | Reject | +| Terminal workspace | Dense operational view | Makes account setup and settings harder to discover | Reject | +| Desktop control center | Connects account, listener, and agents in one path | Needs careful compact layout | Use | + +The selected overview is left aligned, with a quiet sidebar, a single connection panel, a session metric strip, and an operational request table. + +```text +Sidebar Overview Gateway status + GitHub Copilot ----- Gateway ----- Agents + [authenticated endpoint] [copy URL] [copy key] + Requests Input tokens Output tokens Errors + Recent requests Premium allowance + Test request +``` + +## Tokens + +- Canvas `#f6f7f8`. +- Surface `#ffffff`. +- Graphite `#24292f`. +- Muted text `#606c77`. +- Deep teal `#146b65`. +- Hairline `#e1e5e8`. + +The dark theme preserves the same hierarchy with charcoal surfaces and a lighter teal. System appearance follows the OS. Typography uses offline system UI fonts, with a 13px body and 22px page headings. Monospace is reserved for endpoints, request IDs, code, and the actual device code. + +Spacing follows a 4px base, with 24px page insets, 16px group gaps, and 8px control gaps. Controls use 6px corners. Major panels use 10px corners. Tables use aligned columns and horizontal rules. + +## Critique against the brief + +The first sketch used separate metric cards. Replacing them with one ruled strip gives the connection diagram priority and makes the app read as a desktop utility. The diagram has no animated decoration. Teal identifies active gateway state, selected controls, and primary actions. Fixture mode uses a persistent amber banner and explicit account-data wording. + +The interface contains no remote assets, invented history, or default sample models. Empty states lead to sign-in, refresh, setup, or a test request. Narrow layouts collapse the sidebar and stack the connection path without hiding actions. Dialogs trap focus, restore it, and cancel authorization on Escape. + +Model the Domain keeps account and gateway variants in their shared unions. Type System Discipline and Boundary Discipline put snapshot parsing at the bridge. Experience First keeps the connection path and useful empty states ahead of decorative cards. Prove It Works leaves the final UI verdict with the parent's real browser and native runs. diff --git a/desktop/renderer/Github.tsx b/desktop/renderer/Github.tsx new file mode 100644 index 0000000..5e3b142 --- /dev/null +++ b/desktop/renderer/Github.tsx @@ -0,0 +1,21 @@ +import type { LucideProps } from "lucide-react" + +import { forwardRef } from "react" + +export const Github = forwardRef( + ({ size = 24, ...props }, ref) => ( + + + + ), +) + +Github.displayName = "Github" diff --git a/desktop/renderer/ModelsView.tsx b/desktop/renderer/ModelsView.tsx new file mode 100644 index 0000000..5b28bb7 --- /dev/null +++ b/desktop/renderer/ModelsView.tsx @@ -0,0 +1,329 @@ +import { + Box, + Check, + Eye, + RefreshCw, + Search, + Send, + Sparkles, + Wrench, + X, + Zap, + type LucideIcon, +} from "lucide-react" +import { useState } from "react" + +import type { DesktopModel } from "../shared/contracts" +import type { ViewProps } from "./App" + +import { + Badge, + Button, + EmptyState, + formatLimit, + formatTime, +} from "./components" + +const capabilities: Array<{ + id: DesktopModel["supports"][number] + label: string + icon: LucideIcon +}> = [ + { id: "tools", label: "Tools", icon: Wrench }, + { id: "vision", label: "Vision", icon: Eye }, + { id: "reasoning", label: "Reasoning", icon: Sparkles }, + { id: "streaming", label: "Streaming", icon: Zap }, +] + +export function ModelsView({ + snapshot, + run, + pending, + onSetUp, + onProbe, +}: ViewProps & { + onSetUp: () => void + onProbe: (modelId: string) => void +}) { + const [query, setQuery] = useState("") + const [vendor, setVendor] = useState("") + const [feature, setFeature] = useState( + "", + ) + const vendors = Array.from( + new Set(snapshot.models.map((model) => model.vendor)), + ).sort() + const search = query.trim().toLowerCase() + const models = snapshot.models.filter( + (model) => + (!search + || `${model.id} ${model.name} ${model.vendor}` + .toLowerCase() + .includes(search)) + && (!vendor || model.vendor === vendor) + && (!feature || model.supports.includes(feature)), + ) + const filtered = Boolean(query) || Boolean(vendor) || Boolean(feature) + const signedIn = snapshot.account.kind === "signed-in" + + return ( +
+
+
+

Model catalog

+

+ {snapshot.models.length}{" "} + {snapshot.models.length === 1 ? "model" : "models"} from your + Copilot account +

+
+
+ {snapshot.modelsFetchedAt && ( + + Updated {formatTime(snapshot.modelsFetchedAt)} + + )} + +
+
+
+
+ +
+ + + + +
+
+ {snapshot.models.length === 0 && ( + { + void run({ kind: signedIn ? "refresh" : "sign-in" }) + }} + > + {signedIn ? "Refresh models" : "Sign in with GitHub"} + + } + > + {signedIn ? + "Refresh the catalog to load the models available to your account." + : "Your available models will appear here after sign-in."} + + )} + {snapshot.models.length > 0 && models.length === 0 && ( + { + setQuery("") + setVendor("") + setFeature("") + }} + > + Clear filters + + } + > + Try another name, provider, or capability. + + )} + {models.length > 0 && ( +
+ + + + + + + + + + + + {models.map((model) => { + const selected = model.id === snapshot.settings.preferredModel + return ( + + + + + + + + ) + })} + +
ModelCapabilitiesContextOutput + Actions +
+
+ +
+
+ {model.name} + {model.preview && Preview} +
+ + {model.vendor || "Unknown provider"} + +
+
+
+
+ {capabilities + .filter((value) => + model.supports.includes(value.id), + ) + .map(({ id, label, icon: Icon }) => ( + + + ))} + {model.supports.length === 0 && ( + No details + )} +
+
+ {formatLimit(model.contextWindow)} + + {formatLimit(model.maxOutput)} + +
+ +
+
+
+ )} + {snapshot.models.length > 0 && ( +
+ + {models.length} {filtered ? `of ${snapshot.models.length} ` : ""} + {models.length === 1 ? "model" : "models"} + + Limits are tokens per request +
+ )} +
+
+ ) +} diff --git a/desktop/renderer/OverviewView.tsx b/desktop/renderer/OverviewView.tsx new file mode 100644 index 0000000..cd9ca63 --- /dev/null +++ b/desktop/renderer/OverviewView.tsx @@ -0,0 +1,530 @@ +import { + Activity, + ArrowRight, + ArrowUpRight, + Check, + ChevronDown, + Copy, + KeyRound, + LoaderCircle, + LockKeyhole, + Network, + Play, + RefreshCw, + Send, + Square, + Terminal, + TriangleAlert, +} from "lucide-react" +import { useState } from "react" + +import type { ViewProps } from "./App" + +import { RequestTable } from "./ActivityView" +import { + Badge, + Button, + EmptyState, + formatDate, + formatDuration, + formatNumber, + formatTime, + gatewayStatus, + ModelSelect, +} from "./components" +import { Github } from "./Github" + +interface OverviewProps extends ViewProps { + testRequest: { open: boolean; modelId: string } + onTestToggle: (open: boolean) => void + onNavigate: ( + view: "agents" | "activity" | "models" | "settings" | "overview", + ) => void +} + +export function OverviewView({ + snapshot, + run, + pending, + testRequest, + onTestToggle, + onNavigate, +}: OverviewProps) { + const { account, gateway } = snapshot + const signedIn = account.kind === "signed-in" + const running = gateway.kind === "running" + const starting = gateway.kind === "starting" + const stopping = gateway.kind === "stopping" + const canStop = running || starting + let gatewayAction = "Start gateway" + if (stopping) gatewayAction = "Stopping…" + else if (canStop) gatewayAction = "Stop gateway" + const status = gatewayStatus(gateway) + const endpoint = + running ? gateway.url : `http://127.0.0.1:${snapshot.settings.port}/v1` + const recent = snapshot.activity.slice(0, 5) + + return ( +
+
+
+

Connection

+
+ + +
+
+ +
+
+
+
+ GitHub Copilot + {signedIn ? + <> + @{account.login} + Connected + + : <> + No account connected + + + } +
+ +
+
+ {starting || stopping ? +
+ Local gateway + + 127.0.0.1:{snapshot.settings.port} + + + {status.label} + +
+ +
+
+
+ Your agents + Claude Code, Codex, OpenCode + +
+
+ +
+
+
+
+ + + +
+
+ {gateway.kind === "error" && ( +
+
+ )} +
+ + {testRequest.open && ( + { + onTestToggle(false) + }} + /> + )} + +
+
+ Requests + {formatNumber(snapshot.totals.requests)} + This session +
+
+ Input tokens + {formatNumber(snapshot.totals.inputTokens)} + {formatNumber(snapshot.totals.cachedTokens)} cached +
+
+ Output tokens + {formatNumber(snapshot.totals.outputTokens)} + This session +
+
+ Errors + 0 ? "error-number" : undefined} + > + {formatNumber(snapshot.totals.errors)} + + This session +
+
+ +
+
+
+

Recent requests

+ +
+ {recent.length > 0 ? + + : { + onTestToggle(true) + }} + > + Send a test request + + } + > + Connect an agent or test the gateway. + + } +
+ +
+
+
+
+ ) +} + +function UsagePanel({ snapshot, run, pending }: ViewProps) { + const usage = snapshot.usage + const quota = + usage?.quotas.find( + (entry) => /premium/i.test(entry.id) || /premium/i.test(entry.label), + ) ?? usage?.quotas[0] + const signedIn = snapshot.account.kind === "signed-in" + + return ( +
+
+

{quota?.label ?? "Premium allowance"}

+
+
+ {!quota && ( + <> + Unavailable +

+ {signedIn ? + "Refresh to check your Copilot allowance." + : "Sign in to see your Copilot allowance."} +

+ + )} + {quota?.kind === "unlimited" && ( + <> +
Unlimited
+

{usage?.plan || "GitHub Copilot"}

+ + )} + {quota?.kind === "limited" && ( + <> +
+ {formatNumber(quota.remaining)} + / {formatNumber(quota.limit)} +
+

requests remaining

+
+ +
+
+ {quota.percentRemaining.toFixed(0)}% remaining + {usage?.resetAt && ( + Resets {formatDate(usage.resetAt)} + )} +
+ + )} + {usage && ( +
+ Updated {formatTime(usage.fetchedAt)} +
+ )} +
+
+ ) +} + +function TestRequestPanel({ + snapshot, + run, + pending, + initialModelId, + onClose, +}: ViewProps & { initialModelId: string; onClose: () => void }) { + const [modelId, setModelId] = useState( + initialModelId || snapshot.settings.preferredModel, + ) + const [prompt, setPrompt] = useState("Say hello in one short sentence.") + const selectedModel = + snapshot.models.find((model) => model.id === modelId) + ?? snapshot.models.find( + (model) => model.id === snapshot.settings.preferredModel, + ) + ?? snapshot.models.at(0) + const running = snapshot.probe.kind === "running" || pending.has("run-probe") + const enabled = + snapshot.gateway.kind === "running" + && snapshot.account.kind === "signed-in" + && Boolean(selectedModel) + && Boolean(prompt.trim()) + && prompt.length <= 4000 + + return ( +
+
+

+

+
+
{ + event.preventDefault() + if (enabled && selectedModel) + void run({ + kind: "run-probe", + modelId: selectedModel.id, + prompt: prompt.trim(), + }) + }} + > + +