From e55b3b7a8d0b1ea1522da08fd46155ee4f58b794 Mon Sep 17 00:00:00 2001 From: wenjie Date: Mon, 9 Mar 2026 19:42:03 +0800 Subject: [PATCH] feat(web): migrate launcher to modular web frontend/backend and improve management UX (#1275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: remove the legacy picoclaw-launcher * feat: create initial web frontend and backend structure * feat(packaging): add desktop entry for PicoClaw Launcher (#1062) - Add .desktop file with Terminal=true, named "PicoClaw Launcher" - Install to /usr/share/applications/ for app menu visibility - Add 512x512 PNG icon to /usr/share/icons/hicolor/ Co-authored-by: Claude Opus 4.6 * `make dev`: If you haven't built it before, you need to run `build` first. * feat(web): comprehensive web UI and backend refactoring This commit introduces a major overhaul of both the frontend web UI and the Go backend API, transitioning to a highly modular architecture and integrating new core features. Backend: - Refactored monolithic API endpoints into domain-specific modules (config, gateway, log, models, pico, session). - Cleaned up obsolete files (`server.go`, `status.go`, WebSocket handlers) and outdated tests. - Implemented Gateway process lifecycle management (start/stop/restart) and real-time log streaming. Frontend: - Integrated Shadcn UI components to establish a modern, consistent design system. - Introduced a new application layout featuring a responsive sidebar (`app-sidebar`) and header. - Implemented internationalization (i18n) with initial support for English and Chinese. - Restructured API clients, hooks, and Zustand stores into logical domains. - Added new management pages for Settings, Logs, Models, Providers, and Credentials. - Upgraded the Pico chat interface with session history management and dynamic model selection. Build & Config: - Updated frontend dependencies, Vite configuration, and lockfiles. - Refined routing setup and overarching application stylesheets. * feat(web): enhance model management, sorting, and deletion logic - Implement model sorting in UI (default > configured > unconfigured) - Prevent deletion of default models in the frontend - Update backend to clear default settings when a model is deleted - Add existence validation when setting a default model via API - Group models in chat UI by type (API Key, OAuth, Local) - Conditionally display model selector in chat based on configuration status * refactor(web): refactor chat page into modular components/hooks and update i18n - split chat route into dedicated chat components (page, composer, empty state, messages, history, model selector) - extract model/session logic into use-chat-models and use-session-history hooks - update chat locale keys in en/zh and add empty-state/history-related translations * refactor(models): refactor models page into modular components and improve UX - split /models route into dedicated components (page, provider section, card, add/edit sheets, delete dialog) - add provider grouping/sorting, provider labels/icons, and a no-default hint in the models page - add "Set as default model" toggle to add/edit flows with safer defaults - introduce shared form helpers and new UI primitives (field, label, switch) - update i18n strings (en/zh) for models and gateway header text usage - apply minor UI polish (models nav icon, separator client directive) * fix(web): add SPA index fallback for embedded frontend routes Serve existing static assets as-is, keep /api/* and missing asset paths returning 404, and add tests for SPA fallback behavior on refresh. * fix(frontend/chat): normalize message timestamp units to prevent invalid far-future dates * chore: delete TestSPARouteFallsBackToIndex * feat: update build for web-based launcher (#1186) - Makefile: add build-launcher target (builds frontend + Go backend) - GoReleaser: point picoclaw-launcher build to web/backend, add frontend build hook, restore winres hook with updated paths - Restore icon.ico and winres config from main for Windows builds Co-authored-by: Claude Opus 4.6 * feat(credentials): add multi-provider OAuth credential management - add backend `/api/oauth/*` endpoints for provider status, browser/device-code/token login, flow query/polling, and logout - extend API handler with OAuth flow/state tracking and route registration, plus OAuth unit tests - implement frontend credentials page/components for OpenAI, Anthropic, and Google Antigravity login/logout - add OAuth API client and `useCredentialsPage` hook, with new EN/ZH i18n strings * chore: remove placeholder index.html from dist (#1188) The .gitkeep is sufficient for go:embed to find the dist directory. Co-authored-by: Claude Opus 4.6 * fix(frontend): polish model and credential UX; remove Providers nav - remove the Providers item from sidebar navigation and locale keys - simplify chat composer by dropping attach/voice action buttons - support ReactNode titles in credential cards and add provider brand icons - refine sheet header/footer styling and device-code footer button hierarchy - disable “Set default” when a model is unconfigured or already default * feat(web): Update config page (#1173) * feat(web): Update config page * fix(web): useEffect resets editorValue whenever config changes * fix(web): react-hooks/set-state-in-effect error & pnpm lint #1173 * feat(web): add channel management page for web console (#1190) * feat(web): add channel management page for web console Add a complete channel management UI that allows users to configure messaging channels (Telegram, Discord, Slack, Feishu, etc.) directly from the web console instead of manually editing config.json. Backend: GET/PUT/PATCH API endpoints for listing, updating, and toggling channels with secret field masking. Frontend: Channel cards grid with enable/disable toggles, per-channel configuration sheets with dedicated forms for major platforms and a generic fallback for others. Co-Authored-By: Claude Opus 4.6 * fix(web/channels): move channels to own sidebar group and fix sheet padding - Channels now has its own navigation group instead of being under Services - Fix edit sheet form content padding (px-1 -> px-4) to match header/footer - Fix naked return lint error in extractChannelInfo Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 * fix(web): harden channel config updates and resolve frontend lint issues - validate channel PUT/PATCH updates before saving and return structured validation errors - require `enabled` in toggle requests to avoid silent false defaults - support editing `allow_origins` in the generic channel form and parse string/array inputs on backend - replace channel form `any` usage with `ChannelConfig` (`Record`) and add safe value helpers - add i18n strings for allow-origins fields and apply related frontend formatting cleanups * fix(frontend): prevent false "Invalid JSON" errors in config editor * feat: add startup readiness checks and propagate start availability to UI - add gateway precondition validation for default model and credentials - auto-start gateway on backend boot when conditions are met - include gateway_start_allowed and gateway_start_reason in status updates - prevent frontend start actions when gateway cannot be started * feat(web): revamp channel config UX with catalog-based routing - replace legacy channel management endpoints with a backend channel catalog API - switch frontend channel updates to PATCH /api/config and per-channel config pages - add dynamic channel items in the sidebar with support for expand/collapse - migrate /channels to nested routes (/channels/$name) and remove old card/sheet flow - improve channel forms with clearer hints, required/error states, and reusable switch cards - fix Discord mention-only toggle to read/write group_trigger.mention_only * refactor(frontend): move shared-form to components and unify default-model switch with SwitchCardField * fix(frontend): improve model form validation and unify secret placeholder handling - block duplicate model aliases when adding a model (with localized error messages) - share masked secret placeholder logic across model and channel forms - refresh gateway state after setting the default model - apply minor UI cleanup to provider icon rendering * feat(web): add visual system config and launcher/autostart controls - add launcher config model and persistence (`launcher-config.json`) for port/public/CIDR settings - add system APIs for launch-at-login and launcher parameters - apply CIDR-based access-control middleware to backend HTTP routes - split config routing into visual config and raw JSON config pages - add frontend system API client and visual config sections for runtime/devices/launcher - expand i18n strings (en/zh) for new config UI - improve sidebar active matching and session ID generation fallback * refactor(frontend): remove i18n fallback strings and drop providers route - Replace `t(key, defaultValue)` calls with key-only translations across UI pages - Clean up locale files by pruning unused keys and adding missing shared keys - Remove the obsolete `/providers` page and update generated route tree * fix(backend): correct gateway status detection on Windows * fix(repo): keep web backend dist placeholder tracked --------- Co-authored-by: Guoguo <16666742+imguoguo@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 Co-authored-by: Dihubopen Co-authored-by: Dihubopen <130813726+Dihubopen@users.noreply.github.com> --- .github/workflows/release.yml | 8 + .gitignore | 6 + .goreleaser.yaml | 10 +- Makefile | 12 + cmd/picoclaw-launcher/README.md | 290 - cmd/picoclaw-launcher/README.zh.md | 287 - .../internal/server/auth_config.go | 147 - .../internal/server/auth_config_test.go | 222 - .../internal/server/auth_handlers.go | 315 - .../internal/server/logbuffer_test.go | 116 - .../internal/server/process.go | 232 - .../internal/server/server.go | 196 - .../internal/server/server_test.go | 247 - .../internal/server/utils.go | 28 - cmd/picoclaw-launcher/internal/ui/index.html | 2009 ----- cmd/picoclaw-launcher/main.go | 127 - web/Makefile | 38 + web/README.md | 51 + web/backend/.gitignore | 19 + web/backend/api/channels.go | 47 + web/backend/api/config.go | 221 + web/backend/api/events.go | 62 + web/backend/api/gateway.go | 555 ++ web/backend/api/gateway_test.go | 122 + web/backend/api/launcher_config.go | 85 + web/backend/api/launcher_config_test.go | 115 + .../logbuffer.go => web/backend/api/log.go | 10 +- web/backend/api/models.go | 298 + web/backend/api/oauth.go | 844 ++ web/backend/api/oauth_test.go | 293 + web/backend/api/pico.go | 161 + web/backend/api/router.go | 66 + web/backend/api/session.go | 286 + web/backend/api/startup.go | 305 + web/backend/api/startup_test.go | 56 + web/backend/dist/.gitkeep | 0 web/backend/embed.go | 69 + web/backend/embed_test.go | 33 + .../backend}/icon.ico | Bin web/backend/launcherconfig/config.go | 113 + web/backend/launcherconfig/config_test.go | 89 + web/backend/main.go | 164 + web/backend/middleware/access_control.go | 64 + web/backend/middleware/access_control_test.go | 86 + web/backend/middleware/middleware.go | 70 + web/backend/model/status.go | 8 + web/backend/utils.go | 61 + .../backend}/winres/winres.json | 0 web/frontend/.editorconfig | 7 + web/frontend/.gitignore | 26 + web/frontend/.prettierignore | 5 + web/frontend/components.json | 25 + web/frontend/eslint.config.js | 31 + web/frontend/index.html | 18 + web/frontend/package.json | 62 + web/frontend/pnpm-lock.yaml | 7981 +++++++++++++++++ web/frontend/prettier.config.js | 17 + web/frontend/public/apple-touch-icon.png | Bin 0 -> 24799 bytes web/frontend/public/favicon-96x96.png | Bin 0 -> 10543 bytes web/frontend/public/favicon.ico | Bin 0 -> 15086 bytes web/frontend/public/favicon.svg | 1 + web/frontend/public/lark.svg | 1 + web/frontend/public/logo_with_text.png | Bin 0 -> 8979 bytes web/frontend/public/site.webmanifest | 21 + .../public/web-app-manifest-192x192.png | Bin 0 -> 27436 bytes .../public/web-app-manifest-512x512.png | Bin 0 -> 104580 bytes web/frontend/src/api/channels.ts | 65 + web/frontend/src/api/gateway.ts | 62 + web/frontend/src/api/models.ts | 91 + web/frontend/src/api/oauth.ts | 102 + web/frontend/src/api/pico.ts | 38 + web/frontend/src/api/sessions.ts | 50 + web/frontend/src/api/system.ts | 62 + web/frontend/src/components/app-header.tsx | 193 + web/frontend/src/components/app-layout.tsx | 27 + web/frontend/src/components/app-sidebar.tsx | 215 + .../channels/channel-config-page.tsx | 539 ++ .../channels/channel-display-name.ts | 23 + .../channels/channel-forms/discord-form.tsx | 109 + .../channels/channel-forms/feishu-form.tsx | 121 + .../channels/channel-forms/generic-form.tsx | 377 + .../channels/channel-forms/slack-form.tsx | 86 + .../channels/channel-forms/telegram-form.tsx | 147 + .../src/components/chat/assistant-message.tsx | 62 + .../src/components/chat/chat-composer.tsx | 67 + .../src/components/chat/chat-empty-state.tsx | 87 + .../src/components/chat/chat-page.tsx | 150 + .../src/components/chat/model-selector.tsx | 84 + .../components/chat/session-history-menu.tsx | 98 + .../src/components/chat/typing-indicator.tsx | 47 + .../src/components/chat/user-message.tsx | 13 + .../src/components/config/config-page.tsx | 337 + .../src/components/config/config-sections.tsx | 326 + .../src/components/config/form-model.ts | 172 + .../src/components/config/raw-json-panel.tsx | 204 + .../credentials/anthropic-credential-card.tsx | 108 + .../antigravity-credential-card.tsx | 106 + .../credentials/credential-card.tsx | 44 + .../credentials/credentials-page.tsx | 127 + .../credentials/device-code-sheet.tsx | 92 + .../credentials/logout-confirm-dialog.tsx | 57 + .../credentials/openai-credential-card.tsx | 162 + .../credentials/provider-status-line.tsx | 43 + .../src/components/models/add-model-sheet.tsx | 330 + .../components/models/delete-model-dialog.tsx | 74 + .../components/models/edit-model-sheet.tsx | 298 + .../src/components/models/model-card.tsx | 137 + .../src/components/models/models-page.tsx | 213 + .../src/components/models/provider-icon.tsx | 95 + .../src/components/models/provider-label.ts | 33 + .../components/models/provider-section.tsx | 72 + web/frontend/src/components/page-header.tsx | 27 + .../src/components/secret-placeholder.ts | 16 + web/frontend/src/components/shared-form.tsx | 158 + .../src/components/ui/alert-dialog.tsx | 197 + web/frontend/src/components/ui/button.tsx | 67 + web/frontend/src/components/ui/card.tsx | 103 + .../src/components/ui/collapsible.tsx | 31 + .../src/components/ui/dropdown-menu.tsx | 269 + web/frontend/src/components/ui/field.tsx | 236 + web/frontend/src/components/ui/input.tsx | 19 + web/frontend/src/components/ui/label.tsx | 22 + .../src/components/ui/scroll-area.tsx | 53 + web/frontend/src/components/ui/select.tsx | 190 + web/frontend/src/components/ui/separator.tsx | 28 + web/frontend/src/components/ui/sheet.tsx | 144 + web/frontend/src/components/ui/sidebar.tsx | 700 ++ web/frontend/src/components/ui/skeleton.tsx | 13 + web/frontend/src/components/ui/switch.tsx | 31 + web/frontend/src/components/ui/textarea.tsx | 18 + web/frontend/src/components/ui/tooltip.tsx | 55 + web/frontend/src/hooks/use-chat-models.ts | 87 + .../src/hooks/use-credentials-page.ts | 436 + web/frontend/src/hooks/use-gateway.ts | 121 + web/frontend/src/hooks/use-mobile.ts | 19 + web/frontend/src/hooks/use-pico-chat.ts | 388 + web/frontend/src/hooks/use-session-history.ts | 96 + .../src/hooks/use-sidebar-channels.ts | 236 + web/frontend/src/hooks/use-theme.ts | 28 + web/frontend/src/hooks/use-websocket.ts | 47 + web/frontend/src/i18n/index.ts | 49 + web/frontend/src/i18n/locales/en.json | 393 + web/frontend/src/i18n/locales/zh.json | 393 + web/frontend/src/index.css | 181 + web/frontend/src/lib/utils.ts | 6 + web/frontend/src/main.tsx | 35 + web/frontend/src/routeTree.gen.ts | 229 + web/frontend/src/routes/__root.tsx | 15 + web/frontend/src/routes/channels/$name.tsx | 13 + web/frontend/src/routes/channels/route.tsx | 22 + web/frontend/src/routes/config.raw.tsx | 34 + web/frontend/src/routes/config.tsx | 19 + web/frontend/src/routes/credentials.tsx | 7 + web/frontend/src/routes/index.tsx | 7 + web/frontend/src/routes/logs.tsx | 123 + web/frontend/src/routes/models.tsx | 7 + web/frontend/src/store/gateway.ts | 38 + web/frontend/src/store/index.ts | 1 + web/frontend/tsconfig.app.json | 32 + web/frontend/tsconfig.json | 13 + web/frontend/tsconfig.node.json | 26 + web/frontend/vite.config.ts | 38 + web/picoclaw-launcher.desktop | 9 + web/picoclaw-launcher.png | Bin 0 -> 104580 bytes 164 files changed, 24081 insertions(+), 4227 deletions(-) delete mode 100644 cmd/picoclaw-launcher/README.md delete mode 100644 cmd/picoclaw-launcher/README.zh.md delete mode 100644 cmd/picoclaw-launcher/internal/server/auth_config.go delete mode 100644 cmd/picoclaw-launcher/internal/server/auth_config_test.go delete mode 100644 cmd/picoclaw-launcher/internal/server/auth_handlers.go delete mode 100644 cmd/picoclaw-launcher/internal/server/logbuffer_test.go delete mode 100644 cmd/picoclaw-launcher/internal/server/process.go delete mode 100644 cmd/picoclaw-launcher/internal/server/server.go delete mode 100644 cmd/picoclaw-launcher/internal/server/server_test.go delete mode 100644 cmd/picoclaw-launcher/internal/server/utils.go delete mode 100644 cmd/picoclaw-launcher/internal/ui/index.html delete mode 100644 cmd/picoclaw-launcher/main.go create mode 100644 web/Makefile create mode 100644 web/README.md create mode 100644 web/backend/.gitignore create mode 100644 web/backend/api/channels.go create mode 100644 web/backend/api/config.go create mode 100644 web/backend/api/events.go create mode 100644 web/backend/api/gateway.go create mode 100644 web/backend/api/gateway_test.go create mode 100644 web/backend/api/launcher_config.go create mode 100644 web/backend/api/launcher_config_test.go rename cmd/picoclaw-launcher/internal/server/logbuffer.go => web/backend/api/log.go (92%) create mode 100644 web/backend/api/models.go create mode 100644 web/backend/api/oauth.go create mode 100644 web/backend/api/oauth_test.go create mode 100644 web/backend/api/pico.go create mode 100644 web/backend/api/router.go create mode 100644 web/backend/api/session.go create mode 100644 web/backend/api/startup.go create mode 100644 web/backend/api/startup_test.go create mode 100644 web/backend/dist/.gitkeep create mode 100644 web/backend/embed.go create mode 100644 web/backend/embed_test.go rename {cmd/picoclaw-launcher => web/backend}/icon.ico (100%) create mode 100644 web/backend/launcherconfig/config.go create mode 100644 web/backend/launcherconfig/config_test.go create mode 100644 web/backend/main.go create mode 100644 web/backend/middleware/access_control.go create mode 100644 web/backend/middleware/access_control_test.go create mode 100644 web/backend/middleware/middleware.go create mode 100644 web/backend/model/status.go create mode 100644 web/backend/utils.go rename {cmd/picoclaw-launcher => web/backend}/winres/winres.json (100%) create mode 100644 web/frontend/.editorconfig create mode 100644 web/frontend/.gitignore create mode 100644 web/frontend/.prettierignore create mode 100644 web/frontend/components.json create mode 100644 web/frontend/eslint.config.js create mode 100644 web/frontend/index.html create mode 100644 web/frontend/package.json create mode 100644 web/frontend/pnpm-lock.yaml create mode 100644 web/frontend/prettier.config.js create mode 100644 web/frontend/public/apple-touch-icon.png create mode 100644 web/frontend/public/favicon-96x96.png create mode 100644 web/frontend/public/favicon.ico create mode 100644 web/frontend/public/favicon.svg create mode 100644 web/frontend/public/lark.svg create mode 100644 web/frontend/public/logo_with_text.png create mode 100644 web/frontend/public/site.webmanifest create mode 100644 web/frontend/public/web-app-manifest-192x192.png create mode 100644 web/frontend/public/web-app-manifest-512x512.png create mode 100644 web/frontend/src/api/channels.ts create mode 100644 web/frontend/src/api/gateway.ts create mode 100644 web/frontend/src/api/models.ts create mode 100644 web/frontend/src/api/oauth.ts create mode 100644 web/frontend/src/api/pico.ts create mode 100644 web/frontend/src/api/sessions.ts create mode 100644 web/frontend/src/api/system.ts create mode 100644 web/frontend/src/components/app-header.tsx create mode 100644 web/frontend/src/components/app-layout.tsx create mode 100644 web/frontend/src/components/app-sidebar.tsx create mode 100644 web/frontend/src/components/channels/channel-config-page.tsx create mode 100644 web/frontend/src/components/channels/channel-display-name.ts create mode 100644 web/frontend/src/components/channels/channel-forms/discord-form.tsx create mode 100644 web/frontend/src/components/channels/channel-forms/feishu-form.tsx create mode 100644 web/frontend/src/components/channels/channel-forms/generic-form.tsx create mode 100644 web/frontend/src/components/channels/channel-forms/slack-form.tsx create mode 100644 web/frontend/src/components/channels/channel-forms/telegram-form.tsx create mode 100644 web/frontend/src/components/chat/assistant-message.tsx create mode 100644 web/frontend/src/components/chat/chat-composer.tsx create mode 100644 web/frontend/src/components/chat/chat-empty-state.tsx create mode 100644 web/frontend/src/components/chat/chat-page.tsx create mode 100644 web/frontend/src/components/chat/model-selector.tsx create mode 100644 web/frontend/src/components/chat/session-history-menu.tsx create mode 100644 web/frontend/src/components/chat/typing-indicator.tsx create mode 100644 web/frontend/src/components/chat/user-message.tsx create mode 100644 web/frontend/src/components/config/config-page.tsx create mode 100644 web/frontend/src/components/config/config-sections.tsx create mode 100644 web/frontend/src/components/config/form-model.ts create mode 100644 web/frontend/src/components/config/raw-json-panel.tsx create mode 100644 web/frontend/src/components/credentials/anthropic-credential-card.tsx create mode 100644 web/frontend/src/components/credentials/antigravity-credential-card.tsx create mode 100644 web/frontend/src/components/credentials/credential-card.tsx create mode 100644 web/frontend/src/components/credentials/credentials-page.tsx create mode 100644 web/frontend/src/components/credentials/device-code-sheet.tsx create mode 100644 web/frontend/src/components/credentials/logout-confirm-dialog.tsx create mode 100644 web/frontend/src/components/credentials/openai-credential-card.tsx create mode 100644 web/frontend/src/components/credentials/provider-status-line.tsx create mode 100644 web/frontend/src/components/models/add-model-sheet.tsx create mode 100644 web/frontend/src/components/models/delete-model-dialog.tsx create mode 100644 web/frontend/src/components/models/edit-model-sheet.tsx create mode 100644 web/frontend/src/components/models/model-card.tsx create mode 100644 web/frontend/src/components/models/models-page.tsx create mode 100644 web/frontend/src/components/models/provider-icon.tsx create mode 100644 web/frontend/src/components/models/provider-label.ts create mode 100644 web/frontend/src/components/models/provider-section.tsx create mode 100644 web/frontend/src/components/page-header.tsx create mode 100644 web/frontend/src/components/secret-placeholder.ts create mode 100644 web/frontend/src/components/shared-form.tsx create mode 100644 web/frontend/src/components/ui/alert-dialog.tsx create mode 100644 web/frontend/src/components/ui/button.tsx create mode 100644 web/frontend/src/components/ui/card.tsx create mode 100644 web/frontend/src/components/ui/collapsible.tsx create mode 100644 web/frontend/src/components/ui/dropdown-menu.tsx create mode 100644 web/frontend/src/components/ui/field.tsx create mode 100644 web/frontend/src/components/ui/input.tsx create mode 100644 web/frontend/src/components/ui/label.tsx create mode 100644 web/frontend/src/components/ui/scroll-area.tsx create mode 100644 web/frontend/src/components/ui/select.tsx create mode 100644 web/frontend/src/components/ui/separator.tsx create mode 100644 web/frontend/src/components/ui/sheet.tsx create mode 100644 web/frontend/src/components/ui/sidebar.tsx create mode 100644 web/frontend/src/components/ui/skeleton.tsx create mode 100644 web/frontend/src/components/ui/switch.tsx create mode 100644 web/frontend/src/components/ui/textarea.tsx create mode 100644 web/frontend/src/components/ui/tooltip.tsx create mode 100644 web/frontend/src/hooks/use-chat-models.ts create mode 100644 web/frontend/src/hooks/use-credentials-page.ts create mode 100644 web/frontend/src/hooks/use-gateway.ts create mode 100644 web/frontend/src/hooks/use-mobile.ts create mode 100644 web/frontend/src/hooks/use-pico-chat.ts create mode 100644 web/frontend/src/hooks/use-session-history.ts create mode 100644 web/frontend/src/hooks/use-sidebar-channels.ts create mode 100644 web/frontend/src/hooks/use-theme.ts create mode 100644 web/frontend/src/hooks/use-websocket.ts create mode 100644 web/frontend/src/i18n/index.ts create mode 100644 web/frontend/src/i18n/locales/en.json create mode 100644 web/frontend/src/i18n/locales/zh.json create mode 100644 web/frontend/src/index.css create mode 100644 web/frontend/src/lib/utils.ts create mode 100644 web/frontend/src/main.tsx create mode 100644 web/frontend/src/routeTree.gen.ts create mode 100644 web/frontend/src/routes/__root.tsx create mode 100644 web/frontend/src/routes/channels/$name.tsx create mode 100644 web/frontend/src/routes/channels/route.tsx create mode 100644 web/frontend/src/routes/config.raw.tsx create mode 100644 web/frontend/src/routes/config.tsx create mode 100644 web/frontend/src/routes/credentials.tsx create mode 100644 web/frontend/src/routes/index.tsx create mode 100644 web/frontend/src/routes/logs.tsx create mode 100644 web/frontend/src/routes/models.tsx create mode 100644 web/frontend/src/store/gateway.ts create mode 100644 web/frontend/src/store/index.ts create mode 100644 web/frontend/tsconfig.app.json create mode 100644 web/frontend/tsconfig.json create mode 100644 web/frontend/tsconfig.node.json create mode 100644 web/frontend/vite.config.ts create mode 100644 web/picoclaw-launcher.desktop create mode 100644 web/picoclaw-launcher.png diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 56e28b578..4a584773d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -65,6 +65,14 @@ jobs: with: go-version-file: go.mod + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Setup pnpm + run: corepack enable && corepack prepare pnpm@latest --activate + - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/.gitignore b/.gitignore index a52b8d25a..61fe494ca 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,12 @@ docs/plans/ # Added by goreleaser init: dist/ +*.vite/ # Windows Application Icon/Resource *.syso + +# Keep embedded backend dist directory placeholder in VCS +!web/backend/dist/ +web/backend/dist/* +!web/backend/dist/.gitkeep diff --git a/.goreleaser.yaml b/.goreleaser.yaml index fe208ebd4..70ea67323 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -6,8 +6,9 @@ before: hooks: - go mod tidy - go generate ./... + - sh -c 'cd web/frontend && pnpm install && pnpm build:backend' - go install github.com/tc-hib/go-winres@latest - - go-winres make --in cmd/picoclaw-launcher/winres/winres.json --out cmd/picoclaw-launcher/rsrc --product-version={{ .Version }} --file-version={{ .Version }} + - go-winres make --in web/backend/winres/winres.json --out web/backend/rsrc --product-version={{ .Version }} --file-version={{ .Version }} builds: - id: picoclaw @@ -70,7 +71,7 @@ builds: - "7" gomips: - softfloat - main: ./cmd/picoclaw-launcher + main: ./web/backend ignore: - goos: windows goarch: arm @@ -178,6 +179,11 @@ nfpms: - rpm - deb bindir: /usr/bin + contents: + - src: web/picoclaw-launcher.desktop + dst: /usr/share/applications/picoclaw-launcher.desktop + - src: web/picoclaw-launcher.png + dst: /usr/share/icons/hicolor/512x512/apps/picoclaw-launcher.png changelog: sort: asc diff --git a/Makefile b/Makefile index 8de98e984..955c1c966 100644 --- a/Makefile +++ b/Makefile @@ -111,6 +111,18 @@ build: generate @echo "Build complete: $(BINARY_PATH)" @ln -sf $(BINARY_NAME)-$(PLATFORM)-$(ARCH) $(BUILD_DIR)/$(BINARY_NAME) +## build-launcher: Build the picoclaw-launcher (web console) binary +build-launcher: + @echo "Building picoclaw-launcher for $(PLATFORM)/$(ARCH)..." + @mkdir -p $(BUILD_DIR) + @if [ ! -f web/backend/dist/index.html ]; then \ + echo "Building frontend..."; \ + cd web/frontend && pnpm install && pnpm build:backend; \ + fi + @$(GO) build $(GOFLAGS) -o $(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH) ./web/backend + @ln -sf picoclaw-launcher-$(PLATFORM)-$(ARCH) $(BUILD_DIR)/picoclaw-launcher + @echo "Build complete: $(BUILD_DIR)/picoclaw-launcher" + ## build-whatsapp-native: Build with WhatsApp native (whatsmeow) support; larger binary build-whatsapp-native: generate ## @echo "Building $(BINARY_NAME) with WhatsApp native for $(PLATFORM)/$(ARCH)..." diff --git a/cmd/picoclaw-launcher/README.md b/cmd/picoclaw-launcher/README.md deleted file mode 100644 index 0872a5f65..000000000 --- a/cmd/picoclaw-launcher/README.md +++ /dev/null @@ -1,290 +0,0 @@ -# PicoClaw Launcher - -> [!WARNING] -> This project is a temporary solution and will be refactored in the future to provide a complete web service. Therefore, the APIs in this directory are not stable. - -A standalone launcher for PicoClaw, providing visual JSON editing and OAuth provider authentication management. - -## Features - -- 📝 **Config Editor** — Sidebar-based settings UI with model management, channel configuration forms, and a raw JSON editor -- 🤖 **Model Management** — Model card grid with availability status (grayed out without API key), primary model selection, add/edit/delete with required/optional field separation -- 📡 **Channel Configuration** — Form-based settings for 13 channel types (Telegram, Discord, Slack, Matrix, WeCom, DingTalk, Feishu, LINE, WhatsApp, QQ, OneBot, MaixCAM, etc.) with documentation links -- 🔐 **Provider Auth** — Login to OpenAI (Device Code), Anthropic (API Token), Google Antigravity (Browser OAuth) -- 🌐 **Embedded Frontend** — Compiles to a single binary with no external dependencies -- 🌍 **i18n** — Chinese/English language switching with browser auto-detection -- 🎨 **Theme** — Light / Dark / System theme toggle with localStorage persistence - -## Quick Start - -```bash -# Build -go build -o picoclaw-launcher ./cmd/picoclaw-launcher/ - -# Run with default config path (~/.picoclaw/config.json) -./picoclaw-launcher - -# Specify a config file -./picoclaw-launcher ./config.json - -# Allow LAN access -./picoclaw-launcher -public -``` - -Open `http://localhost:18800` in your browser. - -## CLI Options - -``` -Usage: picoclaw-config [options] [config.json] - -Arguments: - config.json Path to the configuration file (default: ~/.picoclaw/config.json) - -Options: - -public Listen on all interfaces (0.0.0.0), allowing access from other devices -``` - -## API Reference - -Base URL: `http://localhost:18800` - ---- - -### Static Files - -#### GET / - -Serves the embedded frontend (`index.html`). - ---- - -### Config API - -#### GET /api/config - -Reads the current configuration file. - -**Response** `200 OK` - -```json -{ - "config": { ... }, - "path": "/Users/xiao/.picoclaw/config.json" -} -``` - ---- - -#### PUT /api/config - -Saves the configuration. The request body must be a complete Config JSON object. - -**Request Body** — `application/json` - -```json -{ - "agents": { "defaults": { "model_name": "gpt-5.2" } }, - "model_list": [ - { - "model_name": "gpt-5.2", - "model": "openai/gpt-5.2", - "auth_method": "oauth" - } - ] -} -``` - -**Response** `200 OK` - -```json -{ "status": "ok" } -``` - -**Error** `400 Bad Request` — Invalid JSON - ---- - -### Auth API - -#### GET /api/auth/status - -Returns the authentication status of all providers and any in-progress device code login. - -**Response** `200 OK` - -```json -{ - "providers": [ - { - "provider": "openai", - "auth_method": "oauth", - "status": "active", - "account_id": "user-xxx", - "expires_at": "2026-03-01T00:00:00Z" - } - ], - "pending_device": { - "provider": "openai", - "status": "pending", - "device_url": "https://auth.openai.com/activate", - "user_code": "ABCD-1234" - } -} -``` - -`status` values: `active` | `expired` | `needs_refresh` - -`pending_device` is only present when a device code login is in progress. - ---- - -#### POST /api/auth/login - -Initiates a provider login. - -**Request Body** — `application/json` - -```json -{ "provider": "openai" } -``` - -Supported `provider` values: `openai` | `anthropic` | `google-antigravity` - -##### OpenAI (Device Code Flow) - -Returns device code info. The server polls for completion in the background. - -```json -{ - "status": "pending", - "device_url": "https://auth.openai.com/activate", - "user_code": "ABCD-1234", - "message": "Open the URL and enter the code to authenticate." -} -``` - -The user opens `device_url` in a browser and enters `user_code`. Once authenticated, `GET /api/auth/status` will show `pending_device.status` as `success`. - -##### Anthropic (API Token) - -Requires a `token` field in the request: - -```json -{ "provider": "anthropic", "token": "sk-ant-xxx" } -``` - -**Response:** - -```json -{ "status": "success", "message": "Anthropic token saved" } -``` - -##### Google Antigravity (Browser OAuth) - -Returns an authorization URL for the frontend to open in a new tab: - -```json -{ - "status": "redirect", - "auth_url": "https://accounts.google.com/o/oauth2/auth?...", - "message": "Open the URL to authenticate with Google." -} -``` - -After authentication, Google redirects to `GET /auth/callback`, which saves the credentials and redirects back to the picoclaw-config UI. - ---- - -#### POST /api/auth/logout - -Logs out from a provider. - -**Request Body** — `application/json` - -```json -{ "provider": "openai" } -``` - -Omit or leave `provider` empty to log out from all providers. - -**Response** `200 OK` - -```json -{ "status": "ok" } -``` - ---- - -#### GET /auth/callback - -OAuth browser callback endpoint (used by Google Antigravity). Called by the OAuth provider's redirect — **not invoked directly by the frontend**. - -**Query Parameters:** -- `state` — OAuth state for CSRF validation -- `code` — Authorization code - -On success, redirects to `/#auth`. - - -### Process API - -#### GET /api/process/status - -Gets the running status of the `picoclaw gateway` process. - -**Response** `200 OK` (Running) - -```json -{ - "process_status": "running", - "status": "ok", - "uptime": "1.010814s" -} -``` - -**Response** `200 OK` (Stopped) - -```json -{ - "process_status": "stopped", - "error": "Get \"http://localhost:18790/health\": dial tcp [::1]:18790: connect: connection refused" -} -``` - ---- - -#### POST /api/process/start - -Starts the `picoclaw gateway` process in the background. - -**Response** `200 OK` - -```json -{ - "status": "ok", - "pid": 12345 -} -``` - ---- - -#### POST /api/process/stop - -Stops the running `picoclaw gateway` process. - -**Response** `200 OK` - -```json -{ - "status": "ok" -} -``` - ---- - -## Testing - -```bash -go test -v ./cmd/picoclaw-launcher/ -``` diff --git a/cmd/picoclaw-launcher/README.zh.md b/cmd/picoclaw-launcher/README.zh.md deleted file mode 100644 index 320de75a5..000000000 --- a/cmd/picoclaw-launcher/README.zh.md +++ /dev/null @@ -1,287 +0,0 @@ -# PicoClaw Launcher - -> [!WARNING] -> 该项目属于临时解决方案,后续会重构并提供完整的 Web 服务,因此该目录下的接口并不稳定。 - -PicoClaw 的独立启动器,提供可视化 JSON 配置编辑和 OAuth Provider 认证管理。 - -## 功能 - -- 📝 **配置编辑** — 侧边栏式设置 UI,支持模型管理、通道配置表单和原始 JSON 编辑器 -- 🤖 **模型管理** — 模型卡片网格,可用性状态显示(无 API Key 时灰色),主模型选择,增删改查,必填/选填字段分离 -- 📡 **通道配置** — 12 种通道类型(Telegram、Discord、Slack、企业微信、钉钉、飞书、LINE、WhatsApp、QQ、OneBot、MaixCAM 等)的表单化配置,附带文档链接 -- 🔐 **Provider 认证** — 支持 OpenAI (Device Code)、Anthropic (API Token)、Google Antigravity (Browser OAuth) 登录 -- 🌐 **嵌入式前端** — 编译为单一二进制文件,无需额外依赖 -- 🌍 **国际化** — 中英文切换,首次访问自动检测浏览器语言 -- 🎨 **主题** — 亮色 / 暗色 / 跟随系统,偏好保存在 localStorage - -## 快速开始 - -```bash -# 编译 -go build -o picoclaw-launcher ./cmd/picoclaw-launcher/ - -# 运行(使用默认配置路径 ~/.picoclaw/config.json) -./picoclaw-launcher - -# 指定配置文件 -./picoclaw-launcher ./config.json - -# 允许局域网访问 -./picoclaw-launcher -public -``` - -启动后在浏览器中打开 `http://localhost:18800`。 - -## 命令行参数 - -``` -Usage: picoclaw-launcher [options] [config.json] - -Arguments: - config.json 配置文件路径(默认: ~/.picoclaw/config.json) - -Options: - -public 监听所有网络接口(0.0.0.0),允许局域网设备访问 -``` - -## API 文档 - -Base URL: `http://localhost:18800` - -### 静态文件 - -#### GET / - -提供嵌入式前端页面(`index.html`)。 - ---- - -### Config API - -#### GET /api/config - -读取当前配置文件内容。 - -**Response** `200 OK` - -```json -{ - "config": { ... }, - "path": "/Users/xiao/.picoclaw/config.json" -} -``` - ---- - -#### PUT /api/config - -保存配置。请求体为完整的 Config JSON。 - -**Request Body** — `application/json` - -```json -{ - "agents": { "defaults": { "model_name": "gpt-5.2" } }, - "model_list": [ - { - "model_name": "gpt-5.2", - "model": "openai/gpt-5.2", - "auth_method": "oauth" - } - ] -} -``` - -**Response** `200 OK` - -```json -{ "status": "ok" } -``` - -**Error** `400 Bad Request` — 无效 JSON - ---- - -### Auth API - -#### GET /api/auth/status - -获取所有 Provider 的认证状态和进行中的 Device Code 登录信息。 - -**Response** `200 OK` - -```json -{ - "providers": [ - { - "provider": "openai", - "auth_method": "oauth", - "status": "active", - "account_id": "user-xxx", - "expires_at": "2026-03-01T00:00:00Z" - } - ], - "pending_device": { - "provider": "openai", - "status": "pending", - "device_url": "https://auth.openai.com/activate", - "user_code": "ABCD-1234" - } -} -``` - -`status` 可选值: `active` | `expired` | `needs_refresh` - -`pending_device` 仅在有进行中的 Device Code 登录时返回。 - ---- - -#### POST /api/auth/login - -发起 Provider 登录。 - -**Request Body** — `application/json` - -```json -{ "provider": "openai" } -``` - -支持的 `provider` 值: `openai` | `anthropic` | `google-antigravity` - -##### OpenAI (Device Code Flow) - -返回 Device Code 信息,后台自动轮询认证结果: - -```json -{ - "status": "pending", - "device_url": "https://auth.openai.com/activate", - "user_code": "ABCD-1234", - "message": "Open the URL and enter the code to authenticate." -} -``` - -用户在浏览器中打开 `device_url` 并输入 `user_code`。认证完成后通过 `GET /api/auth/status` 的 `pending_device.status` 变为 `success` 通知前端。 - -##### Anthropic (API Token) - -需在请求中附带 token: - -```json -{ "provider": "anthropic", "token": "sk-ant-xxx" } -``` - -**Response:** - -```json -{ "status": "success", "message": "Anthropic token saved" } -``` - -##### Google Antigravity (Browser OAuth) - -返回授权 URL,前端打开新标签页: - -```json -{ - "status": "redirect", - "auth_url": "https://accounts.google.com/o/oauth2/auth?...", - "message": "Open the URL to authenticate with Google." -} -``` - -认证完成后 Google 回调至 `GET /auth/callback`,自动保存凭据并重定向回 picoclaw-config 页面。 - ---- - -#### POST /api/auth/logout - -登出 Provider。 - -**Request Body** — `application/json` - -```json -{ "provider": "openai" } -``` - -传空字符串或省略 `provider` 则登出所有 Provider。 - -**Response** `200 OK` - -```json -{ "status": "ok" } -``` - ---- - -#### GET /auth/callback - -OAuth Browser 回调端点(Google Antigravity 专用),由 OAuth Provider 重定向调用,**非前端直接使用**。 - -**Query Parameters:** -- `state` — OAuth state 校验 -- `code` — 授权码 - -认证成功后重定向到 `/#auth`。 - -### Process API - -#### GET /api/process/status - -获取 `picoclaw gateway` 进程的运行状态。 - -**Response** `200 OK` (运行中) - -```json -{ - "process_status": "running", - "status": "ok", - "uptime": "1.010814s" -} -``` - -**Response** `200 OK` (未运行) - -```json -{ - "process_status": "stopped", - "error": "Get \"http://localhost:18790/health\": dial tcp [::1]:18790: connect: connection refused" -} -``` - ---- - -#### POST /api/process/start - -在后台启动 `picoclaw gateway` 进程。 - -**Response** `200 OK` - -```json -{ - "status": "ok", - "pid": 12345 -} -``` - ---- - -#### POST /api/process/stop - -停止正在运行的 `picoclaw gateway` 进程。 - -**Response** `200 OK` - -```json -{ - "status": "ok" -} -``` - ---- - -## 测试 - -```bash -go test -v ./cmd/picoclaw-launcher/ -``` diff --git a/cmd/picoclaw-launcher/internal/server/auth_config.go b/cmd/picoclaw-launcher/internal/server/auth_config.go deleted file mode 100644 index f75e8fff0..000000000 --- a/cmd/picoclaw-launcher/internal/server/auth_config.go +++ /dev/null @@ -1,147 +0,0 @@ -package server - -import ( - "log" - "strings" - - "github.com/sipeed/picoclaw/pkg/auth" - "github.com/sipeed/picoclaw/pkg/config" -) - -// updateConfigAfterLogin updates config.json after a successful provider login. -func updateConfigAfterLogin(configPath, provider string, cred *auth.AuthCredential) { - cfg, err := config.LoadConfig(configPath) - if err != nil { - log.Printf("Warning: could not load config to update auth_method: %v", err) - return - } - - switch provider { - case "openai": - cfg.Providers.OpenAI.AuthMethod = "oauth" - found := false - for i := range cfg.ModelList { - if isOpenAIModel(cfg.ModelList[i].Model) { - cfg.ModelList[i].AuthMethod = "oauth" - found = true - break - } - } - if !found { - cfg.ModelList = append(cfg.ModelList, config.ModelConfig{ - ModelName: "gpt-5.2", - Model: "openai/gpt-5.2", - AuthMethod: "oauth", - }) - } - cfg.Agents.Defaults.ModelName = "gpt-5.2" - - case "anthropic": - cfg.Providers.Anthropic.AuthMethod = "token" - found := false - for i := range cfg.ModelList { - if isAnthropicModel(cfg.ModelList[i].Model) { - cfg.ModelList[i].AuthMethod = "token" - found = true - break - } - } - if !found { - cfg.ModelList = append(cfg.ModelList, config.ModelConfig{ - ModelName: "claude-sonnet-4.6", - Model: "anthropic/claude-sonnet-4.6", - AuthMethod: "token", - }) - } - cfg.Agents.Defaults.ModelName = "claude-sonnet-4.6" - - case "google-antigravity": - cfg.Providers.Antigravity.AuthMethod = "oauth" - found := false - for i := range cfg.ModelList { - if isAntigravityModel(cfg.ModelList[i].Model) { - cfg.ModelList[i].AuthMethod = "oauth" - found = true - break - } - } - if !found { - cfg.ModelList = append(cfg.ModelList, config.ModelConfig{ - ModelName: "gemini-flash", - Model: "antigravity/gemini-3-flash", - AuthMethod: "oauth", - }) - } - cfg.Agents.Defaults.ModelName = "gemini-flash" - } - - if err := config.SaveConfig(configPath, cfg); err != nil { - log.Printf("Warning: could not update config: %v", err) - } -} - -// clearAuthMethodInConfig clears auth_method for a specific provider in config.json. -func clearAuthMethodInConfig(configPath, provider string) { - cfg, err := config.LoadConfig(configPath) - if err != nil { - return - } - - for i := range cfg.ModelList { - switch provider { - case "openai": - if isOpenAIModel(cfg.ModelList[i].Model) { - cfg.ModelList[i].AuthMethod = "" - } - case "anthropic": - if isAnthropicModel(cfg.ModelList[i].Model) { - cfg.ModelList[i].AuthMethod = "" - } - case "google-antigravity", "antigravity": - if isAntigravityModel(cfg.ModelList[i].Model) { - cfg.ModelList[i].AuthMethod = "" - } - } - } - - switch provider { - case "openai": - cfg.Providers.OpenAI.AuthMethod = "" - case "anthropic": - cfg.Providers.Anthropic.AuthMethod = "" - case "google-antigravity", "antigravity": - cfg.Providers.Antigravity.AuthMethod = "" - } - - config.SaveConfig(configPath, cfg) -} - -// clearAllAuthMethodsInConfig clears auth_method for all providers in config.json. -func clearAllAuthMethodsInConfig(configPath string) { - cfg, err := config.LoadConfig(configPath) - if err != nil { - return - } - for i := range cfg.ModelList { - cfg.ModelList[i].AuthMethod = "" - } - cfg.Providers.OpenAI.AuthMethod = "" - cfg.Providers.Anthropic.AuthMethod = "" - cfg.Providers.Antigravity.AuthMethod = "" - config.SaveConfig(configPath, cfg) -} - -// ── Model identification helpers ───────────────────────────────── - -func isOpenAIModel(model string) bool { - return model == "openai" || strings.HasPrefix(model, "openai/") -} - -func isAnthropicModel(model string) bool { - return model == "anthropic" || strings.HasPrefix(model, "anthropic/") -} - -func isAntigravityModel(model string) bool { - return model == "antigravity" || model == "google-antigravity" || - strings.HasPrefix(model, "antigravity/") || strings.HasPrefix(model, "google-antigravity/") -} diff --git a/cmd/picoclaw-launcher/internal/server/auth_config_test.go b/cmd/picoclaw-launcher/internal/server/auth_config_test.go deleted file mode 100644 index 92158d011..000000000 --- a/cmd/picoclaw-launcher/internal/server/auth_config_test.go +++ /dev/null @@ -1,222 +0,0 @@ -package server - -import ( - "path/filepath" - "testing" - - "github.com/sipeed/picoclaw/pkg/auth" - "github.com/sipeed/picoclaw/pkg/config" -) - -// ── Model identification helpers ───────────────────────────────── - -func TestIsOpenAIModel(t *testing.T) { - tests := []struct { - model string - want bool - }{ - {"openai", true}, - {"openai/gpt-4o", true}, - {"openai/gpt-5.2", true}, - {"anthropic", false}, - {"anthropic/claude-sonnet-4.6", false}, - {"openai-compatible", false}, - {"", false}, - } - for _, tt := range tests { - if got := isOpenAIModel(tt.model); got != tt.want { - t.Errorf("isOpenAIModel(%q) = %v, want %v", tt.model, got, tt.want) - } - } -} - -func TestIsAnthropicModel(t *testing.T) { - tests := []struct { - model string - want bool - }{ - {"anthropic", true}, - {"anthropic/claude-sonnet-4.6", true}, - {"openai", false}, - {"openai/gpt-4o", false}, - {"", false}, - } - for _, tt := range tests { - if got := isAnthropicModel(tt.model); got != tt.want { - t.Errorf("isAnthropicModel(%q) = %v, want %v", tt.model, got, tt.want) - } - } -} - -func TestIsAntigravityModel(t *testing.T) { - tests := []struct { - model string - want bool - }{ - {"antigravity", true}, - {"google-antigravity", true}, - {"antigravity/gemini-3-flash", true}, - {"google-antigravity/gemini-3-flash", true}, - {"openai", false}, - {"antigravity-custom", false}, - {"", false}, - } - for _, tt := range tests { - if got := isAntigravityModel(tt.model); got != tt.want { - t.Errorf("isAntigravityModel(%q) = %v, want %v", tt.model, got, tt.want) - } - } -} - -// ── Config update helpers ──────────────────────────────────────── - -func writeTempConfigViaSave(t *testing.T, cfg *config.Config) string { - t.Helper() - dir := t.TempDir() - path := filepath.Join(dir, "config.json") - if err := config.SaveConfig(path, cfg); err != nil { - t.Fatalf("save config: %v", err) - } - return path -} - -func loadTempConfig(t *testing.T, path string) *config.Config { - t.Helper() - cfg, err := config.LoadConfig(path) - if err != nil { - t.Fatalf("load config: %v", err) - } - return cfg -} - -func TestUpdateConfigAfterLogin_OpenAI_ExistingModel(t *testing.T) { - cfg := &config.Config{ - ModelList: []config.ModelConfig{ - {ModelName: "gpt-4o", Model: "openai/gpt-4o"}, - }, - } - path := writeTempConfigViaSave(t, cfg) - - cred := &auth.AuthCredential{AuthMethod: "oauth"} - updateConfigAfterLogin(path, "openai", cred) - - result := loadTempConfig(t, path) - - // Model-level auth_method persists through serialization - if len(result.ModelList) != 1 { - t.Fatalf("expected 1 model, got %d", len(result.ModelList)) - } - if result.ModelList[0].AuthMethod != "oauth" { - t.Errorf("expected model auth_method=oauth, got %q", result.ModelList[0].AuthMethod) - } -} - -func TestUpdateConfigAfterLogin_OpenAI_NoExistingModel(t *testing.T) { - cfg := &config.Config{ - ModelList: []config.ModelConfig{ - {ModelName: "claude", Model: "anthropic/claude-sonnet-4.6"}, - }, - } - path := writeTempConfigViaSave(t, cfg) - - cred := &auth.AuthCredential{AuthMethod: "oauth"} - updateConfigAfterLogin(path, "openai", cred) - - result := loadTempConfig(t, path) - - if len(result.ModelList) != 2 { - t.Fatalf("expected 2 models (original + added), got %d", len(result.ModelList)) - } - if result.ModelList[1].Model != "openai/gpt-5.2" { - t.Errorf("expected added model openai/gpt-5.2, got %q", result.ModelList[1].Model) - } - if result.Agents.Defaults.ModelName != "gpt-5.2" { - t.Errorf("expected default model_name=gpt-5.2, got %q", result.Agents.Defaults.ModelName) - } -} - -func TestUpdateConfigAfterLogin_Anthropic(t *testing.T) { - cfg := &config.Config{} - path := writeTempConfigViaSave(t, cfg) - - cred := &auth.AuthCredential{AuthMethod: "token"} - updateConfigAfterLogin(path, "anthropic", cred) - - result := loadTempConfig(t, path) - - // Model should be added with correct auth_method - if len(result.ModelList) != 1 { - t.Fatalf("expected 1 model added, got %d", len(result.ModelList)) - } - if result.ModelList[0].Model != "anthropic/claude-sonnet-4.6" { - t.Errorf("expected model anthropic/claude-sonnet-4.6, got %q", result.ModelList[0].Model) - } - if result.ModelList[0].AuthMethod != "token" { - t.Errorf("expected model auth_method=token, got %q", result.ModelList[0].AuthMethod) - } -} - -func TestUpdateConfigAfterLogin_GoogleAntigravity(t *testing.T) { - cfg := &config.Config{} - path := writeTempConfigViaSave(t, cfg) - - cred := &auth.AuthCredential{AuthMethod: "oauth"} - updateConfigAfterLogin(path, "google-antigravity", cred) - - result := loadTempConfig(t, path) - - // Model should be added with correct auth_method - if len(result.ModelList) != 1 { - t.Fatalf("expected 1 model added, got %d", len(result.ModelList)) - } - if result.ModelList[0].Model != "antigravity/gemini-3-flash" { - t.Errorf("expected model antigravity/gemini-3-flash, got %q", result.ModelList[0].Model) - } - if result.ModelList[0].AuthMethod != "oauth" { - t.Errorf("expected model auth_method=oauth, got %q", result.ModelList[0].AuthMethod) - } -} - -func TestClearAuthMethodInConfig(t *testing.T) { - cfg := &config.Config{ - ModelList: []config.ModelConfig{ - {ModelName: "gpt-4o", Model: "openai/gpt-4o", AuthMethod: "oauth"}, - {ModelName: "claude", Model: "anthropic/claude-sonnet-4.6", AuthMethod: "token"}, - }, - } - path := writeTempConfigViaSave(t, cfg) - - clearAuthMethodInConfig(path, "openai") - - result := loadTempConfig(t, path) - - // Openai model auth_method should be cleared - if result.ModelList[0].AuthMethod != "" { - t.Errorf("expected openai model auth_method cleared, got %q", result.ModelList[0].AuthMethod) - } - // Anthropic model should be unchanged - if result.ModelList[1].AuthMethod != "token" { - t.Errorf("expected anthropic model auth_method unchanged, got %q", result.ModelList[1].AuthMethod) - } -} - -func TestClearAllAuthMethodsInConfig(t *testing.T) { - cfg := &config.Config{ - ModelList: []config.ModelConfig{ - {ModelName: "gpt-4o", Model: "openai/gpt-4o", AuthMethod: "oauth"}, - {ModelName: "claude", Model: "anthropic/claude-sonnet-4.6", AuthMethod: "token"}, - {ModelName: "gemini", Model: "antigravity/gemini-3-flash", AuthMethod: "oauth"}, - }, - } - path := writeTempConfigViaSave(t, cfg) - - clearAllAuthMethodsInConfig(path) - - result := loadTempConfig(t, path) - - for i, m := range result.ModelList { - if m.AuthMethod != "" { - t.Errorf("model[%d] auth_method not cleared, got %q", i, m.AuthMethod) - } - } -} diff --git a/cmd/picoclaw-launcher/internal/server/auth_handlers.go b/cmd/picoclaw-launcher/internal/server/auth_handlers.go deleted file mode 100644 index 3b48f9739..000000000 --- a/cmd/picoclaw-launcher/internal/server/auth_handlers.go +++ /dev/null @@ -1,315 +0,0 @@ -package server - -import ( - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "sync" - "time" - - "github.com/sipeed/picoclaw/pkg/auth" - "github.com/sipeed/picoclaw/pkg/providers" -) - -// oauthSession stores in-flight OAuth state for browser-based flows. -type oauthSession struct { - Provider string - PKCE auth.PKCECodes - State string - RedirectURI string - OAuthCfg auth.OAuthProviderConfig - ConfigPath string -} - -// deviceCodeSession stores in-flight device code flow state. -type deviceCodeSession struct { - mu sync.Mutex - Provider string - Info *auth.DeviceCodeInfo - OAuthCfg auth.OAuthProviderConfig - ConfigPath string - Status string // "pending", "success", "error" - Error string - Done bool -} - -var ( - oauthSessions = map[string]*oauthSession{} // keyed by state - oauthSessionsMu sync.Mutex - - activeDeviceSession *deviceCodeSession - activeDeviceSessionMu sync.Mutex -) - -// handleOpenAILogin starts the OpenAI device code flow and returns device code info to the frontend. -func handleOpenAILogin(w http.ResponseWriter, configPath string) { - // Check if there's already a pending device code session - activeDeviceSessionMu.Lock() - if activeDeviceSession != nil { - activeDeviceSession.mu.Lock() - if !activeDeviceSession.Done { - resp := map[string]any{ - "status": "pending", - "device_url": activeDeviceSession.Info.VerifyURL, - "user_code": activeDeviceSession.Info.UserCode, - "message": "Device code flow already in progress. Enter the code in your browser.", - } - activeDeviceSession.mu.Unlock() - activeDeviceSessionMu.Unlock() - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(resp) - return - } - activeDeviceSession.mu.Unlock() - } - activeDeviceSessionMu.Unlock() - - // Request a device code - oauthCfg := auth.OpenAIOAuthConfig() - info, err := auth.RequestDeviceCode(oauthCfg) - if err != nil { - http.Error(w, fmt.Sprintf("Failed to request device code: %v", err), http.StatusInternalServerError) - return - } - - session := &deviceCodeSession{ - Provider: "openai", - Info: info, - OAuthCfg: oauthCfg, - ConfigPath: configPath, - Status: "pending", - } - - activeDeviceSessionMu.Lock() - activeDeviceSession = session - activeDeviceSessionMu.Unlock() - - // Start background polling - go func() { - deadline := time.After(15 * time.Minute) - ticker := time.NewTicker(time.Duration(info.Interval) * time.Second) - defer ticker.Stop() - - for { - select { - case <-deadline: - session.mu.Lock() - session.Status = "error" - session.Error = "Authentication timed out after 15 minutes" - session.Done = true - session.mu.Unlock() - return - case <-ticker.C: - cred, err := auth.PollDeviceCodeOnce(oauthCfg, info.DeviceAuthID, info.UserCode) - if err != nil { - continue // Still pending - } - if cred != nil { - if saveErr := auth.SetCredential("openai", cred); saveErr != nil { - session.mu.Lock() - session.Status = "error" - session.Error = saveErr.Error() - session.Done = true - session.mu.Unlock() - return - } - updateConfigAfterLogin(configPath, "openai", cred) - session.mu.Lock() - session.Status = "success" - session.Done = true - session.mu.Unlock() - log.Printf("OpenAI device code login successful (account: %s)", cred.AccountID) - return - } - } - } - }() - - // Return device code info to frontend - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]any{ - "status": "pending", - "device_url": info.VerifyURL, - "user_code": info.UserCode, - "message": "Open the URL and enter the code to authenticate.", - }) -} - -// handleAnthropicLogin saves a pasted API token for Anthropic. -func handleAnthropicLogin(w http.ResponseWriter, token, configPath string) { - if token == "" { - http.Error(w, "Token is required for Anthropic login", http.StatusBadRequest) - return - } - - cred := &auth.AuthCredential{ - AccessToken: token, - Provider: "anthropic", - AuthMethod: "token", - } - - if err := auth.SetCredential("anthropic", cred); err != nil { - http.Error(w, fmt.Sprintf("Failed to save credentials: %v", err), http.StatusInternalServerError) - return - } - - updateConfigAfterLogin(configPath, "anthropic", cred) - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{ - "status": "success", - "message": "Anthropic token saved", - }) -} - -// handleGoogleAntigravityLogin generates a PKCE + auth URL and returns it to the frontend. -func handleGoogleAntigravityLogin(w http.ResponseWriter, r *http.Request, configPath string) { - oauthCfg := auth.GoogleAntigravityOAuthConfig() - - pkce, err := auth.GeneratePKCE() - if err != nil { - http.Error(w, fmt.Sprintf("Failed to generate PKCE: %v", err), http.StatusInternalServerError) - return - } - - state, err := auth.GenerateState() - if err != nil { - http.Error(w, fmt.Sprintf("Failed to generate state: %v", err), http.StatusInternalServerError) - return - } - - // Build redirect URI pointing to picoclaw-launcher's own callback - scheme := "http" - redirectURI := fmt.Sprintf("%s://%s/auth/callback", scheme, r.Host) - - authURL := auth.BuildAuthorizeURL(oauthCfg, pkce, state, redirectURI) - - // Store session for callback - oauthSessionsMu.Lock() - oauthSessions[state] = &oauthSession{ - Provider: "google-antigravity", - PKCE: pkce, - State: state, - RedirectURI: redirectURI, - OAuthCfg: oauthCfg, - ConfigPath: configPath, - } - oauthSessionsMu.Unlock() - - // Clean up stale sessions after 10 minutes - go func() { - time.Sleep(10 * time.Minute) - oauthSessionsMu.Lock() - delete(oauthSessions, state) - oauthSessionsMu.Unlock() - }() - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{ - "status": "redirect", - "auth_url": authURL, - "message": "Open the URL to authenticate with Google.", - }) -} - -// handleOAuthCallback processes the OAuth callback from Google Antigravity. -func handleOAuthCallback(w http.ResponseWriter, r *http.Request) { - state := r.URL.Query().Get("state") - code := r.URL.Query().Get("code") - - oauthSessionsMu.Lock() - session, ok := oauthSessions[state] - if ok { - delete(oauthSessions, state) - } - oauthSessionsMu.Unlock() - - if !ok { - http.Error(w, "Invalid or expired OAuth state", http.StatusBadRequest) - return - } - - if code == "" { - errMsg := r.URL.Query().Get("error") - w.Header().Set("Content-Type", "text/html") - fmt.Fprintf( - w, - `

Authentication failed

%s

You can close this window.

`, - errMsg, - ) - return - } - - cred, err := auth.ExchangeCodeForTokens(session.OAuthCfg, code, session.PKCE.CodeVerifier, session.RedirectURI) - if err != nil { - w.Header().Set("Content-Type", "text/html") - fmt.Fprintf( - w, - `

Authentication failed

%s

You can close this window.

`, - err.Error(), - ) - return - } - - cred.Provider = session.Provider - - // Fetch user info for Google Antigravity - if session.Provider == "google-antigravity" { - if email, err := fetchGoogleUserEmail(cred.AccessToken); err == nil { - cred.Email = email - } - if projectID, err := providers.FetchAntigravityProjectID(cred.AccessToken); err == nil { - cred.ProjectID = projectID - } - } - - if err := auth.SetCredential(session.Provider, cred); err != nil { - w.Header().Set("Content-Type", "text/html") - fmt.Fprintf(w, `

Failed to save credentials

%s

`, err.Error()) - return - } - - updateConfigAfterLogin(session.ConfigPath, session.Provider, cred) - - // Redirect back to picoclaw-launcher UI - w.Header().Set("Content-Type", "text/html") - fmt.Fprintf(w, ` -

Authentication successful!

-

Redirecting back to Config Editor...

- - `) -} - -// fetchGoogleUserEmail retrieves the user's email from Google's userinfo endpoint. -func fetchGoogleUserEmail(accessToken string) (string, error) { - req, err := http.NewRequest("GET", "https://www.googleapis.com/oauth2/v2/userinfo", nil) - if err != nil { - return "", err - } - req.Header.Set("Authorization", "Bearer "+accessToken) - - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("reading userinfo response: %w", err) - } - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("userinfo request failed: %s", string(body)) - } - - var userInfo struct { - Email string `json:"email"` - } - if err := json.Unmarshal(body, &userInfo); err != nil { - return "", err - } - return userInfo.Email, nil -} diff --git a/cmd/picoclaw-launcher/internal/server/logbuffer_test.go b/cmd/picoclaw-launcher/internal/server/logbuffer_test.go deleted file mode 100644 index dc525be16..000000000 --- a/cmd/picoclaw-launcher/internal/server/logbuffer_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package server - -import ( - "fmt" - "sync" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestLogBuffer_Basic(t *testing.T) { - buf := NewLogBuffer(5) - - // Empty buffer - lines, total, runID := buf.LinesSince(0) - assert.Nil(t, lines) - assert.Equal(t, 0, total) - assert.Equal(t, 0, runID) - - // Append some lines - buf.Append("line1") - buf.Append("line2") - buf.Append("line3") - - lines, total, runID = buf.LinesSince(0) - assert.Equal(t, []string{"line1", "line2", "line3"}, lines) - assert.Equal(t, 3, total) - assert.Equal(t, 0, runID) - - // Incremental read - lines, total, _ = buf.LinesSince(2) - assert.Equal(t, []string{"line3"}, lines) - assert.Equal(t, 3, total) - - // No new lines - lines, total, _ = buf.LinesSince(3) - assert.Nil(t, lines) - assert.Equal(t, 3, total) -} - -func TestLogBuffer_Wrap(t *testing.T) { - buf := NewLogBuffer(3) - - buf.Append("a") - buf.Append("b") - buf.Append("c") - buf.Append("d") // evicts "a" - buf.Append("e") // evicts "b" - - lines, total, _ := buf.LinesSince(0) - assert.Equal(t, []string{"c", "d", "e"}, lines) - assert.Equal(t, 5, total) - - // Incremental after wrap - lines, total, _ = buf.LinesSince(3) - assert.Equal(t, []string{"d", "e"}, lines) - assert.Equal(t, 5, total) - - // Offset too old (before buffer start), get all buffered - lines, total, _ = buf.LinesSince(1) - assert.Equal(t, []string{"c", "d", "e"}, lines) - assert.Equal(t, 5, total) -} - -func TestLogBuffer_Reset(t *testing.T) { - buf := NewLogBuffer(5) - - buf.Append("before") - assert.Equal(t, 0, buf.RunID()) - - buf.Reset() - assert.Equal(t, 1, buf.RunID()) - assert.Equal(t, 0, buf.Total()) - - lines, total, runID := buf.LinesSince(0) - assert.Nil(t, lines) - assert.Equal(t, 0, total) - assert.Equal(t, 1, runID) - - buf.Append("after") - lines, total, runID = buf.LinesSince(0) - assert.Equal(t, []string{"after"}, lines) - assert.Equal(t, 1, total) - assert.Equal(t, 1, runID) -} - -func TestLogBuffer_Concurrent(t *testing.T) { - buf := NewLogBuffer(100) - var wg sync.WaitGroup - - // 10 writers - for i := range 10 { - wg.Add(1) - go func(id int) { - defer wg.Done() - for j := range 50 { - buf.Append(fmt.Sprintf("writer-%d-line-%d", id, j)) - } - }(i) - } - - // 5 readers - for range 5 { - wg.Add(1) - go func() { - defer wg.Done() - for range 100 { - buf.LinesSince(0) - } - }() - } - - wg.Wait() - - assert.Equal(t, 500, buf.Total()) -} diff --git a/cmd/picoclaw-launcher/internal/server/process.go b/cmd/picoclaw-launcher/internal/server/process.go deleted file mode 100644 index bc2129bf5..000000000 --- a/cmd/picoclaw-launcher/internal/server/process.go +++ /dev/null @@ -1,232 +0,0 @@ -package server - -import ( - "bufio" - "encoding/json" - "fmt" - "io" - "log" - "net" - "net/http" - "os" - "os/exec" - "path/filepath" - "runtime" - "strconv" - "time" - - "github.com/sipeed/picoclaw/pkg/config" -) - -// gatewayLogs stores captured stdout/stderr from the gateway process launched by the launcher. -var gatewayLogs = NewLogBuffer(200) - -// RegisterProcessAPI registers endpoints to start, stop and check status of the picoclaw gateway. -func RegisterProcessAPI(mux *http.ServeMux, absPath string) { - mux.HandleFunc("GET /api/process/status", func(w http.ResponseWriter, r *http.Request) { - handleStatusGateway(w, r, absPath) - }) - mux.HandleFunc("POST /api/process/start", handleStartGateway) - mux.HandleFunc("POST /api/process/stop", handleStopGateway) -} - -func handleStartGateway(w http.ResponseWriter, r *http.Request) { - // Locate picoclaw executable: - // 1. Try same directory as current executable - // 2. Fallback to just "picoclaw" (relies on $PATH) - execPath := "picoclaw" - - if exe, err := os.Executable(); err == nil { - dir := filepath.Dir(exe) - candidate := filepath.Join(dir, "picoclaw") - if runtime.GOOS == "windows" { - candidate += ".exe" - } - - if info, err := os.Stat(candidate); err == nil && !info.IsDir() { - execPath = candidate - } - } - - cmd := exec.Command(execPath, "gateway") - - stdoutPipe, err := cmd.StdoutPipe() - if err != nil { - log.Printf("Failed to create stdout pipe: %v\n", err) - http.Error(w, fmt.Sprintf("Failed to start gateway: %v", err), http.StatusInternalServerError) - return - } - - stderrPipe, err := cmd.StderrPipe() - if err != nil { - log.Printf("Failed to create stderr pipe: %v\n", err) - http.Error(w, fmt.Sprintf("Failed to start gateway: %v", err), http.StatusInternalServerError) - return - } - - // Clear old logs and increment runID before starting - gatewayLogs.Reset() - - if err := cmd.Start(); err != nil { - log.Printf("Failed to start picoclaw gateway: %v\n", err) - http.Error(w, fmt.Sprintf("Failed to start gateway: %v", err), http.StatusInternalServerError) - return - } - - // Read stdout and stderr into the log buffer - go scanPipe(stdoutPipe, gatewayLogs) - go scanPipe(stderrPipe, gatewayLogs) - - // Wait for the process to exit in the background to avoid zombies - go func() { - if err := cmd.Wait(); err != nil { - log.Printf("Gateway process exited: %v\n", err) - } - }() - - log.Printf("Started picoclaw gateway (PID: %d) from %s\n", cmd.Process.Pid, execPath) - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]any{ - "status": "ok", - "pid": cmd.Process.Pid, - }) -} - -// scanPipe reads lines from r and appends them to buf. It returns when r reaches EOF. -func scanPipe(r io.Reader, buf *LogBuffer) { - scanner := bufio.NewScanner(r) - scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) // up to 1MB per line - - for scanner.Scan() { - buf.Append(scanner.Text()) - } -} - -func handleStopGateway(w http.ResponseWriter, r *http.Request) { - var err error - if runtime.GOOS == "windows" { - // Kill via taskkill finding picoclaw.exe (though it might kill this config tool if it's named picoclaw-launcher.exe...? No, /IM does exact match usually, but just to be safe let's stop exactly picoclaw.exe) - // Alternatively, we use powershell to kill processes with commandline containing 'gateway' - psCmd := `Get-WmiObject Win32_Process | Where-Object { $_.CommandLine -match 'picoclaw.*gateway' } | ForEach-Object { Stop-Process $_.ProcessId -Force }` - err = exec.Command("powershell", "-Command", psCmd).Run() - } else { - // Linux/macOS - err = exec.Command("pkill", "-f", "picoclaw gateway").Run() - } - - if err != nil { - log.Printf("Warning: Failed to stop gateway (perhaps not running?): %v\n", err) - // We still return 200 OK because pkill returns an error if no process was found - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]any{ - "status": "ok", // or "not_found" - "msg": "Stop command executed, but returned error (process might not be running).", - "error": err.Error(), - }) - return - } - - log.Printf("Stopped picoclaw gateway processes.\n") - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{ - "status": "ok", - }) -} - -func handleStatusGateway(w http.ResponseWriter, r *http.Request, absPath string) { - cfg, cfgErr := config.LoadConfig(absPath) - host := "127.0.0.1" - port := 18790 - if cfgErr == nil && cfg != nil { - if cfg.Gateway.Host != "" && cfg.Gateway.Host != "0.0.0.0" { - host = cfg.Gateway.Host - } - if cfg.Gateway.Port != 0 { - port = cfg.Gateway.Port - } - } - - url := fmt.Sprintf("http://%s/health", net.JoinHostPort(host, strconv.Itoa(port))) - client := http.Client{Timeout: 2 * time.Second} - resp, err := client.Get(url) - - // Build the response data map - data := map[string]any{} - - if err != nil { - data["process_status"] = "stopped" - data["error"] = err.Error() - } else { - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - data["process_status"] = "error" - data["status_code"] = resp.StatusCode - } else { - var healthData map[string]any - if decErr := json.NewDecoder(resp.Body).Decode(&healthData); decErr != nil { - data["process_status"] = "error" - data["error"] = "invalid response from gateway" - } else { - // Gateway is running and responded properly — merge health data - for k, v := range healthData { - data[k] = v - } - data["process_status"] = "running" - } - } - } - - // Append log data from the buffer - appendLogData(r, data) - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(data) -} - -// appendLogData reads log_offset and log_run_id query params from the request and -// populates the response data map with incremental log lines. -func appendLogData(r *http.Request, data map[string]any) { - clientOffset := 0 - clientRunID := -1 - - if v := r.URL.Query().Get("log_offset"); v != "" { - if n, err := strconv.Atoi(v); err == nil { - clientOffset = n - } - } - - if v := r.URL.Query().Get("log_run_id"); v != "" { - if n, err := strconv.Atoi(v); err == nil { - clientRunID = n - } - } - - runID := gatewayLogs.RunID() - - // If runID is 0 (never reset = never launched from this launcher), report no source - if runID == 0 { - data["logs"] = []string{} - data["log_total"] = 0 - data["log_run_id"] = 0 - data["log_source"] = "none" - return - } - - // If the client's runID doesn't match, send all buffered lines (gateway restarted) - offset := clientOffset - if clientRunID != runID { - offset = 0 - } - - lines, total, runID := gatewayLogs.LinesSince(offset) - if lines == nil { - lines = []string{} - } - - data["logs"] = lines - data["log_total"] = total - data["log_run_id"] = runID - data["log_source"] = "launcher" -} diff --git a/cmd/picoclaw-launcher/internal/server/server.go b/cmd/picoclaw-launcher/internal/server/server.go deleted file mode 100644 index 4fc68f04c..000000000 --- a/cmd/picoclaw-launcher/internal/server/server.go +++ /dev/null @@ -1,196 +0,0 @@ -package server - -import ( - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "time" - - "github.com/sipeed/picoclaw/pkg/auth" - "github.com/sipeed/picoclaw/pkg/config" -) - -const DefaultPort = "18800" - -// providerStatus represents the auth status of a single provider in API responses. -type providerStatus struct { - Provider string `json:"provider"` - AuthMethod string `json:"auth_method"` - Status string `json:"status"` - AccountID string `json:"account_id,omitempty"` - Email string `json:"email,omitempty"` - ProjectID string `json:"project_id,omitempty"` - ExpiresAt string `json:"expires_at,omitempty"` -} - -// ── Route registration ─────────────────────────────────────────── - -func RegisterConfigAPI(mux *http.ServeMux, absPath string) { - // GET /api/config — read config - mux.HandleFunc("GET /api/config", func(w http.ResponseWriter, r *http.Request) { - cfg, err := config.LoadConfig(absPath) - if err != nil { - http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError) - return - } - w.Header().Set("Content-Type", "application/json") - resp := map[string]any{ - "config": cfg, - "path": absPath, - } - enc := json.NewEncoder(w) - enc.SetIndent("", " ") - if err := enc.Encode(resp); err != nil { - log.Printf("Failed to encode response: %v", err) - } - }) - - // PUT /api/config — save config - mux.HandleFunc("PUT /api/config", func(w http.ResponseWriter, r *http.Request) { - body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) - if err != nil { - http.Error(w, "Failed to read request body", http.StatusBadRequest) - return - } - defer r.Body.Close() - - var cfg config.Config - if err := json.Unmarshal(body, &cfg); err != nil { - http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) - return - } - - if err := config.SaveConfig(absPath, &cfg); err != nil { - http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) - }) -} - -func RegisterAuthAPI(mux *http.ServeMux, absPath string) { - // GET /api/auth/status — all authenticated providers + pending login state - mux.HandleFunc("GET /api/auth/status", func(w http.ResponseWriter, r *http.Request) { - store, err := auth.LoadStore() - if err != nil { - http.Error(w, fmt.Sprintf("Failed to load auth store: %v", err), http.StatusInternalServerError) - return - } - - result := []providerStatus{} - for name, cred := range store.Credentials { - status := "active" - if cred.IsExpired() { - status = "expired" - } else if cred.NeedsRefresh() { - status = "needs_refresh" - } - ps := providerStatus{ - Provider: name, - AuthMethod: cred.AuthMethod, - Status: status, - AccountID: cred.AccountID, - Email: cred.Email, - ProjectID: cred.ProjectID, - } - if !cred.ExpiresAt.IsZero() { - ps.ExpiresAt = cred.ExpiresAt.Format(time.RFC3339) - } - result = append(result, ps) - } - - // Include pending device code state - var pendingDevice map[string]any - activeDeviceSessionMu.Lock() - if activeDeviceSession != nil { - activeDeviceSession.mu.Lock() - pendingDevice = map[string]any{ - "provider": activeDeviceSession.Provider, - "status": activeDeviceSession.Status, - "device_url": activeDeviceSession.Info.VerifyURL, - "user_code": activeDeviceSession.Info.UserCode, - } - if activeDeviceSession.Error != "" { - pendingDevice["error"] = activeDeviceSession.Error - } - if activeDeviceSession.Done { - activeDeviceSession.mu.Unlock() - activeDeviceSession = nil - } else { - activeDeviceSession.mu.Unlock() - } - } - activeDeviceSessionMu.Unlock() - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]any{ - "providers": result, - "pending_device": pendingDevice, - }) - }) - - // POST /api/auth/login — initiate provider login - mux.HandleFunc("POST /api/auth/login", func(w http.ResponseWriter, r *http.Request) { - var req struct { - Provider string `json:"provider"` - Token string `json:"token,omitempty"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - return - } - - switch req.Provider { - case "openai": - handleOpenAILogin(w, absPath) - case "anthropic": - handleAnthropicLogin(w, req.Token, absPath) - case "google-antigravity", "antigravity": - handleGoogleAntigravityLogin(w, r, absPath) - default: - http.Error( - w, - fmt.Sprintf( - "Unsupported provider: %s (supported: openai, anthropic, google-antigravity)", - req.Provider, - ), - http.StatusBadRequest, - ) - } - }) - - // POST /api/auth/logout — logout a provider - mux.HandleFunc("POST /api/auth/logout", func(w http.ResponseWriter, r *http.Request) { - var req struct { - Provider string `json:"provider"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - return - } - - if req.Provider == "" { - if err := auth.DeleteAllCredentials(); err != nil { - http.Error(w, fmt.Sprintf("Failed to logout: %v", err), http.StatusInternalServerError) - return - } - clearAllAuthMethodsInConfig(absPath) - } else { - if err := auth.DeleteCredential(req.Provider); err != nil { - http.Error(w, fmt.Sprintf("Failed to logout: %v", err), http.StatusInternalServerError) - return - } - clearAuthMethodInConfig(absPath, req.Provider) - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) - }) - - // GET /auth/callback — OAuth browser callback for Google Antigravity - mux.HandleFunc("GET /auth/callback", handleOAuthCallback) -} diff --git a/cmd/picoclaw-launcher/internal/server/server_test.go b/cmd/picoclaw-launcher/internal/server/server_test.go deleted file mode 100644 index c87e93d8c..000000000 --- a/cmd/picoclaw-launcher/internal/server/server_test.go +++ /dev/null @@ -1,247 +0,0 @@ -package server - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/sipeed/picoclaw/pkg/config" -) - -// ── Config API tests ───────────────────────────────────────────── - -func setupConfigMux(t *testing.T, cfg *config.Config) (*http.ServeMux, string) { - t.Helper() - dir := t.TempDir() - path := filepath.Join(dir, "config.json") - data, err := json.MarshalIndent(cfg, "", " ") - if err != nil { - t.Fatalf("marshal config: %v", err) - } - if err := os.WriteFile(path, data, 0o600); err != nil { - t.Fatalf("write config: %v", err) - } - - mux := http.NewServeMux() - RegisterConfigAPI(mux, path) - RegisterAuthAPI(mux, path) - return mux, path -} - -func TestGetConfig(t *testing.T) { - cfg := &config.Config{ - ModelList: []config.ModelConfig{ - {ModelName: "gpt-4o", Model: "openai/gpt-4o"}, - }, - } - mux, path := setupConfigMux(t, cfg) - - req := httptest.NewRequest("GET", "/api/config", nil) - w := httptest.NewRecorder() - mux.ServeHTTP(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("GET /api/config: expected 200, got %d: %s", w.Code, w.Body.String()) - } - - var resp struct { - Config config.Config `json:"config"` - Path string `json:"path"` - } - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { - t.Fatalf("decode response: %v", err) - } - - if resp.Path != path { - t.Errorf("expected path %q, got %q", path, resp.Path) - } - if len(resp.Config.ModelList) != 1 { - t.Errorf("expected 1 model, got %d", len(resp.Config.ModelList)) - } -} - -func TestGetConfig_MissingFile_ReturnsDefault(t *testing.T) { - mux := http.NewServeMux() - RegisterConfigAPI(mux, "/tmp/nonexistent-picoclaw-launcher-test/config.json") - - req := httptest.NewRequest("GET", "/api/config", nil) - w := httptest.NewRecorder() - mux.ServeHTTP(w, req) - - // LoadConfig returns a default empty config when file is missing - if w.Code != http.StatusOK { - t.Errorf("expected 200 for missing file (default config), got %d", w.Code) - } -} - -func TestPutConfig(t *testing.T) { - cfg := &config.Config{} - mux, path := setupConfigMux(t, cfg) - - newCfg := config.Config{ - ModelList: []config.ModelConfig{ - {ModelName: "claude", Model: "anthropic/claude-sonnet-4.6", AuthMethod: "token"}, - }, - } - body, _ := json.Marshal(newCfg) - - req := httptest.NewRequest("PUT", "/api/config", strings.NewReader(string(body))) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - mux.ServeHTTP(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("PUT /api/config: expected 200, got %d: %s", w.Code, w.Body.String()) - } - - saved, err := config.LoadConfig(path) - if err != nil { - t.Fatalf("load saved config: %v", err) - } - if len(saved.ModelList) != 1 { - t.Fatalf("expected 1 model saved, got %d", len(saved.ModelList)) - } - if saved.ModelList[0].Model != "anthropic/claude-sonnet-4.6" { - t.Errorf("expected model anthropic/claude-sonnet-4.6, got %q", saved.ModelList[0].Model) - } -} - -func TestPutConfig_InvalidJSON(t *testing.T) { - cfg := &config.Config{} - mux, _ := setupConfigMux(t, cfg) - - req := httptest.NewRequest("PUT", "/api/config", strings.NewReader("{invalid")) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - mux.ServeHTTP(w, req) - - if w.Code != http.StatusBadRequest { - t.Errorf("expected 400 for invalid JSON, got %d", w.Code) - } -} - -// ── Auth API tests ─────────────────────────────────────────────── - -func TestAuthStatus(t *testing.T) { - cfg := &config.Config{} - mux, _ := setupConfigMux(t, cfg) - - req := httptest.NewRequest("GET", "/api/auth/status", nil) - w := httptest.NewRecorder() - mux.ServeHTTP(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("GET /api/auth/status: expected 200, got %d: %s", w.Code, w.Body.String()) - } - - var resp struct { - Providers []providerStatus `json:"providers"` - PendingDevice map[string]any `json:"pending_device"` - } - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { - t.Fatalf("decode response: %v", err) - } - - // providers should be a non-nil list (could be empty) - if resp.Providers == nil { - t.Error("providers should not be nil") - } -} - -func TestAuthLogin_UnsupportedProvider(t *testing.T) { - cfg := &config.Config{} - mux, _ := setupConfigMux(t, cfg) - - body := `{"provider": "unsupported"}` - req := httptest.NewRequest("POST", "/api/auth/login", strings.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - mux.ServeHTTP(w, req) - - if w.Code != http.StatusBadRequest { - t.Errorf("expected 400 for unsupported provider, got %d", w.Code) - } -} - -func TestAuthLogin_AnthropicNoToken(t *testing.T) { - cfg := &config.Config{} - mux, _ := setupConfigMux(t, cfg) - - body := `{"provider": "anthropic"}` - req := httptest.NewRequest("POST", "/api/auth/login", strings.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - mux.ServeHTTP(w, req) - - if w.Code != http.StatusBadRequest { - t.Errorf("expected 400 for anthropic without token, got %d", w.Code) - } -} - -func TestAuthLogin_InvalidBody(t *testing.T) { - cfg := &config.Config{} - mux, _ := setupConfigMux(t, cfg) - - req := httptest.NewRequest("POST", "/api/auth/login", strings.NewReader("{bad")) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - mux.ServeHTTP(w, req) - - if w.Code != http.StatusBadRequest { - t.Errorf("expected 400 for invalid JSON body, got %d", w.Code) - } -} - -func TestAuthLogout_InvalidBody(t *testing.T) { - cfg := &config.Config{} - mux, _ := setupConfigMux(t, cfg) - - req := httptest.NewRequest("POST", "/api/auth/logout", strings.NewReader("{bad")) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - mux.ServeHTTP(w, req) - - if w.Code != http.StatusBadRequest { - t.Errorf("expected 400 for invalid body, got %d", w.Code) - } -} - -func TestOAuthCallback_InvalidState(t *testing.T) { - cfg := &config.Config{} - mux, _ := setupConfigMux(t, cfg) - - req := httptest.NewRequest("GET", "/auth/callback?state=invalid&code=test", nil) - w := httptest.NewRecorder() - mux.ServeHTTP(w, req) - - if w.Code != http.StatusBadRequest { - t.Errorf("expected 400 for invalid state, got %d", w.Code) - } -} - -// ── Utility tests ──────────────────────────────────────────────── - -func TestDefaultConfigPath(t *testing.T) { - path := DefaultConfigPath() - if path == "" { - t.Error("defaultConfigPath should not return empty") - } - if !strings.HasSuffix(path, filepath.Join(".picoclaw", "config.json")) { - t.Errorf("expected path ending with .picoclaw/config.json, got %q", path) - } -} - -func TestGetLocalIP(t *testing.T) { - // Just ensure it doesn't panic; IP may or may not be available - ip := GetLocalIP() - if ip != "" { - // If returned, should look like an IP - if !strings.Contains(ip, ".") { - t.Errorf("getLocalIP returned non-IPv4 looking string: %q", ip) - } - } -} diff --git a/cmd/picoclaw-launcher/internal/server/utils.go b/cmd/picoclaw-launcher/internal/server/utils.go deleted file mode 100644 index a46adbece..000000000 --- a/cmd/picoclaw-launcher/internal/server/utils.go +++ /dev/null @@ -1,28 +0,0 @@ -package server - -import ( - "net" - "os" - "path/filepath" -) - -func DefaultConfigPath() string { - home, err := os.UserHomeDir() - if err != nil { - return "config.json" - } - return filepath.Join(home, ".picoclaw", "config.json") -} - -func GetLocalIP() string { - addrs, err := net.InterfaceAddrs() - if err != nil { - return "" - } - for _, a := range addrs { - if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.To4() != nil { - return ipnet.IP.String() - } - } - return "" -} diff --git a/cmd/picoclaw-launcher/internal/ui/index.html b/cmd/picoclaw-launcher/internal/ui/index.html deleted file mode 100644 index e77ef4fea..000000000 --- a/cmd/picoclaw-launcher/internal/ui/index.html +++ /dev/null @@ -1,2009 +0,0 @@ - - - - - - - - PicoClaw Config - - - - - - - - - -
-
- -

PicoClaw Config

-
-
- - -
-
- - -
-
- -
- -
-
- - - - -
-
-
Models
-
Manage LLM model configurations. Models without an API key are grayed out. Only available models can be set as primary.
-
- -
-
-
- -
-
Provider Authentication
-
-
-
-
- OpenAI - Not logged in -
-
-
- -
-
-
-
- Anthropic - Not logged in -
-
-
- -
-
-
-
- Google Antigravity - Not logged in -
-
-
- -
-
-
-
- - -
-
-
-
-
-
-
-
-
-
-
-
-
- - -
-
Gateway Logs
-
Real-time output from the gateway process.
-
-
- -
-
- -
-
-
-
No logs available. Start the gateway to see output here.
-
-
- - -
-
Raw JSON
-
Directly edit the configuration file.
-
- config.json - - -
-
- - - -
-
- -
-
-
-
-
-
-
-
- - - - -
- PicoClaw Config - - -
- - - - - diff --git a/cmd/picoclaw-launcher/main.go b/cmd/picoclaw-launcher/main.go deleted file mode 100644 index 3323c31a8..000000000 --- a/cmd/picoclaw-launcher/main.go +++ /dev/null @@ -1,127 +0,0 @@ -// PicoClaw Launcher - Standalone HTTP service -// -// Provides a web-based JSON editor for picoclaw config files, -// with OAuth provider authentication support. -// -// Usage: -// -// go build -o picoclaw-launcher ./cmd/picoclaw-launcher/ -// ./picoclaw-launcher [config.json] -// ./picoclaw-launcher -public config.json - -package main - -import ( - "embed" - "flag" - "fmt" - "io/fs" - "log" - "net/http" - "os" - "os/exec" - "path/filepath" - "runtime" - "time" - - "github.com/sipeed/picoclaw/cmd/picoclaw-launcher/internal/server" -) - -//go:embed internal/ui/index.html -var staticFiles embed.FS - -func main() { - public := flag.Bool("public", false, "Listen on all interfaces (0.0.0.0) instead of localhost only") - flag.Usage = func() { - fmt.Fprintf(os.Stderr, "PicoClaw Launcher - A web-based configuration editor\n\n") - fmt.Fprintf(os.Stderr, "Usage: %s [options] [config.json]\n\n", os.Args[0]) - fmt.Fprintf(os.Stderr, "Arguments:\n") - fmt.Fprintf(os.Stderr, " config.json Path to the configuration file (default: ~/.picoclaw/config.json)\n\n") - fmt.Fprintf(os.Stderr, "Options:\n") - flag.PrintDefaults() - fmt.Fprintf(os.Stderr, "\nExamples:\n") - fmt.Fprintf(os.Stderr, " %s Use default config path\n", os.Args[0]) - fmt.Fprintf(os.Stderr, " %s ./config.json Specify a config file\n", os.Args[0]) - fmt.Fprintf( - os.Stderr, - " %s -public ./config.json Allow access from other devices on the network\n", - os.Args[0], - ) - } - flag.Parse() - - configPath := server.DefaultConfigPath() - if flag.NArg() > 0 { - configPath = flag.Arg(0) - } - - absPath, err := filepath.Abs(configPath) - if err != nil { - log.Fatalf("Failed to resolve config path: %v", err) - } - - var addr string - if *public { - addr = "0.0.0.0:" + server.DefaultPort - } else { - addr = "127.0.0.1:" + server.DefaultPort - } - - mux := http.NewServeMux() - server.RegisterConfigAPI(mux, absPath) - server.RegisterAuthAPI(mux, absPath) - server.RegisterProcessAPI(mux, absPath) - - staticFS, err := fs.Sub(staticFiles, "internal/ui") - if err != nil { - log.Fatalf("Failed to create sub filesystem: %v", err) - } - mux.Handle("/", http.FileServer(http.FS(staticFS))) - - // Print startup banner - fmt.Println("=============================================") - fmt.Println(" PicoClaw Launcher") - fmt.Println("=============================================") - fmt.Printf(" Config file : %s\n", absPath) - fmt.Printf(" Listen addr : %s\n\n", addr) - fmt.Println(" Open the following URL in your browser") - fmt.Println(" to view and edit the configuration:") - fmt.Println() - fmt.Printf(" >> http://localhost:%s <<\n", server.DefaultPort) - if *public { - if ip := server.GetLocalIP(); ip != "" { - fmt.Printf(" >> http://%s:%s <<\n", ip, server.DefaultPort) - } - } - fmt.Println() - // fmt.Println("=============================================") - - go func() { - // Wait briefly to ensure the server is ready before opening the browser - time.Sleep(500 * time.Millisecond) - url := "http://localhost:" + server.DefaultPort - if err := openBrowser(url); err != nil { - log.Printf("Warning: Failed to auto-open browser: %v\n", err) - } - }() - - if err := http.ListenAndServe(addr, mux); err != nil { - log.Fatalf("Server failed: %v", err) - } -} - -// openBrowser automatically opens the given URL in the default browser. -func openBrowser(url string) error { - var err error - switch runtime.GOOS { - case "linux": - err = exec.Command("xdg-open", url).Start() - case "windows": - err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() - case "darwin": - err = exec.Command("open", url).Start() - default: - err = fmt.Errorf("unsupported platform") - } - return err -} diff --git a/web/Makefile b/web/Makefile new file mode 100644 index 000000000..559005956 --- /dev/null +++ b/web/Makefile @@ -0,0 +1,38 @@ +.PHONY: dev dev-frontend dev-backend build test lint clean + +# Run both frontend and backend dev servers +dev: + @if [ ! -f backend/picoclaw-web ] || [ ! -d backend/dist ]; then \ + echo "Build artifacts not found, building..."; \ + $(MAKE) build; \ + fi + @echo "Starting backend and frontend dev servers..." + @$(MAKE) dev-backend & $(MAKE) dev-frontend + +# Start frontend dev server (Vite, with proxy to backend) +dev-frontend: + cd frontend && pnpm dev + +# Start backend dev server +dev-backend: + cd backend && go run . + +# Build frontend and embed into Go binary +build: + cd frontend && pnpm build:backend + cd backend && go build -o picoclaw-web . + +# Run all tests +test: + cd backend && go test ./... + cd frontend && pnpm lint + +# Lint and format +lint: + cd backend && go vet ./... + cd frontend && pnpm check + +# Clean build artifacts +clean: + rm -rf frontend/dist backend/dist backend/picoclaw-web + mkdir -p backend/dist && touch backend/dist/.gitkeep diff --git a/web/README.md b/web/README.md new file mode 100644 index 000000000..6ec247bae --- /dev/null +++ b/web/README.md @@ -0,0 +1,51 @@ +# Picoclaw Web + +This directory contains the standalone web service for `picoclaw`. +It provides a complete unified web interface, acting as a dashboard, configuration center, and interactive console (channel client) for the core `picoclaw` engine. + +## Architecture + +The service is structured as a monorepo containing both the backend and frontend code to ensure high cohesion and simplify deployment. + +* **`backend/`**: The Go-based web server. It provides RESTful APIs, manages WebSocket connections for chat, and handles the lifecycle of the `picoclaw` process. It eventually embeds the compiled frontend assets into a single executable. +* **`frontend/`**: The Vite + React + TanStack Router single-page application (SPA). It provides the interactive user interface. + +## Getting Started + +### Prerequisites + +* Go 1.25+ +* Node.js 20+ with pnpm + +### Development + +Run both the frontend dev server and the Go backend simultaneously: + +```bash +make dev +``` + +Or run them separately: + +```bash +make dev-frontend # Vite dev server +make dev-backend # Go backend +``` + +### Build + +Build the frontend and embed it into a single Go binary: + +```bash +make build +``` + +The output binary is `backend/picoclaw-web`. + +### Other Commands + +```bash +make test # Run backend tests and frontend lint +make lint # Run go vet and prettier/eslint +make clean # Remove all build artifacts +``` diff --git a/web/backend/.gitignore b/web/backend/.gitignore new file mode 100644 index 000000000..509042171 --- /dev/null +++ b/web/backend/.gitignore @@ -0,0 +1,19 @@ +# Go build output +*.exe +*.dll +*.so +*.dylib +*.test +*.out +picoclaw-web + +# Frontend build artifacts (embedded by Go) +dist/* +!dist/.gitkeep + +# OS +.DS_Store + +# Editors +.vscode/ +.idea/ \ No newline at end of file diff --git a/web/backend/api/channels.go b/web/backend/api/channels.go new file mode 100644 index 000000000..507882823 --- /dev/null +++ b/web/backend/api/channels.go @@ -0,0 +1,47 @@ +package api + +import ( + "encoding/json" + "net/http" +) + +type channelCatalogItem struct { + Name string `json:"name"` + ConfigKey string `json:"config_key"` + Variant string `json:"variant,omitempty"` +} + +var channelCatalog = []channelCatalogItem{ + {Name: "telegram", ConfigKey: "telegram"}, + {Name: "discord", ConfigKey: "discord"}, + {Name: "slack", ConfigKey: "slack"}, + {Name: "feishu", ConfigKey: "feishu"}, + {Name: "dingtalk", ConfigKey: "dingtalk"}, + {Name: "line", ConfigKey: "line"}, + {Name: "qq", ConfigKey: "qq"}, + {Name: "onebot", ConfigKey: "onebot"}, + {Name: "wecom", ConfigKey: "wecom"}, + {Name: "wecom_app", ConfigKey: "wecom_app"}, + {Name: "wecom_aibot", ConfigKey: "wecom_aibot"}, + {Name: "whatsapp", ConfigKey: "whatsapp", Variant: "bridge"}, + {Name: "whatsapp_native", ConfigKey: "whatsapp", Variant: "native"}, + {Name: "pico", ConfigKey: "pico"}, + {Name: "maixcam", ConfigKey: "maixcam"}, + {Name: "matrix", ConfigKey: "matrix"}, + {Name: "irc", ConfigKey: "irc"}, +} + +// registerChannelRoutes binds read-only channel catalog endpoints to the ServeMux. +func (h *Handler) registerChannelRoutes(mux *http.ServeMux) { + mux.HandleFunc("GET /api/channels/catalog", h.handleListChannelCatalog) +} + +// handleListChannelCatalog returns the channels supported by backend. +// +// GET /api/channels/catalog +func (h *Handler) handleListChannelCatalog(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{ + "channels": channelCatalog, + }) +} diff --git a/web/backend/api/config.go b/web/backend/api/config.go new file mode 100644 index 000000000..f160b42b6 --- /dev/null +++ b/web/backend/api/config.go @@ -0,0 +1,221 @@ +package api + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + + "github.com/sipeed/picoclaw/pkg/config" +) + +// registerConfigRoutes binds configuration management endpoints to the ServeMux. +func (h *Handler) registerConfigRoutes(mux *http.ServeMux) { + mux.HandleFunc("GET /api/config", h.handleGetConfig) + mux.HandleFunc("PUT /api/config", h.handleUpdateConfig) + mux.HandleFunc("PATCH /api/config", h.handlePatchConfig) +} + +// loadFilteredConfig loads the configuration and filters out default placeholder credentials +// (like API limits/keys) if the configuration file has not been created yet by the user. +func (h *Handler) loadFilteredConfig() (*config.Config, error) { + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + return nil, err + } + + configExists := false + if h.configPath != "" { + if _, err := os.Stat(h.configPath); err == nil { + configExists = true + } + } + + if !configExists { + for i := range cfg.ModelList { + cfg.ModelList[i].APIKey = "" + cfg.ModelList[i].AuthMethod = "" + } + } + + return cfg, nil +} + +// handleGetConfig returns the complete system configuration. +// +// GET /api/config +func (h *Handler) handleGetConfig(w http.ResponseWriter, r *http.Request) { + cfg, err := h.loadFilteredConfig() + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(cfg); err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + } +} + +// handleUpdateConfig updates the complete system configuration. +// +// PUT /api/config +func (h *Handler) handleUpdateConfig(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) + if err != nil { + http.Error(w, "Failed to read request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + var cfg config.Config + if err := json.Unmarshal(body, &cfg); err != nil { + http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) + return + } + + if errs := validateConfig(&cfg); len(errs) > 0 { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]any{ + "status": "validation_error", + "errors": errs, + }) + return + } + + if err := config.SaveConfig(h.configPath, &cfg); err != nil { + http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) +} + +// handlePatchConfig partially updates the system configuration using JSON Merge Patch (RFC 7396). +// Only the fields present in the request body will be updated; all other fields remain unchanged. +// +// PATCH /api/config +func (h *Handler) handlePatchConfig(w http.ResponseWriter, r *http.Request) { + patchBody, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) + if err != nil { + http.Error(w, "Failed to read request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + // Validate the patch is valid JSON + var patch map[string]any + if err = json.Unmarshal(patchBody, &patch); err != nil { + http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) + return + } + + // Load existing config and marshal to a map for merging + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError) + return + } + + existing, err := json.Marshal(cfg) + if err != nil { + http.Error(w, "Failed to serialize current config", http.StatusInternalServerError) + return + } + + var base map[string]any + if err = json.Unmarshal(existing, &base); err != nil { + http.Error(w, "Failed to parse current config", http.StatusInternalServerError) + return + } + + // Recursively merge patch into base + mergeMap(base, patch) + + // Convert merged map back to Config struct + merged, err := json.Marshal(base) + if err != nil { + http.Error(w, "Failed to serialize merged config", http.StatusInternalServerError) + return + } + + var newCfg config.Config + if err := json.Unmarshal(merged, &newCfg); err != nil { + http.Error(w, fmt.Sprintf("Merged config is invalid: %v", err), http.StatusBadRequest) + return + } + + if errs := validateConfig(&newCfg); len(errs) > 0 { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]any{ + "status": "validation_error", + "errors": errs, + }) + return + } + + if err := config.SaveConfig(h.configPath, &newCfg); err != nil { + http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) +} + +// validateConfig checks the config for common errors before saving. +// Returns a list of human-readable error strings; empty means valid. +func validateConfig(cfg *config.Config) []string { + var errs []string + + // Validate model_list entries + if err := cfg.ValidateModelList(); err != nil { + errs = append(errs, err.Error()) + } + + // Gateway port range + if cfg.Gateway.Port != 0 && (cfg.Gateway.Port < 1 || cfg.Gateway.Port > 65535) { + errs = append(errs, fmt.Sprintf("gateway.port %d is out of valid range (1-65535)", cfg.Gateway.Port)) + } + + // Pico channel: token required when enabled + if cfg.Channels.Pico.Enabled && cfg.Channels.Pico.Token == "" { + errs = append(errs, "channels.pico.token is required when pico channel is enabled") + } + + // Telegram: token required when enabled + if cfg.Channels.Telegram.Enabled && cfg.Channels.Telegram.Token == "" { + errs = append(errs, "channels.telegram.token is required when telegram channel is enabled") + } + + // Discord: token required when enabled + if cfg.Channels.Discord.Enabled && cfg.Channels.Discord.Token == "" { + errs = append(errs, "channels.discord.token is required when discord channel is enabled") + } + + return errs +} + +// mergeMap recursively merges src into dst (JSON Merge Patch semantics). +// - If a key in src has a null value, it is deleted from dst. +// - If both dst and src have a nested object for the same key, merge recursively. +// - Otherwise the value from src overwrites dst. +func mergeMap(dst, src map[string]any) { + for key, srcVal := range src { + if srcVal == nil { + delete(dst, key) + continue + } + srcMap, srcIsMap := srcVal.(map[string]any) + dstMap, dstIsMap := dst[key].(map[string]any) + if srcIsMap && dstIsMap { + mergeMap(dstMap, srcMap) + } else { + dst[key] = srcVal + } + } +} diff --git a/web/backend/api/events.go b/web/backend/api/events.go new file mode 100644 index 000000000..0a8d4a9bb --- /dev/null +++ b/web/backend/api/events.go @@ -0,0 +1,62 @@ +package api + +import ( + "encoding/json" + "sync" +) + +// GatewayEvent represents a state change event for the gateway process. +type GatewayEvent struct { + Status string `json:"gateway_status"` // "running", "starting", "stopped", "error" + PID int `json:"pid,omitempty"` +} + +// EventBroadcaster manages SSE client subscriptions and broadcasts events. +type EventBroadcaster struct { + mu sync.RWMutex + clients map[chan string]struct{} +} + +// NewEventBroadcaster creates a new broadcaster. +func NewEventBroadcaster() *EventBroadcaster { + return &EventBroadcaster{ + clients: make(map[chan string]struct{}), + } +} + +// Subscribe adds a new listener channel and returns it. +// The caller must call Unsubscribe when done. +func (b *EventBroadcaster) Subscribe() chan string { + ch := make(chan string, 8) + b.mu.Lock() + b.clients[ch] = struct{}{} + b.mu.Unlock() + return ch +} + +// Unsubscribe removes a listener channel and closes it. +func (b *EventBroadcaster) Unsubscribe(ch chan string) { + b.mu.Lock() + delete(b.clients, ch) + b.mu.Unlock() + close(ch) +} + +// Broadcast sends a GatewayEvent to all connected SSE clients. +func (b *EventBroadcaster) Broadcast(event GatewayEvent) { + data, err := json.Marshal(event) + if err != nil { + return + } + + b.mu.RLock() + defer b.mu.RUnlock() + + for ch := range b.clients { + // Non-blocking send; drop event if client is slow + select { + case ch <- string(data): + default: + } + } +} diff --git a/web/backend/api/gateway.go b/web/backend/api/gateway.go new file mode 100644 index 000000000..1aea1c801 --- /dev/null +++ b/web/backend/api/gateway.go @@ -0,0 +1,555 @@ +package api + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "log" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/sipeed/picoclaw/pkg/config" +) + +// gateway holds the state for the managed gateway process. +var gateway = struct { + mu sync.Mutex + cmd *exec.Cmd + logs *LogBuffer + events *EventBroadcaster +}{ + logs: NewLogBuffer(200), + events: NewEventBroadcaster(), +} + +// registerGatewayRoutes binds gateway lifecycle endpoints to the ServeMux. +func (h *Handler) registerGatewayRoutes(mux *http.ServeMux) { + mux.HandleFunc("GET /api/gateway/status", h.handleGatewayStatus) + mux.HandleFunc("GET /api/gateway/events", h.handleGatewayEvents) + mux.HandleFunc("POST /api/gateway/start", h.handleGatewayStart) + mux.HandleFunc("POST /api/gateway/stop", h.handleGatewayStop) + mux.HandleFunc("POST /api/gateway/restart", h.handleGatewayRestart) +} + +// TryAutoStartGateway checks whether gateway start preconditions are met and +// starts it when possible. Intended to be called by the backend at startup. +func (h *Handler) TryAutoStartGateway() { + gateway.mu.Lock() + defer gateway.mu.Unlock() + + if isGatewayProcessAliveLocked() { + return + } + if gateway.cmd != nil && gateway.cmd.Process != nil { + gateway.cmd = nil + } + + ready, reason, err := h.gatewayStartReady() + if err != nil { + log.Printf("Skip auto-starting gateway: %v", err) + return + } + if !ready { + log.Printf("Skip auto-starting gateway: %s", reason) + return + } + + pid, err := h.startGatewayLocked() + if err != nil { + log.Printf("Failed to auto-start gateway: %v", err) + return + } + log.Printf("Gateway auto-started (PID: %d)", pid) +} + +// gatewayStartReady validates whether current config can start the gateway. +func (h *Handler) gatewayStartReady() (bool, string, error) { + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + return false, "", fmt.Errorf("failed to load config: %w", err) + } + + modelName := strings.TrimSpace(cfg.Agents.Defaults.GetModelName()) + if modelName == "" { + return false, "no default model configured", nil + } + + modelCfg := lookupModelConfig(cfg, modelName) + if modelCfg == nil { + return false, fmt.Sprintf("default model %q is invalid", modelName), nil + } + + hasCredential := strings.TrimSpace(modelCfg.APIKey) != "" || + strings.TrimSpace(modelCfg.AuthMethod) != "" + if !hasCredential { + return false, fmt.Sprintf("default model %q has no credentials configured", modelName), nil + } + + return true, "", nil +} + +func lookupModelConfig(cfg *config.Config, modelName string) *config.ModelConfig { + modelCfg, err := cfg.GetModelConfig(modelName) + if err != nil { + return nil + } + return modelCfg +} + +func isGatewayProcessAliveLocked() bool { + return isCmdProcessAliveLocked(gateway.cmd) +} + +func isCmdProcessAliveLocked(cmd *exec.Cmd) bool { + if cmd == nil || cmd.Process == nil { + return false + } + + // Wait() sets ProcessState when the process exits; use it when available. + if cmd.ProcessState != nil && cmd.ProcessState.Exited() { + return false + } + + // Windows does not support Signal(0) probing. If we still own cmd and it + // has not reported exit, treat it as alive. + if runtime.GOOS == "windows" { + return true + } + + return cmd.Process.Signal(syscall.Signal(0)) == nil +} + +func (h *Handler) startGatewayLocked() (int, error) { + // Locate the picoclaw executable + execPath := findPicoclawBinary() + + cmd := exec.Command(execPath, "gateway") + + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + return 0, fmt.Errorf("failed to create stdout pipe: %w", err) + } + + stderrPipe, err := cmd.StderrPipe() + if err != nil { + return 0, fmt.Errorf("failed to create stderr pipe: %w", err) + } + + // Clear old logs for this new run + gateway.logs.Reset() + + // Ensure Pico Channel is configured before starting gateway + if _, err := h.ensurePicoChannel(); err != nil { + log.Printf("Warning: failed to ensure pico channel: %v", err) + // Non-fatal: gateway can still start without pico channel + } + + if err := cmd.Start(); err != nil { + return 0, fmt.Errorf("failed to start gateway: %w", err) + } + + gateway.cmd = cmd + pid := cmd.Process.Pid + log.Printf("Started picoclaw gateway (PID: %d) from %s", pid, execPath) + + // Broadcast starting event + gateway.events.Broadcast(GatewayEvent{Status: "starting", PID: pid}) + + // Capture stdout/stderr in background + go scanPipe(stdoutPipe, gateway.logs) + go scanPipe(stderrPipe, gateway.logs) + + // Wait for exit in background and clean up + go func() { + if err := cmd.Wait(); err != nil { + log.Printf("Gateway process exited: %v", err) + } else { + log.Printf("Gateway process exited normally") + } + + gateway.mu.Lock() + if gateway.cmd == cmd { + gateway.cmd = nil + } + gateway.mu.Unlock() + + // Broadcast stopped event + gateway.events.Broadcast(GatewayEvent{Status: "stopped"}) + }() + + // Start a goroutine to probe health and broadcast "running" once ready + go func() { + for i := 0; i < 30; i++ { // try for up to 15 seconds + time.Sleep(500 * time.Millisecond) + gateway.mu.Lock() + stillOurs := gateway.cmd == cmd + gateway.mu.Unlock() + if !stillOurs { + return + } + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + continue + } + healthHost := "127.0.0.1" + if cfg.Gateway.Host != "" && cfg.Gateway.Host != "0.0.0.0" { + healthHost = cfg.Gateway.Host + } + healthPort := cfg.Gateway.Port + if healthPort == 0 { + healthPort = 18790 + } + healthURL := fmt.Sprintf("http://%s/health", net.JoinHostPort(healthHost, strconv.Itoa(healthPort))) + client := http.Client{Timeout: 1 * time.Second} + resp, err := client.Get(healthURL) + if err == nil { + resp.Body.Close() + if resp.StatusCode == http.StatusOK { + gateway.events.Broadcast(GatewayEvent{Status: "running", PID: pid}) + return + } + } + } + }() + + return pid, nil +} + +// handleGatewayStart starts the picoclaw gateway subprocess. +// +// POST /api/gateway/start +func (h *Handler) handleGatewayStart(w http.ResponseWriter, r *http.Request) { + gateway.mu.Lock() + defer gateway.mu.Unlock() + + // Prevent duplicate starts + if isGatewayProcessAliveLocked() { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusConflict) + json.NewEncoder(w).Encode(map[string]any{ + "status": "already_running", + "pid": gateway.cmd.Process.Pid, + }) + return + } + if gateway.cmd != nil && gateway.cmd.Process != nil { + gateway.cmd = nil + } + + ready, reason, err := h.gatewayStartReady() + if err != nil { + http.Error( + w, + fmt.Sprintf("Failed to validate gateway start conditions: %v", err), + http.StatusInternalServerError, + ) + return + } + if !ready { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]any{ + "status": "precondition_failed", + "message": reason, + }) + return + } + + pid, err := h.startGatewayLocked() + if err != nil { + http.Error(w, fmt.Sprintf("Failed to start gateway: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{ + "status": "ok", + "pid": pid, + }) +} + +// handleGatewayStop stops the running gateway subprocess gracefully. +// +// POST /api/gateway/stop +func (h *Handler) handleGatewayStop(w http.ResponseWriter, r *http.Request) { + gateway.mu.Lock() + defer gateway.mu.Unlock() + + if gateway.cmd == nil || gateway.cmd.Process == nil { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{ + "status": "not_running", + }) + return + } + + pid := gateway.cmd.Process.Pid + + // Send SIGTERM for graceful shutdown (SIGKILL on Windows) + var sigErr error + if runtime.GOOS == "windows" { + sigErr = gateway.cmd.Process.Kill() + } else { + sigErr = gateway.cmd.Process.Signal(syscall.SIGTERM) + } + + if sigErr != nil { + http.Error(w, fmt.Sprintf("Failed to stop gateway (PID %d): %v", pid, sigErr), http.StatusInternalServerError) + return + } + + log.Printf("Sent stop signal to gateway (PID: %d)", pid) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{ + "status": "ok", + "pid": pid, + }) +} + +// handleGatewayRestart stops the gateway (if running) and starts a new instance. +// +// POST /api/gateway/restart +func (h *Handler) handleGatewayRestart(w http.ResponseWriter, r *http.Request) { + gateway.mu.Lock() + + // Stop existing process if running + if gateway.cmd != nil && gateway.cmd.Process != nil { + if isCmdProcessAliveLocked(gateway.cmd) { + // Process is alive, send SIGTERM + if runtime.GOOS == "windows" { + gateway.cmd.Process.Kill() + } else { + gateway.cmd.Process.Signal(syscall.SIGTERM) + } + + // Wait briefly for it to exit + gateway.mu.Unlock() + time.Sleep(2 * time.Second) + gateway.mu.Lock() + } + gateway.cmd = nil + } + + gateway.mu.Unlock() + + // Start fresh via the existing handler + h.handleGatewayStart(w, r) +} + +// handleGatewayStatus returns the gateway run status, health info, and logs. +// +// GET /api/gateway/status +func (h *Handler) handleGatewayStatus(w http.ResponseWriter, r *http.Request) { + data := map[string]any{} + + // Check process state + gateway.mu.Lock() + processAlive := isGatewayProcessAliveLocked() + if processAlive { + data["pid"] = gateway.cmd.Process.Pid + } + gateway.mu.Unlock() + + if !processAlive { + data["gateway_status"] = "stopped" + } else { + // Process is alive — probe its health endpoint + cfg, err := config.LoadConfig(h.configPath) + host := "127.0.0.1" + port := 18790 + if err == nil && cfg != nil { + if cfg.Gateway.Host != "" && cfg.Gateway.Host != "0.0.0.0" { + host = cfg.Gateway.Host + } + if cfg.Gateway.Port != 0 { + port = cfg.Gateway.Port + } + } + + url := fmt.Sprintf("http://%s/health", net.JoinHostPort(host, strconv.Itoa(port))) + client := http.Client{Timeout: 2 * time.Second} + resp, err := client.Get(url) + + if err != nil { + data["gateway_status"] = "starting" + } else { + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + data["gateway_status"] = "error" + data["status_code"] = resp.StatusCode + } else { + var healthData map[string]any + if decErr := json.NewDecoder(resp.Body).Decode(&healthData); decErr != nil { + data["gateway_status"] = "error" + } else { + for k, v := range healthData { + data[k] = v + } + data["gateway_status"] = "running" + } + } + } + } + + ready, reason, readyErr := h.gatewayStartReady() + if readyErr != nil { + data["gateway_start_allowed"] = false + data["gateway_start_reason"] = readyErr.Error() + } else { + data["gateway_start_allowed"] = ready + if !ready { + data["gateway_start_reason"] = reason + } + } + + // Append incremental log data + appendGatewayLogs(r, data) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(data) +} + +// appendGatewayLogs reads log_offset and log_run_id query params from the request +// and populates the response data map with incremental log lines. +func appendGatewayLogs(r *http.Request, data map[string]any) { + clientOffset := 0 + clientRunID := -1 + + if v := r.URL.Query().Get("log_offset"); v != "" { + if n, err := strconv.Atoi(v); err == nil { + clientOffset = n + } + } + + if v := r.URL.Query().Get("log_run_id"); v != "" { + if n, err := strconv.Atoi(v); err == nil { + clientRunID = n + } + } + + runID := gateway.logs.RunID() + + if runID == 0 { + data["logs"] = []string{} + data["log_total"] = 0 + data["log_run_id"] = 0 + return + } + + // If runID changed, reset offset to get all logs from new run + offset := clientOffset + if clientRunID != runID { + offset = 0 + } + + lines, total, runID := gateway.logs.LinesSince(offset) + if lines == nil { + lines = []string{} + } + + data["logs"] = lines + data["log_total"] = total + data["log_run_id"] = runID +} + +// handleGatewayEvents serves an SSE stream of gateway state change events. +// +// GET /api/gateway/events +func (h *Handler) handleGatewayEvents(w http.ResponseWriter, r *http.Request) { + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "SSE not supported", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("Access-Control-Allow-Origin", "*") + + // Subscribe to gateway events + ch := gateway.events.Subscribe() + defer gateway.events.Unsubscribe(ch) + + // Send initial status so the client doesn't start blank + initial := h.currentGatewayStatus() + fmt.Fprintf(w, "data: %s\n\n", initial) + flusher.Flush() + + for { + select { + case <-r.Context().Done(): + return + case data, ok := <-ch: + if !ok { + return + } + fmt.Fprintf(w, "data: %s\n\n", data) + flusher.Flush() + } + } +} + +// currentGatewayStatus returns the current gateway status as a JSON string. +func (h *Handler) currentGatewayStatus() string { + gateway.mu.Lock() + defer gateway.mu.Unlock() + + data := map[string]any{ + "gateway_status": "stopped", + } + if isGatewayProcessAliveLocked() { + data["gateway_status"] = "running" + data["pid"] = gateway.cmd.Process.Pid + } + + ready, reason, readyErr := h.gatewayStartReady() + if readyErr != nil { + data["gateway_start_allowed"] = false + data["gateway_start_reason"] = readyErr.Error() + } else { + data["gateway_start_allowed"] = ready + if !ready { + data["gateway_start_reason"] = reason + } + } + + encoded, _ := json.Marshal(data) + return string(encoded) +} + +// findPicoclawBinary locates the picoclaw executable. +// Tries the same directory as the current executable first, then falls back to $PATH. +func findPicoclawBinary() string { + if exe, err := os.Executable(); err == nil { + dir := filepath.Dir(exe) + candidate := filepath.Join(dir, "picoclaw") + if runtime.GOOS == "windows" { + candidate += ".exe" + } + if info, err := os.Stat(candidate); err == nil && !info.IsDir() { + return candidate + } + } + return "picoclaw" +} + +// scanPipe reads lines from r and appends them to buf. Returns when r reaches EOF. +func scanPipe(r io.Reader, buf *LogBuffer) { + scanner := bufio.NewScanner(r) + scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) + for scanner.Scan() { + buf.Append(scanner.Text()) + } +} diff --git a/web/backend/api/gateway_test.go b/web/backend/api/gateway_test.go new file mode 100644 index 000000000..336bb6a0c --- /dev/null +++ b/web/backend/api/gateway_test.go @@ -0,0 +1,122 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "path/filepath" + "strings" + "testing" + + "github.com/sipeed/picoclaw/pkg/config" +) + +func TestGatewayStartReady_NoDefaultModel(t *testing.T) { + configPath := filepath.Join(t.TempDir(), "config.json") + h := NewHandler(configPath) + + ready, reason, err := h.gatewayStartReady() + if err != nil { + t.Fatalf("gatewayStartReady() error = %v", err) + } + if ready { + t.Fatalf("gatewayStartReady() ready = true, want false") + } + if reason != "no default model configured" { + t.Fatalf("gatewayStartReady() reason = %q, want %q", reason, "no default model configured") + } +} + +func TestGatewayStartReady_InvalidDefaultModel(t *testing.T) { + configPath := filepath.Join(t.TempDir(), "config.json") + cfg := config.DefaultConfig() + cfg.Agents.Defaults.Model = "missing-model" + if err := config.SaveConfig(configPath, cfg); err != nil { + t.Fatalf("SaveConfig() error = %v", err) + } + + h := NewHandler(configPath) + ready, reason, err := h.gatewayStartReady() + if err != nil { + t.Fatalf("gatewayStartReady() error = %v", err) + } + if ready { + t.Fatalf("gatewayStartReady() ready = true, want false") + } + if reason == "" { + t.Fatalf("gatewayStartReady() reason is empty") + } +} + +func TestGatewayStartReady_ValidDefaultModel(t *testing.T) { + configPath := filepath.Join(t.TempDir(), "config.json") + cfg := config.DefaultConfig() + cfg.Agents.Defaults.ModelName = cfg.ModelList[0].ModelName + cfg.ModelList[0].APIKey = "test-key" + if err := config.SaveConfig(configPath, cfg); err != nil { + t.Fatalf("SaveConfig() error = %v", err) + } + + h := NewHandler(configPath) + ready, reason, err := h.gatewayStartReady() + if err != nil { + t.Fatalf("gatewayStartReady() error = %v", err) + } + if !ready { + t.Fatalf("gatewayStartReady() ready = false, want true (reason=%q)", reason) + } +} + +func TestGatewayStartReady_DefaultModelWithoutCredential(t *testing.T) { + configPath := filepath.Join(t.TempDir(), "config.json") + cfg := config.DefaultConfig() + cfg.Agents.Defaults.ModelName = cfg.ModelList[0].ModelName + cfg.ModelList[0].APIKey = "" + cfg.ModelList[0].AuthMethod = "" + if err := config.SaveConfig(configPath, cfg); err != nil { + t.Fatalf("SaveConfig() error = %v", err) + } + + h := NewHandler(configPath) + ready, reason, err := h.gatewayStartReady() + if err != nil { + t.Fatalf("gatewayStartReady() error = %v", err) + } + if ready { + t.Fatalf("gatewayStartReady() ready = true, want false") + } + if !strings.Contains(reason, "no credentials configured") { + t.Fatalf("gatewayStartReady() reason = %q, want contains %q", reason, "no credentials configured") + } +} + +func TestGatewayStatusIncludesStartConditionWhenNotReady(t *testing.T) { + configPath := filepath.Join(t.TempDir(), "config.json") + h := NewHandler(configPath) + mux := http.NewServeMux() + h.RegisterRoutes(mux) + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/gateway/status", nil) + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) + } + + var body map[string]any + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("unmarshal response: %v", err) + } + + allowed, ok := body["gateway_start_allowed"].(bool) + if !ok { + t.Fatalf("gateway_start_allowed missing or not bool: %#v", body["gateway_start_allowed"]) + } + if allowed { + t.Fatalf("gateway_start_allowed = true, want false") + } + if _, ok := body["gateway_start_reason"].(string); !ok { + t.Fatalf("gateway_start_reason missing or not string: %#v", body["gateway_start_reason"]) + } +} diff --git a/web/backend/api/launcher_config.go b/web/backend/api/launcher_config.go new file mode 100644 index 000000000..e149d5671 --- /dev/null +++ b/web/backend/api/launcher_config.go @@ -0,0 +1,85 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/sipeed/picoclaw/web/backend/launcherconfig" +) + +type launcherConfigPayload struct { + Port int `json:"port"` + Public bool `json:"public"` + AllowedCIDRs []string `json:"allowed_cidrs"` +} + +func (h *Handler) registerLauncherConfigRoutes(mux *http.ServeMux) { + mux.HandleFunc("GET /api/system/launcher-config", h.handleGetLauncherConfig) + mux.HandleFunc("PUT /api/system/launcher-config", h.handleUpdateLauncherConfig) +} + +func (h *Handler) launcherConfigPath() string { + return launcherconfig.PathForAppConfig(h.configPath) +} + +func (h *Handler) launcherFallbackConfig() launcherconfig.Config { + port := h.serverPort + if port <= 0 { + port = launcherconfig.DefaultPort + } + return launcherconfig.Config{ + Port: port, + Public: h.serverPublic, + AllowedCIDRs: append([]string(nil), h.serverCIDRs...), + } +} + +func (h *Handler) loadLauncherConfig() (launcherconfig.Config, error) { + return launcherconfig.Load(h.launcherConfigPath(), h.launcherFallbackConfig()) +} + +func (h *Handler) handleGetLauncherConfig(w http.ResponseWriter, r *http.Request) { + cfg, err := h.loadLauncherConfig() + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load launcher config: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(launcherConfigPayload{ + Port: cfg.Port, + Public: cfg.Public, + AllowedCIDRs: append([]string(nil), cfg.AllowedCIDRs...), + }) +} + +func (h *Handler) handleUpdateLauncherConfig(w http.ResponseWriter, r *http.Request) { + var payload launcherConfigPayload + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) + return + } + + cfg := launcherconfig.Config{ + Port: payload.Port, + Public: payload.Public, + AllowedCIDRs: append([]string(nil), payload.AllowedCIDRs...), + } + if err := launcherconfig.Validate(cfg); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if err := launcherconfig.Save(h.launcherConfigPath(), cfg); err != nil { + http.Error(w, fmt.Sprintf("Failed to save launcher config: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(launcherConfigPayload{ + Port: cfg.Port, + Public: cfg.Public, + AllowedCIDRs: append([]string(nil), cfg.AllowedCIDRs...), + }) +} diff --git a/web/backend/api/launcher_config_test.go b/web/backend/api/launcher_config_test.go new file mode 100644 index 000000000..5049dd88f --- /dev/null +++ b/web/backend/api/launcher_config_test.go @@ -0,0 +1,115 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "path/filepath" + "strings" + "testing" + + "github.com/sipeed/picoclaw/web/backend/launcherconfig" +) + +func TestGetLauncherConfigUsesRuntimeFallback(t *testing.T) { + configPath := filepath.Join(t.TempDir(), "config.json") + h := NewHandler(configPath) + h.SetServerOptions(19999, true, []string{"192.168.1.0/24"}) + + mux := http.NewServeMux() + h.RegisterRoutes(mux) + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/system/launcher-config", nil) + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String()) + } + + var got launcherConfigPayload + if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil { + t.Fatalf("unmarshal response: %v", err) + } + if got.Port != 19999 || !got.Public { + t.Fatalf("response = %+v, want port=19999 public=true", got) + } + if len(got.AllowedCIDRs) != 1 || got.AllowedCIDRs[0] != "192.168.1.0/24" { + t.Fatalf("response allowed_cidrs = %v, want [192.168.1.0/24]", got.AllowedCIDRs) + } +} + +func TestPutLauncherConfigPersists(t *testing.T) { + configPath := filepath.Join(t.TempDir(), "config.json") + h := NewHandler(configPath) + + mux := http.NewServeMux() + h.RegisterRoutes(mux) + + rec := httptest.NewRecorder() + req := httptest.NewRequest( + http.MethodPut, + "/api/system/launcher-config", + strings.NewReader(`{"port":18080,"public":true,"allowed_cidrs":["192.168.1.0/24"]}`), + ) + req.Header.Set("Content-Type", "application/json") + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String()) + } + + path := launcherconfig.PathForAppConfig(configPath) + cfg, err := launcherconfig.Load(path, launcherconfig.Default()) + if err != nil { + t.Fatalf("launcherconfig.Load() error = %v", err) + } + if cfg.Port != 18080 || !cfg.Public { + t.Fatalf("saved config = %+v, want port=18080 public=true", cfg) + } + if len(cfg.AllowedCIDRs) != 1 || cfg.AllowedCIDRs[0] != "192.168.1.0/24" { + t.Fatalf("saved config allowed_cidrs = %v, want [192.168.1.0/24]", cfg.AllowedCIDRs) + } +} + +func TestPutLauncherConfigRejectsInvalidPort(t *testing.T) { + configPath := filepath.Join(t.TempDir(), "config.json") + h := NewHandler(configPath) + + mux := http.NewServeMux() + h.RegisterRoutes(mux) + + rec := httptest.NewRecorder() + req := httptest.NewRequest( + http.MethodPut, + "/api/system/launcher-config", + strings.NewReader(`{"port":70000,"public":false}`), + ) + req.Header.Set("Content-Type", "application/json") + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusBadRequest { + t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusBadRequest, rec.Body.String()) + } +} + +func TestPutLauncherConfigRejectsInvalidCIDR(t *testing.T) { + configPath := filepath.Join(t.TempDir(), "config.json") + h := NewHandler(configPath) + + mux := http.NewServeMux() + h.RegisterRoutes(mux) + + rec := httptest.NewRecorder() + req := httptest.NewRequest( + http.MethodPut, + "/api/system/launcher-config", + strings.NewReader(`{"port":18080,"public":false,"allowed_cidrs":["bad-cidr"]}`), + ) + req.Header.Set("Content-Type", "application/json") + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusBadRequest { + t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusBadRequest, rec.Body.String()) + } +} diff --git a/cmd/picoclaw-launcher/internal/server/logbuffer.go b/web/backend/api/log.go similarity index 92% rename from cmd/picoclaw-launcher/internal/server/logbuffer.go rename to web/backend/api/log.go index 4d70f6466..ecf7d422f 100644 --- a/cmd/picoclaw-launcher/internal/server/logbuffer.go +++ b/web/backend/api/log.go @@ -1,4 +1,4 @@ -package server +package api import "sync" @@ -89,11 +89,3 @@ func (b *LogBuffer) RunID() int { return b.runID } - -// Total returns the total number of lines appended in the current run. -func (b *LogBuffer) Total() int { - b.mu.RLock() - defer b.mu.RUnlock() - - return b.total -} diff --git a/web/backend/api/models.go b/web/backend/api/models.go new file mode 100644 index 000000000..cb57d6f2e --- /dev/null +++ b/web/backend/api/models.go @@ -0,0 +1,298 @@ +package api + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + + "github.com/sipeed/picoclaw/pkg/config" +) + +// registerModelRoutes binds model list management endpoints to the ServeMux. +func (h *Handler) registerModelRoutes(mux *http.ServeMux) { + mux.HandleFunc("GET /api/models", h.handleListModels) + mux.HandleFunc("POST /api/models", h.handleAddModel) + mux.HandleFunc("POST /api/models/default", h.handleSetDefaultModel) + mux.HandleFunc("PUT /api/models/{index}", h.handleUpdateModel) + mux.HandleFunc("DELETE /api/models/{index}", h.handleDeleteModel) +} + +// modelResponse is the JSON structure returned for each model in the list. +// All ModelConfig fields are included so the frontend can display and edit them. +type modelResponse struct { + Index int `json:"index"` + ModelName string `json:"model_name"` + Model string `json:"model"` + APIBase string `json:"api_base,omitempty"` + APIKey string `json:"api_key"` + Proxy string `json:"proxy,omitempty"` + AuthMethod string `json:"auth_method,omitempty"` + // Advanced fields + ConnectMode string `json:"connect_mode,omitempty"` + Workspace string `json:"workspace,omitempty"` + RPM int `json:"rpm,omitempty"` + MaxTokensField string `json:"max_tokens_field,omitempty"` + RequestTimeout int `json:"request_timeout,omitempty"` + ThinkingLevel string `json:"thinking_level,omitempty"` + // Meta + Configured bool `json:"configured"` + IsDefault bool `json:"is_default"` +} + +// handleListModels returns all model_list entries with masked API keys. +// +// GET /api/models +func (h *Handler) handleListModels(w http.ResponseWriter, r *http.Request) { + cfg, err := h.loadFilteredConfig() + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError) + return + } + + defaultModel := cfg.Agents.Defaults.GetModelName() + + models := make([]modelResponse, 0, len(cfg.ModelList)) + for i, m := range cfg.ModelList { + models = append(models, modelResponse{ + Index: i, + ModelName: m.ModelName, + Model: m.Model, + APIBase: m.APIBase, + APIKey: maskAPIKey(m.APIKey), + Proxy: m.Proxy, + AuthMethod: m.AuthMethod, + ConnectMode: m.ConnectMode, + Workspace: m.Workspace, + RPM: m.RPM, + MaxTokensField: m.MaxTokensField, + RequestTimeout: m.RequestTimeout, + ThinkingLevel: m.ThinkingLevel, + Configured: m.APIKey != "" || m.AuthMethod != "", + IsDefault: m.ModelName == defaultModel, + }) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{ + "models": models, + "total": len(models), + "default_model": defaultModel, + }) +} + +// handleAddModel appends a new model configuration entry. +// +// POST /api/models +func (h *Handler) handleAddModel(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) + if err != nil { + http.Error(w, "Failed to read request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + var mc config.ModelConfig + if err = json.Unmarshal(body, &mc); err != nil { + http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) + return + } + + if err = mc.Validate(); err != nil { + http.Error(w, fmt.Sprintf("Validation error: %v", err), http.StatusBadRequest) + return + } + + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError) + return + } + + cfg.ModelList = append(cfg.ModelList, mc) + + if err := config.SaveConfig(h.configPath, cfg); err != nil { + http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{ + "status": "ok", + "index": len(cfg.ModelList) - 1, + }) +} + +// handleUpdateModel replaces a model configuration entry at the given index. +// If the request body omits api_key (or sends an empty string), the existing +// stored key is preserved so callers can update only api_base / proxy without +// exposing or clearing the secret. +// +// PUT /api/models/{index} +func (h *Handler) handleUpdateModel(w http.ResponseWriter, r *http.Request) { + idx, err := strconv.Atoi(r.PathValue("index")) + if err != nil { + http.Error(w, "Invalid index", http.StatusBadRequest) + return + } + + body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) + if err != nil { + http.Error(w, "Failed to read request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + var mc config.ModelConfig + if err = json.Unmarshal(body, &mc); err != nil { + http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) + return + } + + if err = mc.Validate(); err != nil { + http.Error(w, fmt.Sprintf("Validation error: %v", err), http.StatusBadRequest) + return + } + + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError) + return + } + + if idx < 0 || idx >= len(cfg.ModelList) { + http.Error(w, fmt.Sprintf("Index %d out of range (0-%d)", idx, len(cfg.ModelList)-1), http.StatusNotFound) + return + } + + // Preserve the existing API key when the caller omits it (empty string). + // This lets the UI update api_base / proxy without clearing the stored secret. + if mc.APIKey == "" { + mc.APIKey = cfg.ModelList[idx].APIKey + } + + cfg.ModelList[idx] = mc + + if err := config.SaveConfig(h.configPath, cfg); err != nil { + http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) +} + +// handleDeleteModel removes a model configuration entry at the given index. +// +// DELETE /api/models/{index} +func (h *Handler) handleDeleteModel(w http.ResponseWriter, r *http.Request) { + idx, err := strconv.Atoi(r.PathValue("index")) + if err != nil { + http.Error(w, "Invalid index", http.StatusBadRequest) + return + } + + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError) + return + } + + if idx < 0 || idx >= len(cfg.ModelList) { + http.Error(w, fmt.Sprintf("Index %d out of range (0-%d)", idx, len(cfg.ModelList)-1), http.StatusNotFound) + return + } + + deletedModelName := cfg.ModelList[idx].ModelName + + cfg.ModelList = append(cfg.ModelList[:idx], cfg.ModelList[idx+1:]...) + + // If the deleted model was the default, clear it. + if cfg.Agents.Defaults.ModelName == deletedModelName { + cfg.Agents.Defaults.ModelName = "" + } + if cfg.Agents.Defaults.Model == deletedModelName { + cfg.Agents.Defaults.Model = "" + } + + if err := config.SaveConfig(h.configPath, cfg); err != nil { + http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) +} + +// handleSetDefaultModel sets the default model for all agents. +// +// POST /api/models/default +func (h *Handler) handleSetDefaultModel(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) + if err != nil { + http.Error(w, "Failed to read request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + var req struct { + ModelName string `json:"model_name"` + } + if err = json.Unmarshal(body, &req); err != nil { + http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) + return + } + + if req.ModelName == "" { + http.Error(w, "model_name is required", http.StatusBadRequest) + return + } + + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError) + return + } + + // Verify the model_name exists in model_list + found := false + for _, m := range cfg.ModelList { + if m.ModelName == req.ModelName { + found = true + break + } + } + if !found { + http.Error(w, fmt.Sprintf("Model %q not found in model_list", req.ModelName), http.StatusNotFound) + return + } + + cfg.Agents.Defaults.ModelName = req.ModelName + + if err := config.SaveConfig(h.configPath, cfg); err != nil { + http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{ + "status": "ok", + "default_model": req.ModelName, + }) +} + +// maskAPIKey returns a masked version of an API key for safe display. +// Keys longer than 8 chars show prefix + last 4 chars: "sk-****abcd" +// Shorter keys are fully masked as "****". +// Empty keys return empty string. +func maskAPIKey(key string) string { + if key == "" { + return "" + } + if len(key) <= 8 { + return "****" + } + // Show first 3 chars and last 4 chars + return key[:3] + "****" + key[len(key)-4:] +} diff --git a/web/backend/api/oauth.go b/web/backend/api/oauth.go new file mode 100644 index 000000000..04cd595f2 --- /dev/null +++ b/web/backend/api/oauth.go @@ -0,0 +1,844 @@ +package api + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "html" + "io" + "log" + "net/http" + "strings" + "time" + + "github.com/sipeed/picoclaw/pkg/auth" + "github.com/sipeed/picoclaw/pkg/config" + "github.com/sipeed/picoclaw/pkg/providers" +) + +const ( + oauthProviderOpenAI = "openai" + oauthProviderAnthropic = "anthropic" + oauthProviderGoogleAntigravity = "google-antigravity" + + oauthMethodBrowser = "browser" + oauthMethodDeviceCode = "device_code" + oauthMethodToken = "token" + + oauthFlowPending = "pending" + oauthFlowSuccess = "success" + oauthFlowError = "error" + oauthFlowExpired = "expired" +) + +const ( + oauthBrowserFlowTTL = 10 * time.Minute + oauthDeviceCodeFlowTTL = 15 * time.Minute + oauthTerminalFlowGC = 30 * time.Minute +) + +var oauthProviderOrder = []string{ + oauthProviderOpenAI, + oauthProviderAnthropic, + oauthProviderGoogleAntigravity, +} + +var oauthProviderMethods = map[string][]string{ + oauthProviderOpenAI: {oauthMethodBrowser, oauthMethodDeviceCode, oauthMethodToken}, + oauthProviderAnthropic: {oauthMethodToken}, + oauthProviderGoogleAntigravity: {oauthMethodBrowser}, +} + +var oauthProviderLabels = map[string]string{ + oauthProviderOpenAI: "OpenAI", + oauthProviderAnthropic: "Anthropic", + oauthProviderGoogleAntigravity: "Google Antigravity", +} + +var ( + oauthNow = time.Now + oauthGeneratePKCE = auth.GeneratePKCE + oauthGenerateState = auth.GenerateState + oauthBuildAuthorizeURL = auth.BuildAuthorizeURL + oauthRequestDeviceCode = auth.RequestDeviceCode + oauthPollDeviceCodeOnce = auth.PollDeviceCodeOnce + oauthExchangeCodeForTokens = auth.ExchangeCodeForTokens + oauthGetCredential = auth.GetCredential + oauthSetCredential = auth.SetCredential + oauthDeleteCredential = auth.DeleteCredential + oauthLoadConfig = config.LoadConfig + oauthSaveConfig = config.SaveConfig + oauthFetchAntigravityProject = providers.FetchAntigravityProjectID + oauthFetchGoogleUserEmailFunc = fetchGoogleUserEmail +) + +type oauthFlow struct { + ID string + Provider string + Method string + Status string + CreatedAt time.Time + UpdatedAt time.Time + ExpiresAt time.Time + Error string + CodeVerifier string + OAuthState string + RedirectURI string + DeviceAuthID string + UserCode string + VerifyURL string + Interval int +} + +type oauthProviderStatus struct { + Provider string `json:"provider"` + DisplayName string `json:"display_name"` + Methods []string `json:"methods"` + LoggedIn bool `json:"logged_in"` + Status string `json:"status"` + AuthMethod string `json:"auth_method,omitempty"` + ExpiresAt string `json:"expires_at,omitempty"` + AccountID string `json:"account_id,omitempty"` + Email string `json:"email,omitempty"` + ProjectID string `json:"project_id,omitempty"` +} + +type oauthFlowResponse struct { + FlowID string `json:"flow_id"` + Provider string `json:"provider"` + Method string `json:"method"` + Status string `json:"status"` + ExpiresAt string `json:"expires_at,omitempty"` + Error string `json:"error,omitempty"` + UserCode string `json:"user_code,omitempty"` + VerifyURL string `json:"verify_url,omitempty"` + Interval int `json:"interval,omitempty"` +} + +// registerOAuthRoutes binds OAuth login/logout endpoints to the ServeMux. +func (h *Handler) registerOAuthRoutes(mux *http.ServeMux) { + mux.HandleFunc("GET /api/oauth/providers", h.handleListOAuthProviders) + mux.HandleFunc("POST /api/oauth/login", h.handleOAuthLogin) + mux.HandleFunc("GET /api/oauth/flows/{id}", h.handleGetOAuthFlow) + mux.HandleFunc("POST /api/oauth/flows/{id}/poll", h.handlePollOAuthFlow) + mux.HandleFunc("POST /api/oauth/logout", h.handleOAuthLogout) + mux.HandleFunc("GET /oauth/callback", h.handleOAuthCallback) +} + +func (h *Handler) handleListOAuthProviders(w http.ResponseWriter, r *http.Request) { + providersResp := make([]oauthProviderStatus, 0, len(oauthProviderOrder)) + + for _, provider := range oauthProviderOrder { + cred, err := oauthGetCredential(provider) + if err != nil { + http.Error(w, fmt.Sprintf("failed to load credentials: %v", err), http.StatusInternalServerError) + return + } + + item := oauthProviderStatus{ + Provider: provider, + DisplayName: oauthProviderLabels[provider], + Methods: oauthProviderMethods[provider], + Status: "not_logged_in", + } + if cred != nil { + item.LoggedIn = true + item.AuthMethod = cred.AuthMethod + item.AccountID = cred.AccountID + item.Email = cred.Email + item.ProjectID = cred.ProjectID + if !cred.ExpiresAt.IsZero() { + item.ExpiresAt = cred.ExpiresAt.Format(time.RFC3339) + } + switch { + case cred.IsExpired(): + item.Status = "expired" + case cred.NeedsRefresh(): + item.Status = "needs_refresh" + default: + item.Status = "connected" + } + } + + providersResp = append(providersResp, item) + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "providers": providersResp, + }) +} + +func (h *Handler) handleOAuthLogin(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) + if err != nil { + http.Error(w, "failed to read request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + var req struct { + Provider string `json:"provider"` + Method string `json:"method"` + Token string `json:"token"` + } + if err = json.Unmarshal(body, &req); err != nil { + http.Error(w, fmt.Sprintf("invalid JSON: %v", err), http.StatusBadRequest) + return + } + + provider, err := normalizeOAuthProvider(req.Provider) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + method := strings.ToLower(strings.TrimSpace(req.Method)) + if !isOAuthMethodSupported(provider, method) { + http.Error( + w, + fmt.Sprintf("unsupported login method %q for provider %q", method, provider), + http.StatusBadRequest, + ) + return + } + + switch method { + case oauthMethodToken: + token := strings.TrimSpace(req.Token) + if token == "" { + http.Error(w, "token is required", http.StatusBadRequest) + return + } + + cred := &auth.AuthCredential{ + AccessToken: token, + Provider: provider, + AuthMethod: oauthMethodToken, + } + if err := h.persistCredentialAndConfig(provider, oauthMethodToken, cred); err != nil { + http.Error(w, fmt.Sprintf("token login failed: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "status": "ok", + "provider": provider, + "method": method, + }) + return + + case oauthMethodDeviceCode: + cfg := auth.OpenAIOAuthConfig() + info, err := oauthRequestDeviceCode(cfg) + if err != nil { + http.Error(w, fmt.Sprintf("failed to request device code: %v", err), http.StatusInternalServerError) + return + } + + now := oauthNow() + flow := &oauthFlow{ + ID: newOAuthFlowID(), + Provider: provider, + Method: method, + Status: oauthFlowPending, + CreatedAt: now, + UpdatedAt: now, + ExpiresAt: now.Add(oauthDeviceCodeFlowTTL), + DeviceAuthID: info.DeviceAuthID, + UserCode: info.UserCode, + VerifyURL: info.VerifyURL, + Interval: info.Interval, + } + h.storeOAuthFlow(flow) + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "status": "ok", + "provider": provider, + "method": method, + "flow_id": flow.ID, + "user_code": flow.UserCode, + "verify_url": flow.VerifyURL, + "interval": flow.Interval, + "expires_at": flow.ExpiresAt.Format(time.RFC3339), + }) + return + + case oauthMethodBrowser: + cfg, err := oauthConfigForProvider(provider) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + pkce, err := oauthGeneratePKCE() + if err != nil { + http.Error(w, fmt.Sprintf("failed to generate PKCE: %v", err), http.StatusInternalServerError) + return + } + state, err := oauthGenerateState() + if err != nil { + http.Error(w, fmt.Sprintf("failed to generate state: %v", err), http.StatusInternalServerError) + return + } + + redirectURI := buildOAuthRedirectURI(r) + authURL := oauthBuildAuthorizeURL(cfg, pkce, state, redirectURI) + + now := oauthNow() + flow := &oauthFlow{ + ID: newOAuthFlowID(), + Provider: provider, + Method: method, + Status: oauthFlowPending, + CreatedAt: now, + UpdatedAt: now, + ExpiresAt: now.Add(oauthBrowserFlowTTL), + CodeVerifier: pkce.CodeVerifier, + OAuthState: state, + RedirectURI: redirectURI, + } + h.storeOAuthFlow(flow) + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "status": "ok", + "provider": provider, + "method": method, + "flow_id": flow.ID, + "auth_url": authURL, + "expires_at": flow.ExpiresAt.Format(time.RFC3339), + }) + return + default: + http.Error(w, "unsupported login method", http.StatusBadRequest) + } +} + +func (h *Handler) handleGetOAuthFlow(w http.ResponseWriter, r *http.Request) { + flowID := strings.TrimSpace(r.PathValue("id")) + if flowID == "" { + http.Error(w, "missing flow id", http.StatusBadRequest) + return + } + + flow, ok := h.getOAuthFlow(flowID) + if !ok { + http.Error(w, "flow not found", http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(flowToResponse(flow)) +} + +func (h *Handler) handlePollOAuthFlow(w http.ResponseWriter, r *http.Request) { + flowID := strings.TrimSpace(r.PathValue("id")) + if flowID == "" { + http.Error(w, "missing flow id", http.StatusBadRequest) + return + } + + flow, ok := h.getOAuthFlow(flowID) + if !ok { + http.Error(w, "flow not found", http.StatusNotFound) + return + } + + if flow.Method != oauthMethodDeviceCode { + http.Error(w, "flow does not support polling", http.StatusBadRequest) + return + } + if flow.Status != oauthFlowPending { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(flowToResponse(flow)) + return + } + + cfg := auth.OpenAIOAuthConfig() + cred, err := oauthPollDeviceCodeOnce(cfg, flow.DeviceAuthID, flow.UserCode) + if err != nil { + if strings.Contains(strings.ToLower(err.Error()), "pending") { + updated, _ := h.getOAuthFlow(flowID) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(flowToResponse(updated)) + return + } + h.setOAuthFlowError(flowID, fmt.Sprintf("device code poll failed: %v", err)) + updated, _ := h.getOAuthFlow(flowID) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(flowToResponse(updated)) + return + } + if cred == nil { + updated, _ := h.getOAuthFlow(flowID) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(flowToResponse(updated)) + return + } + + if err := h.persistCredentialAndConfig(flow.Provider, oauthMethodTokenOrOAuth(flow.Method), cred); err != nil { + h.setOAuthFlowError(flowID, fmt.Sprintf("failed to save credential: %v", err)) + updated, _ := h.getOAuthFlow(flowID) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(flowToResponse(updated)) + return + } + + h.setOAuthFlowSuccess(flowID) + updated, _ := h.getOAuthFlow(flowID) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(flowToResponse(updated)) +} + +func (h *Handler) handleOAuthCallback(w http.ResponseWriter, r *http.Request) { + state := strings.TrimSpace(r.URL.Query().Get("state")) + if state == "" { + renderOAuthCallbackPage(w, "", oauthFlowError, "Missing state", "missing_state") + return + } + + flow, ok := h.getOAuthFlowByState(state) + if !ok { + renderOAuthCallbackPage(w, "", oauthFlowError, "OAuth flow not found", "flow_not_found") + return + } + + if flow.Status != oauthFlowPending { + renderOAuthCallbackPage(w, flow.ID, flow.Status, "Flow already completed", flow.Error) + return + } + + if errMsg := strings.TrimSpace(r.URL.Query().Get("error")); errMsg != "" { + if desc := strings.TrimSpace(r.URL.Query().Get("error_description")); desc != "" { + errMsg += ": " + desc + } + h.setOAuthFlowError(flow.ID, errMsg) + renderOAuthCallbackPage(w, flow.ID, oauthFlowError, "Authorization failed", errMsg) + return + } + + code := strings.TrimSpace(r.URL.Query().Get("code")) + if code == "" { + h.setOAuthFlowError(flow.ID, "missing authorization code") + renderOAuthCallbackPage(w, flow.ID, oauthFlowError, "Missing authorization code", "missing_code") + return + } + + cfg, err := oauthConfigForProvider(flow.Provider) + if err != nil { + h.setOAuthFlowError(flow.ID, err.Error()) + renderOAuthCallbackPage(w, flow.ID, oauthFlowError, "Unsupported provider", err.Error()) + return + } + + cred, err := oauthExchangeCodeForTokens(cfg, code, flow.CodeVerifier, flow.RedirectURI) + if err != nil { + h.setOAuthFlowError(flow.ID, fmt.Sprintf("token exchange failed: %v", err)) + renderOAuthCallbackPage(w, flow.ID, oauthFlowError, "Token exchange failed", err.Error()) + return + } + + if err := h.persistCredentialAndConfig(flow.Provider, oauthMethodTokenOrOAuth(flow.Method), cred); err != nil { + h.setOAuthFlowError(flow.ID, fmt.Sprintf("failed to save credential: %v", err)) + renderOAuthCallbackPage(w, flow.ID, oauthFlowError, "Failed to save credential", err.Error()) + return + } + + h.setOAuthFlowSuccess(flow.ID) + renderOAuthCallbackPage(w, flow.ID, oauthFlowSuccess, "Authentication successful", "") +} + +func (h *Handler) handleOAuthLogout(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) + if err != nil { + http.Error(w, "failed to read request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + var req struct { + Provider string `json:"provider"` + } + if err = json.Unmarshal(body, &req); err != nil { + http.Error(w, fmt.Sprintf("invalid JSON: %v", err), http.StatusBadRequest) + return + } + + provider, err := normalizeOAuthProvider(req.Provider) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if err := oauthDeleteCredential(provider); err != nil { + http.Error(w, fmt.Sprintf("failed to delete credential: %v", err), http.StatusInternalServerError) + return + } + if err := h.syncProviderAuthMethod(provider, ""); err != nil { + http.Error(w, fmt.Sprintf("failed to update config: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "status": "ok", + "provider": provider, + }) +} + +func renderOAuthCallbackPage(w http.ResponseWriter, flowID, status, title, errMsg string) { + payload := map[string]string{ + "type": "picoclaw-oauth-result", + "flowId": flowID, + "status": status, + } + if errMsg != "" { + payload["error"] = errMsg + } + payloadJSON, _ := json.Marshal(payload) + + message := title + if errMsg != "" { + message = fmt.Sprintf("%s: %s", title, errMsg) + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if status == oauthFlowSuccess { + w.WriteHeader(http.StatusOK) + } else { + w.WriteHeader(http.StatusBadRequest) + } + + _, _ = fmt.Fprintf( + w, + "PicoClaw OAuth

%s

%s

You can close this window.

", + string(payloadJSON), + html.EscapeString(title), + html.EscapeString(message), + ) +} + +func normalizeOAuthProvider(raw string) (string, error) { + provider := strings.ToLower(strings.TrimSpace(raw)) + switch provider { + case "antigravity": + return oauthProviderGoogleAntigravity, nil + case oauthProviderOpenAI, oauthProviderAnthropic, oauthProviderGoogleAntigravity: + return provider, nil + default: + return "", fmt.Errorf("unsupported provider %q", raw) + } +} + +func isOAuthMethodSupported(provider, method string) bool { + methods := oauthProviderMethods[provider] + for _, m := range methods { + if m == method { + return true + } + } + return false +} + +func oauthConfigForProvider(provider string) (auth.OAuthProviderConfig, error) { + switch provider { + case oauthProviderOpenAI: + return auth.OpenAIOAuthConfig(), nil + case oauthProviderGoogleAntigravity: + return auth.GoogleAntigravityOAuthConfig(), nil + default: + return auth.OAuthProviderConfig{}, fmt.Errorf("provider %q does not support browser oauth", provider) + } +} + +func oauthMethodTokenOrOAuth(method string) string { + if method == oauthMethodToken { + return oauthMethodToken + } + return "oauth" +} + +func buildOAuthRedirectURI(r *http.Request) string { + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + if forwarded := strings.TrimSpace(r.Header.Get("X-Forwarded-Proto")); forwarded != "" { + scheme = strings.Split(forwarded, ",")[0] + } + return fmt.Sprintf("%s://%s/oauth/callback", scheme, r.Host) +} + +func flowToResponse(flow *oauthFlow) oauthFlowResponse { + resp := oauthFlowResponse{ + FlowID: flow.ID, + Provider: flow.Provider, + Method: flow.Method, + Status: flow.Status, + Error: flow.Error, + } + if !flow.ExpiresAt.IsZero() { + resp.ExpiresAt = flow.ExpiresAt.Format(time.RFC3339) + } + if flow.Method == oauthMethodDeviceCode { + resp.UserCode = flow.UserCode + resp.VerifyURL = flow.VerifyURL + resp.Interval = flow.Interval + } + return resp +} + +func newOAuthFlowID() string { + buf := make([]byte, 16) + if _, err := rand.Read(buf); err != nil { + return fmt.Sprintf("oauth_%d", time.Now().UnixNano()) + } + return hex.EncodeToString(buf) +} + +func (h *Handler) storeOAuthFlow(flow *oauthFlow) { + now := oauthNow() + h.oauthMu.Lock() + defer h.oauthMu.Unlock() + + h.gcOAuthFlowsLocked(now) + h.oauthFlows[flow.ID] = flow + if flow.OAuthState != "" { + h.oauthState[flow.OAuthState] = flow.ID + } +} + +func (h *Handler) getOAuthFlow(flowID string) (*oauthFlow, bool) { + now := oauthNow() + h.oauthMu.Lock() + defer h.oauthMu.Unlock() + + h.gcOAuthFlowsLocked(now) + flow, ok := h.oauthFlows[flowID] + if !ok { + return nil, false + } + cp := *flow + return &cp, true +} + +func (h *Handler) getOAuthFlowByState(state string) (*oauthFlow, bool) { + now := oauthNow() + h.oauthMu.Lock() + defer h.oauthMu.Unlock() + + h.gcOAuthFlowsLocked(now) + flowID, ok := h.oauthState[state] + if !ok { + return nil, false + } + flow, ok := h.oauthFlows[flowID] + if !ok { + delete(h.oauthState, state) + return nil, false + } + cp := *flow + return &cp, true +} + +func (h *Handler) setOAuthFlowSuccess(flowID string) { + now := oauthNow() + h.oauthMu.Lock() + defer h.oauthMu.Unlock() + + flow, ok := h.oauthFlows[flowID] + if !ok { + return + } + flow.Status = oauthFlowSuccess + flow.Error = "" + flow.UpdatedAt = now + if flow.OAuthState != "" { + delete(h.oauthState, flow.OAuthState) + } +} + +func (h *Handler) setOAuthFlowError(flowID, errMsg string) { + now := oauthNow() + h.oauthMu.Lock() + defer h.oauthMu.Unlock() + + flow, ok := h.oauthFlows[flowID] + if !ok { + return + } + flow.Status = oauthFlowError + flow.Error = errMsg + flow.UpdatedAt = now + if flow.OAuthState != "" { + delete(h.oauthState, flow.OAuthState) + } +} + +func (h *Handler) gcOAuthFlowsLocked(now time.Time) { + for id, flow := range h.oauthFlows { + if flow.Status == oauthFlowPending && !flow.ExpiresAt.IsZero() && now.After(flow.ExpiresAt) { + flow.Status = oauthFlowExpired + flow.Error = "flow expired" + flow.UpdatedAt = now + if flow.OAuthState != "" { + delete(h.oauthState, flow.OAuthState) + } + } + + if flow.Status != oauthFlowPending && now.Sub(flow.UpdatedAt) > oauthTerminalFlowGC { + if flow.OAuthState != "" { + delete(h.oauthState, flow.OAuthState) + } + delete(h.oauthFlows, id) + } + } +} + +func (h *Handler) persistCredentialAndConfig(provider, authMethod string, cred *auth.AuthCredential) error { + if cred == nil { + return fmt.Errorf("empty credential") + } + + cp := *cred + cp.Provider = provider + if cp.AuthMethod == "" { + cp.AuthMethod = authMethod + } + + if provider == oauthProviderGoogleAntigravity { + if cp.Email == "" { + email, err := oauthFetchGoogleUserEmailFunc(cp.AccessToken) + if err != nil { + log.Printf("oauth warning: could not fetch google email: %v", err) + } else { + cp.Email = email + } + } + if cp.ProjectID == "" { + projectID, err := oauthFetchAntigravityProject(cp.AccessToken) + if err != nil { + log.Printf("oauth warning: could not fetch antigravity project id: %v", err) + } else { + cp.ProjectID = projectID + } + } + } + + if err := oauthSetCredential(provider, &cp); err != nil { + return fmt.Errorf("saving credential: %w", err) + } + if err := h.syncProviderAuthMethod(provider, authMethod); err != nil { + return fmt.Errorf("syncing provider auth config: %w", err) + } + return nil +} + +func (h *Handler) syncProviderAuthMethod(provider, authMethod string) error { + cfg, err := oauthLoadConfig(h.configPath) + if err != nil { + return err + } + + switch provider { + case oauthProviderOpenAI: + cfg.Providers.OpenAI.AuthMethod = authMethod + case oauthProviderAnthropic: + cfg.Providers.Anthropic.AuthMethod = authMethod + case oauthProviderGoogleAntigravity: + cfg.Providers.Antigravity.AuthMethod = authMethod + default: + return fmt.Errorf("unsupported provider %q", provider) + } + + found := false + for i := range cfg.ModelList { + if modelBelongsToProvider(provider, cfg.ModelList[i].Model) { + cfg.ModelList[i].AuthMethod = authMethod + found = true + } + } + + if !found && authMethod != "" { + cfg.ModelList = append(cfg.ModelList, defaultModelConfigForProvider(provider, authMethod)) + } + + return oauthSaveConfig(h.configPath, cfg) +} + +func modelBelongsToProvider(provider, model string) bool { + lower := strings.ToLower(strings.TrimSpace(model)) + switch provider { + case oauthProviderOpenAI: + return lower == "openai" || strings.HasPrefix(lower, "openai/") + case oauthProviderAnthropic: + return lower == "anthropic" || strings.HasPrefix(lower, "anthropic/") + case oauthProviderGoogleAntigravity: + return lower == "antigravity" || + lower == "google-antigravity" || + strings.HasPrefix(lower, "antigravity/") || + strings.HasPrefix(lower, "google-antigravity/") + default: + return false + } +} + +func defaultModelConfigForProvider(provider, authMethod string) config.ModelConfig { + switch provider { + case oauthProviderOpenAI: + return config.ModelConfig{ + ModelName: "gpt-5.2", + Model: "openai/gpt-5.2", + AuthMethod: authMethod, + } + case oauthProviderAnthropic: + return config.ModelConfig{ + ModelName: "claude-sonnet-4.6", + Model: "anthropic/claude-sonnet-4.6", + AuthMethod: authMethod, + } + case oauthProviderGoogleAntigravity: + return config.ModelConfig{ + ModelName: "gemini-flash", + Model: "antigravity/gemini-3-flash", + AuthMethod: authMethod, + } + default: + return config.ModelConfig{} + } +} + +func fetchGoogleUserEmail(accessToken string) (string, error) { + req, err := http.NewRequest(http.MethodGet, "https://www.googleapis.com/oauth2/v2/userinfo", nil) + if err != nil { + return "", err + } + req.Header.Set("Authorization", "Bearer "+accessToken) + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("userinfo request failed: %s", string(body)) + } + + var userInfo struct { + Email string `json:"email"` + } + if err := json.Unmarshal(body, &userInfo); err != nil { + return "", err + } + if userInfo.Email == "" { + return "", fmt.Errorf("empty email in userinfo response") + } + return userInfo.Email, nil +} diff --git a/web/backend/api/oauth_test.go b/web/backend/api/oauth_test.go new file mode 100644 index 000000000..2103e1efc --- /dev/null +++ b/web/backend/api/oauth_test.go @@ -0,0 +1,293 @@ +package api + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/sipeed/picoclaw/pkg/auth" + "github.com/sipeed/picoclaw/pkg/config" +) + +func TestOAuthLoginRejectsUnsupportedMethod(t *testing.T) { + configPath, cleanup := setupOAuthTestEnv(t) + defer cleanup() + resetOAuthHooks(t) + + h := NewHandler(configPath) + mux := http.NewServeMux() + h.RegisterRoutes(mux) + + rec := httptest.NewRecorder() + req := httptest.NewRequest( + http.MethodPost, + "/api/oauth/login", + strings.NewReader(`{"provider":"anthropic","method":"browser"}`), + ) + req.Header.Set("Content-Type", "application/json") + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusBadRequest { + t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusBadRequest, rec.Body.String()) + } +} + +func TestOAuthBrowserFlowCreatedAndQueried(t *testing.T) { + configPath, cleanup := setupOAuthTestEnv(t) + defer cleanup() + resetOAuthHooks(t) + + oauthGeneratePKCE = func() (auth.PKCECodes, error) { + return auth.PKCECodes{CodeVerifier: "verifier-1", CodeChallenge: "challenge-1"}, nil + } + oauthGenerateState = func() (string, error) { return "state-1", nil } + oauthBuildAuthorizeURL = func(cfg auth.OAuthProviderConfig, pkce auth.PKCECodes, state, redirectURI string) string { + return "https://example.com/authorize?state=" + state + } + + h := NewHandler(configPath) + mux := http.NewServeMux() + h.RegisterRoutes(mux) + + rec := httptest.NewRecorder() + req := httptest.NewRequest( + http.MethodPost, + "/api/oauth/login", + strings.NewReader(`{"provider":"openai","method":"browser"}`), + ) + req.Host = "localhost:18800" + req.Header.Set("Content-Type", "application/json") + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String()) + } + + var loginResp map[string]any + if err := json.Unmarshal(rec.Body.Bytes(), &loginResp); err != nil { + t.Fatalf("unmarshal login response: %v", err) + } + flowID, _ := loginResp["flow_id"].(string) + if flowID == "" { + t.Fatalf("flow_id is empty: %v", loginResp) + } + if loginResp["auth_url"] != "https://example.com/authorize?state=state-1" { + t.Fatalf("unexpected auth_url: %v", loginResp["auth_url"]) + } + + rec2 := httptest.NewRecorder() + req2 := httptest.NewRequest(http.MethodGet, "/api/oauth/flows/"+flowID, nil) + mux.ServeHTTP(rec2, req2) + if rec2.Code != http.StatusOK { + t.Fatalf("flow status code = %d, want %d, body=%s", rec2.Code, http.StatusOK, rec2.Body.String()) + } + var flowResp oauthFlowResponse + if err := json.Unmarshal(rec2.Body.Bytes(), &flowResp); err != nil { + t.Fatalf("unmarshal flow response: %v", err) + } + if flowResp.Status != oauthFlowPending { + t.Fatalf("flow status = %q, want %q", flowResp.Status, oauthFlowPending) + } + if flowResp.Method != oauthMethodBrowser { + t.Fatalf("flow method = %q, want %q", flowResp.Method, oauthMethodBrowser) + } +} + +func TestOAuthFlowExpiresWhenQueried(t *testing.T) { + configPath, cleanup := setupOAuthTestEnv(t) + defer cleanup() + resetOAuthHooks(t) + + now := time.Date(2026, 3, 6, 12, 0, 0, 0, time.UTC) + oauthNow = func() time.Time { return now } + + h := NewHandler(configPath) + h.storeOAuthFlow(&oauthFlow{ + ID: "expired-flow", + Provider: oauthProviderOpenAI, + Method: oauthMethodBrowser, + Status: oauthFlowPending, + CreatedAt: now.Add(-20 * time.Minute), + UpdatedAt: now.Add(-20 * time.Minute), + ExpiresAt: now.Add(-1 * time.Minute), + }) + + mux := http.NewServeMux() + h.RegisterRoutes(mux) + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/oauth/flows/expired-flow", nil) + mux.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String()) + } + var flowResp oauthFlowResponse + if err := json.Unmarshal(rec.Body.Bytes(), &flowResp); err != nil { + t.Fatalf("unmarshal flow response: %v", err) + } + if flowResp.Status != oauthFlowExpired { + t.Fatalf("flow status = %q, want %q", flowResp.Status, oauthFlowExpired) + } +} + +func TestOAuthCallbackUnknownState(t *testing.T) { + configPath, cleanup := setupOAuthTestEnv(t) + defer cleanup() + resetOAuthHooks(t) + + h := NewHandler(configPath) + mux := http.NewServeMux() + h.RegisterRoutes(mux) + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/oauth/callback?state=unknown&code=abc", nil) + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusBadRequest { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest) + } + if !strings.Contains(rec.Body.String(), "OAuth flow not found") { + t.Fatalf("unexpected body: %s", rec.Body.String()) + } +} + +func TestOAuthLogoutClearsCredentialAndConfig(t *testing.T) { + configPath, cleanup := setupOAuthTestEnv(t) + defer cleanup() + resetOAuthHooks(t) + + cfg, err := config.LoadConfig(configPath) + if err != nil { + t.Fatalf("LoadConfig error: %v", err) + } + cfg.Providers.OpenAI.AuthMethod = "oauth" + cfg.ModelList = append(cfg.ModelList, config.ModelConfig{ + ModelName: "gpt-5.2", + Model: "openai/gpt-5.2", + AuthMethod: "oauth", + }) + if err = config.SaveConfig(configPath, cfg); err != nil { + t.Fatalf("SaveConfig error: %v", err) + } + if err = auth.SetCredential(oauthProviderOpenAI, &auth.AuthCredential{ + AccessToken: "token-before-logout", + Provider: oauthProviderOpenAI, + AuthMethod: "oauth", + }); err != nil { + t.Fatalf("SetCredential error: %v", err) + } + + h := NewHandler(configPath) + mux := http.NewServeMux() + h.RegisterRoutes(mux) + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/api/oauth/logout", bytes.NewBufferString(`{"provider":"openai"}`)) + req.Header.Set("Content-Type", "application/json") + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String()) + } + + cred, err := auth.GetCredential(oauthProviderOpenAI) + if err != nil { + t.Fatalf("GetCredential error: %v", err) + } + if cred != nil { + t.Fatalf("expected credential deleted, got %#v", cred) + } + + updated, err := config.LoadConfig(configPath) + if err != nil { + t.Fatalf("LoadConfig error: %v", err) + } + if updated.Providers.OpenAI.AuthMethod != "" { + t.Fatalf("providers.openai.auth_method = %q, want empty", updated.Providers.OpenAI.AuthMethod) + } + for _, m := range updated.ModelList { + if strings.HasPrefix(m.Model, "openai/") && m.AuthMethod != "" { + t.Fatalf("openai model auth_method = %q, want empty", m.AuthMethod) + } + } +} + +func setupOAuthTestEnv(t *testing.T) (string, func()) { + t.Helper() + + tmp := t.TempDir() + oldHome := os.Getenv("HOME") + oldPicoHome := os.Getenv("PICOCLAW_HOME") + + if err := os.Setenv("HOME", tmp); err != nil { + t.Fatalf("set HOME: %v", err) + } + if err := os.Setenv("PICOCLAW_HOME", filepath.Join(tmp, ".picoclaw")); err != nil { + t.Fatalf("set PICOCLAW_HOME: %v", err) + } + + cfg := config.DefaultConfig() + cfg.ModelList = []config.ModelConfig{{ + ModelName: "custom-default", + Model: "openai/gpt-4o", + APIKey: "sk-default", + }} + cfg.Agents.Defaults.ModelName = "custom-default" + + configPath := filepath.Join(tmp, "config.json") + if err := config.SaveConfig(configPath, cfg); err != nil { + t.Fatalf("SaveConfig error: %v", err) + } + + cleanup := func() { + _ = os.Setenv("HOME", oldHome) + if oldPicoHome == "" { + _ = os.Unsetenv("PICOCLAW_HOME") + } else { + _ = os.Setenv("PICOCLAW_HOME", oldPicoHome) + } + } + return configPath, cleanup +} + +func resetOAuthHooks(t *testing.T) { + t.Helper() + + origNow := oauthNow + origGeneratePKCE := oauthGeneratePKCE + origGenerateState := oauthGenerateState + origBuildAuthorizeURL := oauthBuildAuthorizeURL + origRequestDeviceCode := oauthRequestDeviceCode + origPollDeviceCodeOnce := oauthPollDeviceCodeOnce + origExchangeCodeForTokens := oauthExchangeCodeForTokens + origGetCredential := oauthGetCredential + origSetCredential := oauthSetCredential + origDeleteCredential := oauthDeleteCredential + origLoadConfig := oauthLoadConfig + origSaveConfig := oauthSaveConfig + origFetchProject := oauthFetchAntigravityProject + origFetchGoogleEmail := oauthFetchGoogleUserEmailFunc + + t.Cleanup(func() { + oauthNow = origNow + oauthGeneratePKCE = origGeneratePKCE + oauthGenerateState = origGenerateState + oauthBuildAuthorizeURL = origBuildAuthorizeURL + oauthRequestDeviceCode = origRequestDeviceCode + oauthPollDeviceCodeOnce = origPollDeviceCodeOnce + oauthExchangeCodeForTokens = origExchangeCodeForTokens + oauthGetCredential = origGetCredential + oauthSetCredential = origSetCredential + oauthDeleteCredential = origDeleteCredential + oauthLoadConfig = origLoadConfig + oauthSaveConfig = origSaveConfig + oauthFetchAntigravityProject = origFetchProject + oauthFetchGoogleUserEmailFunc = origFetchGoogleEmail + }) +} diff --git a/web/backend/api/pico.go b/web/backend/api/pico.go new file mode 100644 index 000000000..fc942d51c --- /dev/null +++ b/web/backend/api/pico.go @@ -0,0 +1,161 @@ +package api + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "net" + "net/http" + "strconv" + "time" + + "github.com/sipeed/picoclaw/pkg/config" +) + +// registerPicoRoutes binds Pico Channel management endpoints to the ServeMux. +func (h *Handler) registerPicoRoutes(mux *http.ServeMux) { + mux.HandleFunc("GET /api/pico/token", h.handleGetPicoToken) + mux.HandleFunc("POST /api/pico/token", h.handleRegenPicoToken) + mux.HandleFunc("POST /api/pico/setup", h.handlePicoSetup) +} + +// handleGetPicoToken returns the current WS token and URL for the frontend. +// +// GET /api/pico/token +func (h *Handler) handleGetPicoToken(w http.ResponseWriter, r *http.Request) { + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError) + return + } + + wsURL := buildWsURL(r, cfg) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{ + "token": cfg.Channels.Pico.Token, + "ws_url": wsURL, + "enabled": cfg.Channels.Pico.Enabled, + }) +} + +// handleRegenPicoToken generates a new Pico WebSocket token and saves it. +// +// POST /api/pico/token +func (h *Handler) handleRegenPicoToken(w http.ResponseWriter, r *http.Request) { + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError) + return + } + + token := generateSecureToken() + cfg.Channels.Pico.Token = token + + if err := config.SaveConfig(h.configPath, cfg); err != nil { + http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError) + return + } + + wsURL := fmt.Sprintf("ws://%s/pico/ws", net.JoinHostPort(cfg.Gateway.Host, strconv.Itoa(cfg.Gateway.Port))) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{ + "token": token, + "ws_url": wsURL, + }) +} + +// ensurePicoChannel checks if the Pico Channel is properly configured and +// enables it with sensible defaults if not. Returns true if config was changed. +func (h *Handler) ensurePicoChannel() (bool, error) { + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + return false, fmt.Errorf("failed to load config: %w", err) + } + + changed := false + + if !cfg.Channels.Pico.Enabled { + cfg.Channels.Pico.Enabled = true + changed = true + } + + if cfg.Channels.Pico.Token == "" { + cfg.Channels.Pico.Token = generateSecureToken() + changed = true + } + + if !cfg.Channels.Pico.AllowTokenQuery { + cfg.Channels.Pico.AllowTokenQuery = true + changed = true + } + + // Make sure origins are allowed (frontend might be running on a different port like 5173 during dev) + if len(cfg.Channels.Pico.AllowOrigins) == 0 { + cfg.Channels.Pico.AllowOrigins = []string{"*"} + changed = true + } + + if changed { + if err := config.SaveConfig(h.configPath, cfg); err != nil { + return false, fmt.Errorf("failed to save config: %w", err) + } + } + + return changed, nil +} + +// handlePicoSetup automatically configures everything needed for the Pico Channel to work. +// +// POST /api/pico/setup +func (h *Handler) handlePicoSetup(w http.ResponseWriter, r *http.Request) { + changed, err := h.ensurePicoChannel() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError) + return + } + + wsURL := buildWsURL(r, cfg) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{ + "token": cfg.Channels.Pico.Token, + "ws_url": wsURL, + "enabled": true, + "changed": changed, + }) +} + +// buildWsURL creates a WebSocket URL for the Pico Channel. +// When the gateway host is "0.0.0.0" or empty, it uses the hostname from the +// incoming HTTP request so the browser gets a connectable address. +func buildWsURL(r *http.Request, cfg *config.Config) string { + host := cfg.Gateway.Host + if host == "" || host == "0.0.0.0" { + // Use the hostname the browser used to reach this backend + reqHost, _, err := net.SplitHostPort(r.Host) + if err != nil { + reqHost = r.Host // r.Host might not have a port + } + host = reqHost + } + return "ws://" + net.JoinHostPort(host, strconv.Itoa(cfg.Gateway.Port)) + "/pico/ws" +} + +// generateSecureToken creates a random 32-character hex string. +func generateSecureToken() string { + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + // Fallback to something pseudo-random if crypto/rand fails + return fmt.Sprintf("pico_%x", time.Now().UnixNano()) + } + return hex.EncodeToString(b) +} diff --git a/web/backend/api/router.go b/web/backend/api/router.go new file mode 100644 index 000000000..c250724d1 --- /dev/null +++ b/web/backend/api/router.go @@ -0,0 +1,66 @@ +package api + +import ( + "net/http" + "sync" + + "github.com/sipeed/picoclaw/web/backend/launcherconfig" +) + +// Handler serves HTTP API requests. +type Handler struct { + configPath string + serverPort int + serverPublic bool + serverCIDRs []string + oauthMu sync.Mutex + oauthFlows map[string]*oauthFlow + oauthState map[string]string +} + +// NewHandler creates an instance of the API handler. +func NewHandler(configPath string) *Handler { + return &Handler{ + configPath: configPath, + serverPort: launcherconfig.DefaultPort, + oauthFlows: make(map[string]*oauthFlow), + oauthState: make(map[string]string), + } +} + +// SetServerOptions stores current backend listen options for fallback behavior. +func (h *Handler) SetServerOptions(port int, public bool, allowedCIDRs []string) { + h.serverPort = port + h.serverPublic = public + h.serverCIDRs = append([]string(nil), allowedCIDRs...) +} + +// RegisterRoutes binds all API endpoint handlers to the ServeMux. +func (h *Handler) RegisterRoutes(mux *http.ServeMux) { + // Config CRUD + h.registerConfigRoutes(mux) + + // Pico Channel (WebSocket chat) + h.registerPicoRoutes(mux) + + // Gateway process lifecycle + h.registerGatewayRoutes(mux) + + // Session history + h.registerSessionRoutes(mux) + + // OAuth login and credential management + h.registerOAuthRoutes(mux) + + // Model list management + h.registerModelRoutes(mux) + + // Channel catalog (for frontend navigation/config pages) + h.registerChannelRoutes(mux) + + // OS startup / launch-at-login + h.registerStartupRoutes(mux) + + // Launcher service parameters (port/public) + h.registerLauncherConfigRoutes(mux) +} diff --git a/web/backend/api/session.go b/web/backend/api/session.go new file mode 100644 index 000000000..e3cf674fc --- /dev/null +++ b/web/backend/api/session.go @@ -0,0 +1,286 @@ +package api + +import ( + "encoding/json" + "net/http" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "time" + + "github.com/sipeed/picoclaw/pkg/config" + "github.com/sipeed/picoclaw/pkg/providers" +) + +// registerSessionRoutes binds session list and detail endpoints to the ServeMux. +func (h *Handler) registerSessionRoutes(mux *http.ServeMux) { + mux.HandleFunc("GET /api/sessions", h.handleListSessions) + mux.HandleFunc("GET /api/sessions/{id}", h.handleGetSession) + mux.HandleFunc("DELETE /api/sessions/{id}", h.handleDeleteSession) +} + +// sessionFile mirrors the on-disk session JSON structure from pkg/session. +type sessionFile struct { + Key string `json:"key"` + Messages []providers.Message `json:"messages"` + Summary string `json:"summary,omitempty"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` +} + +// sessionListItem is a lightweight summary returned by GET /api/sessions. +type sessionListItem struct { + ID string `json:"id"` + Preview string `json:"preview"` + MessageCount int `json:"message_count"` + Created string `json:"created"` + Updated string `json:"updated"` +} + +// picoSessionPrefix is the key prefix used by the gateway's routing for Pico +// channel sessions. The full key format is: +// +// agent:main:pico:direct:pico: +// +// The sanitized filename replaces ':' with '_', so on disk it becomes: +// +// agent_main_pico_direct_pico_.json +const picoSessionPrefix = "agent:main:pico:direct:pico:" + +// extractPicoSessionID extracts the session UUID from a full session key. +// Returns the UUID and true if the key matches the Pico session pattern. +func extractPicoSessionID(key string) (string, bool) { + if strings.HasPrefix(key, picoSessionPrefix) { + return strings.TrimPrefix(key, picoSessionPrefix), true + } + return "", false +} + +// sessionsDir resolves the path to the gateway's session storage directory. +// It reads the workspace from config, falling back to ~/.picoclaw/workspace. +func (h *Handler) sessionsDir() (string, error) { + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + return "", err + } + + workspace := cfg.Agents.Defaults.Workspace + if workspace == "" { + home, _ := os.UserHomeDir() + workspace = filepath.Join(home, ".picoclaw", "workspace") + } + + // Expand ~ prefix + if len(workspace) > 0 && workspace[0] == '~' { + home, _ := os.UserHomeDir() + if len(workspace) > 1 && workspace[1] == '/' { + workspace = home + workspace[1:] + } else { + workspace = home + } + } + + return filepath.Join(workspace, "sessions"), nil +} + +// handleListSessions returns a list of Pico session summaries. +// +// GET /api/sessions +func (h *Handler) handleListSessions(w http.ResponseWriter, r *http.Request) { + dir, err := h.sessionsDir() + if err != nil { + http.Error(w, "failed to resolve sessions directory", http.StatusInternalServerError) + return + } + + entries, err := os.ReadDir(dir) + if err != nil { + // Directory doesn't exist yet = no sessions + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode([]sessionListItem{}) + return + } + + items := []sessionListItem{} + + for _, entry := range entries { + if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" { + continue + } + + data, err := os.ReadFile(filepath.Join(dir, entry.Name())) + if err != nil { + continue + } + + var sess sessionFile + if err := json.Unmarshal(data, &sess); err != nil { + continue + } + + // Only include Pico channel sessions + sessionID, ok := extractPicoSessionID(sess.Key) + if !ok { + continue + } + + // Build a preview from the first user message + preview := "" + for _, msg := range sess.Messages { + if msg.Role == "user" && strings.TrimSpace(msg.Content) != "" { + preview = msg.Content + break + } + } + if len([]rune(preview)) > 60 { + preview = string([]rune(preview)[:60]) + "..." + } + if preview == "" { + preview = "(empty)" + } + + // Only count non-empty user and assistant messages + validMessageCount := 0 + for _, msg := range sess.Messages { + if (msg.Role == "user" || msg.Role == "assistant") && strings.TrimSpace(msg.Content) != "" { + validMessageCount++ + } + } + + items = append(items, sessionListItem{ + ID: sessionID, + Preview: preview, + MessageCount: validMessageCount, + Created: sess.Created.Format(time.RFC3339), + Updated: sess.Updated.Format(time.RFC3339), + }) + } + + // Sort by updated descending (most recent first) + sort.Slice(items, func(i, j int) bool { + return items[i].Updated > items[j].Updated + }) + + // Pagination parameters + offsetStr := r.URL.Query().Get("offset") + limitStr := r.URL.Query().Get("limit") + + offset := 0 + limit := 20 // Default limit + + if val, err := strconv.Atoi(offsetStr); err == nil && val >= 0 { + offset = val + } + if val, err := strconv.Atoi(limitStr); err == nil && val > 0 { + limit = val + } + + totalItems := len(items) + + end := offset + limit + if offset >= totalItems { + items = []sessionListItem{} // Out of bounds, return empty + } else { + if end > totalItems { + end = totalItems + } + items = items[offset:end] + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(items) +} + +// handleGetSession returns the full message history for a specific session. +// +// GET /api/sessions/{id} +func (h *Handler) handleGetSession(w http.ResponseWriter, r *http.Request) { + sessionID := r.PathValue("id") + if sessionID == "" { + http.Error(w, "missing session id", http.StatusBadRequest) + return + } + + dir, err := h.sessionsDir() + if err != nil { + http.Error(w, "failed to resolve sessions directory", http.StatusInternalServerError) + return + } + + // The sanitized filename replaces ':' with '_': + // agent:main:pico:direct:pico: -> agent_main_pico_direct_pico_.json + filename := strings.ReplaceAll(picoSessionPrefix+sessionID, ":", "_") + ".json" + + data, err := os.ReadFile(filepath.Join(dir, filename)) + if err != nil { + http.Error(w, "session not found", http.StatusNotFound) + return + } + + var sess sessionFile + if err := json.Unmarshal(data, &sess); err != nil { + http.Error(w, "failed to parse session", http.StatusInternalServerError) + return + } + + // Convert to a simpler format for the frontend + type chatMessage struct { + Role string `json:"role"` + Content string `json:"content"` + } + + messages := make([]chatMessage, 0, len(sess.Messages)) + for _, msg := range sess.Messages { + // Only include user and assistant messages that have actual content + if (msg.Role == "user" || msg.Role == "assistant") && strings.TrimSpace(msg.Content) != "" { + messages = append(messages, chatMessage{ + Role: msg.Role, + Content: msg.Content, + }) + } + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{ + "id": sessionID, + "messages": messages, + "summary": sess.Summary, + "created": sess.Created.Format(time.RFC3339), + "updated": sess.Updated.Format(time.RFC3339), + }) +} + +// handleDeleteSession deletes a specific session. +// +// DELETE /api/sessions/{id} +func (h *Handler) handleDeleteSession(w http.ResponseWriter, r *http.Request) { + sessionID := r.PathValue("id") + if sessionID == "" { + http.Error(w, "missing session id", http.StatusBadRequest) + return + } + + dir, err := h.sessionsDir() + if err != nil { + http.Error(w, "failed to resolve sessions directory", http.StatusInternalServerError) + return + } + + // The sanitized filename replaces ':' with '_': + // agent:main:pico:direct:pico: -> agent_main_pico_direct_pico_.json + filename := strings.ReplaceAll(picoSessionPrefix+sessionID, ":", "_") + ".json" + filePath := filepath.Join(dir, filename) + + if err := os.Remove(filePath); err != nil { + if os.IsNotExist(err) { + http.Error(w, "session not found", http.StatusNotFound) + } else { + http.Error(w, "failed to delete session", http.StatusInternalServerError) + } + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/web/backend/api/startup.go b/web/backend/api/startup.go new file mode 100644 index 000000000..1c685bc90 --- /dev/null +++ b/web/backend/api/startup.go @@ -0,0 +1,305 @@ +package api + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +const ( + autoStartEntryName = "PicoClawLauncher" + launchAgentLabel = "io.picoclaw.launcher" +) + +type autoStartRequest struct { + Enabled bool `json:"enabled"` +} + +type autoStartResponse struct { + Enabled bool `json:"enabled"` + Supported bool `json:"supported"` + Platform string `json:"platform"` + Message string `json:"message,omitempty"` +} + +var errAutoStartUnsupported = errors.New("autostart is not supported on this platform") + +func (h *Handler) registerStartupRoutes(mux *http.ServeMux) { + mux.HandleFunc("GET /api/system/autostart", h.handleGetAutoStart) + mux.HandleFunc("PUT /api/system/autostart", h.handleSetAutoStart) +} + +func (h *Handler) handleGetAutoStart(w http.ResponseWriter, r *http.Request) { + enabled, supported, message, err := h.getAutoStartStatus() + if err != nil { + http.Error(w, fmt.Sprintf("Failed to read startup setting: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(autoStartResponse{ + Enabled: enabled, + Supported: supported, + Platform: runtime.GOOS, + Message: message, + }) +} + +func (h *Handler) handleSetAutoStart(w http.ResponseWriter, r *http.Request) { + var req autoStartRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) + return + } + + if err := h.setAutoStart(req.Enabled); err != nil { + if errors.Is(err, errAutoStartUnsupported) { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + http.Error(w, fmt.Sprintf("Failed to update startup setting: %v", err), http.StatusInternalServerError) + return + } + + enabled, supported, message, err := h.getAutoStartStatus() + if err != nil { + http.Error(w, fmt.Sprintf("Failed to verify startup setting: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(autoStartResponse{ + Enabled: enabled, + Supported: supported, + Platform: runtime.GOOS, + Message: message, + }) +} + +func (h *Handler) resolveLaunchCommand() (string, []string, error) { + exePath, err := os.Executable() + if err != nil { + return "", nil, err + } + + args := []string{"-no-browser"} + if h.configPath != "" { + args = append(args, h.configPath) + } + + return exePath, args, nil +} + +func (h *Handler) getAutoStartStatus() (enabled bool, supported bool, message string, err error) { + switch runtime.GOOS { + case "darwin": + exists, err := fileExists(macLaunchAgentPath()) + return exists, true, "Changes apply on next login.", err + case "linux": + exists, err := fileExists(linuxAutoStartPath()) + return exists, true, "Changes apply on next login.", err + case "windows": + exists, err := windowsRunKeyExists() + return exists, true, "Changes apply on next login.", err + default: + return false, false, "Current platform does not support launch at login.", nil + } +} + +func (h *Handler) setAutoStart(enabled bool) error { + exePath, args, err := h.resolveLaunchCommand() + if err != nil { + return err + } + + switch runtime.GOOS { + case "darwin": + return setDarwinAutoStart(enabled, exePath, args) + case "linux": + return setLinuxAutoStart(enabled, exePath, args) + case "windows": + return setWindowsAutoStart(enabled, exePath, args) + default: + return errAutoStartUnsupported + } +} + +func fileExists(path string) (bool, error) { + _, err := os.Stat(path) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} + +func macLaunchAgentPath() string { + home, _ := os.UserHomeDir() + return filepath.Join(home, "Library", "LaunchAgents", launchAgentLabel+".plist") +} + +func setDarwinAutoStart(enabled bool, exePath string, args []string) error { + plistPath := macLaunchAgentPath() + if enabled { + if err := os.MkdirAll(filepath.Dir(plistPath), 0o755); err != nil { + return err + } + content := buildDarwinPlist(exePath, args) + return os.WriteFile(plistPath, []byte(content), 0o644) + } + + if err := os.Remove(plistPath); err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + +func xmlEscape(s string) string { + var b bytes.Buffer + for _, r := range s { + switch r { + case '&': + b.WriteString("&") + case '<': + b.WriteString("<") + case '>': + b.WriteString(">") + case '"': + b.WriteString(""") + case '\'': + b.WriteString("'") + default: + b.WriteRune(r) + } + } + return b.String() +} + +func buildDarwinPlist(exePath string, args []string) string { + programArgs := make([]string, 0, len(args)+1) + programArgs = append(programArgs, exePath) + programArgs = append(programArgs, args...) + + var b strings.Builder + b.WriteString(`` + "\n") + b.WriteString( + `` + "\n", + ) + b.WriteString(`` + "\n") + b.WriteString(`` + "\n") + b.WriteString(` Label` + "\n") + b.WriteString(` ` + launchAgentLabel + `` + "\n") + b.WriteString(` ProgramArguments` + "\n") + b.WriteString(` ` + "\n") + for _, arg := range programArgs { + b.WriteString(` ` + xmlEscape(arg) + `` + "\n") + } + b.WriteString(` ` + "\n") + b.WriteString(` RunAtLoad` + "\n") + b.WriteString(` ` + "\n") + b.WriteString(` ProcessType` + "\n") + b.WriteString(` Background` + "\n") + b.WriteString(`` + "\n") + b.WriteString(`` + "\n") + return b.String() +} + +func linuxAutoStartPath() string { + home, _ := os.UserHomeDir() + return filepath.Join(home, ".config", "autostart", "picoclaw-web.desktop") +} + +func shellQuote(s string) string { + if s == "" { + return "''" + } + if !strings.ContainsAny(s, " \t\n'\"\\$`") { + return s + } + return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'" +} + +func buildLinuxExecLine(exePath string, args []string) string { + parts := make([]string, 0, len(args)+1) + parts = append(parts, shellQuote(exePath)) + for _, arg := range args { + parts = append(parts, shellQuote(arg)) + } + return strings.Join(parts, " ") +} + +func setLinuxAutoStart(enabled bool, exePath string, args []string) error { + desktopPath := linuxAutoStartPath() + if enabled { + if err := os.MkdirAll(filepath.Dir(desktopPath), 0o755); err != nil { + return err + } + content := strings.Join([]string{ + "[Desktop Entry]", + "Type=Application", + "Version=1.0", + "Name=PicoClaw Web", + "Comment=Start PicoClaw Web on login", + "Exec=" + buildLinuxExecLine(exePath, args), + "Terminal=false", + "X-GNOME-Autostart-enabled=true", + "NoDisplay=true", + "", + }, "\n") + return os.WriteFile(desktopPath, []byte(content), 0o644) + } + + if err := os.Remove(desktopPath); err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + +func windowsCommandLine(exePath string, args []string) string { + parts := make([]string, 0, len(args)+1) + parts = append(parts, fmt.Sprintf("%q", exePath)) + for _, arg := range args { + parts = append(parts, fmt.Sprintf("%q", arg)) + } + return strings.Join(parts, " ") +} + +func windowsRunKeyExists() (bool, error) { + cmd := exec.Command("reg", "query", `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, "/v", autoStartEntryName) + if err := cmd.Run(); err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + return false, nil + } + return false, err + } + return true, nil +} + +func setWindowsAutoStart(enabled bool, exePath string, args []string) error { + key := `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` + if enabled { + commandLine := windowsCommandLine(exePath, args) + cmd := exec.Command("reg", "add", key, "/v", autoStartEntryName, "/t", "REG_SZ", "/d", commandLine, "/f") + return cmd.Run() + } + + cmd := exec.Command("reg", "delete", key, "/v", autoStartEntryName, "/f") + if err := cmd.Run(); err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + return nil + } + return err + } + return nil +} diff --git a/web/backend/api/startup_test.go b/web/backend/api/startup_test.go new file mode 100644 index 000000000..cfa9b4c53 --- /dev/null +++ b/web/backend/api/startup_test.go @@ -0,0 +1,56 @@ +package api + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/sipeed/picoclaw/web/backend/launcherconfig" +) + +func TestResolveLaunchCommandUsesConfigFileDefaults(t *testing.T) { + configPath := filepath.Join(t.TempDir(), "config.json") + h := NewHandler(configPath) + + // Persist non-default launcher options to ensure resolveLaunchCommand does not + // pin them into autostart args. + launcherPath := launcherconfig.PathForAppConfig(configPath) + if err := launcherconfig.Save(launcherPath, launcherconfig.Config{ + Port: 19999, + Public: true, + }); err != nil { + t.Fatalf("launcherconfig.Save() error = %v", err) + } + + exePath, args, err := h.resolveLaunchCommand() + if err != nil { + t.Fatalf("resolveLaunchCommand() error = %v", err) + } + if exePath == "" { + t.Fatal("resolveLaunchCommand() returned empty executable path") + } + if len(args) != 2 { + t.Fatalf("args len = %d, want 2 (got %v)", len(args), args) + } + if args[0] != "-no-browser" { + t.Fatalf("args[0] = %q, want %q", args[0], "-no-browser") + } + if args[1] != configPath { + t.Fatalf("args[1] = %q, want %q", args[1], configPath) + } + for _, arg := range args { + if arg == "-port" || arg == "-public" { + t.Fatalf("autostart args should not pin network flags, got %v", args) + } + } +} + +func TestBuildDarwinPlistIncludesRunAtLoad(t *testing.T) { + plist := buildDarwinPlist("/tmp/picoclaw-web", []string{"-no-browser", "/tmp/config.json"}) + if !strings.Contains(plist, "RunAtLoad") { + t.Fatalf("plist missing RunAtLoad key:\n%s", plist) + } + if !strings.Contains(plist, "") { + t.Fatalf("plist missing RunAtLoad true value:\n%s", plist) + } +} diff --git a/web/backend/dist/.gitkeep b/web/backend/dist/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/web/backend/embed.go b/web/backend/embed.go new file mode 100644 index 000000000..556fb7384 --- /dev/null +++ b/web/backend/embed.go @@ -0,0 +1,69 @@ +package main + +import ( + "embed" + "io/fs" + "log" + "net/http" + "path" + "strings" +) + +//go:embed all:dist +var frontendFS embed.FS + +// registerEmbedRoutes sets up the HTTP handler to serve the embedded frontend files +func registerEmbedRoutes(mux *http.ServeMux) { + // Attempt to get the subdirectory 'dist' where Vite usually builds + subFS, err := fs.Sub(frontendFS, "dist") + if err != nil { + // Log a warning if dist doesn't exist yet (e.g., during development before a frontend build) + log.Printf( + "Warning: no 'dist' folder found in embedded frontend. " + + "Ensure you run `pnpm build:backend` in the frontend directory " + + "before building the Go backend.", + ) + return + } + + fileServer := http.FileServer(http.FS(subFS)) + + // Serve static assets and fallback to index.html for SPA routes. + mux.Handle( + "/", + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet && r.Method != http.MethodHead { + http.NotFound(w, r) + return + } + + // Keep unknown API paths as 404 instead of falling back to SPA entry. + if r.URL.Path == "/api" || strings.HasPrefix(r.URL.Path, "/api/") { + http.NotFound(w, r) + return + } + + cleanPath := path.Clean(strings.TrimPrefix(r.URL.Path, "/")) + if cleanPath == "." { + cleanPath = "" + } + + // Existing static files/directories should be served directly. + if cleanPath != "" { + if _, statErr := fs.Stat(subFS, cleanPath); statErr == nil { + fileServer.ServeHTTP(w, r) + return + } + // Missing asset-like paths should remain 404. + if strings.Contains(path.Base(cleanPath), ".") { + fileServer.ServeHTTP(w, r) + return + } + } + + indexReq := r.Clone(r.Context()) + indexReq.URL.Path = "/" + fileServer.ServeHTTP(w, indexReq) + }), + ) +} diff --git a/web/backend/embed_test.go b/web/backend/embed_test.go new file mode 100644 index 000000000..c0365488e --- /dev/null +++ b/web/backend/embed_test.go @@ -0,0 +1,33 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestUnknownAPIPathStays404(t *testing.T) { + mux := http.NewServeMux() + registerEmbedRoutes(mux) + + req := httptest.NewRequest(http.MethodGet, "/api/not-found", nil) + rr := httptest.NewRecorder() + mux.ServeHTTP(rr, req) + + if rr.Code != http.StatusNotFound { + t.Fatalf("status = %d, want %d", rr.Code, http.StatusNotFound) + } +} + +func TestMissingAssetStays404(t *testing.T) { + mux := http.NewServeMux() + registerEmbedRoutes(mux) + + req := httptest.NewRequest(http.MethodGet, "/assets/not-found.js", nil) + rr := httptest.NewRecorder() + mux.ServeHTTP(rr, req) + + if rr.Code != http.StatusNotFound { + t.Fatalf("status = %d, want %d", rr.Code, http.StatusNotFound) + } +} diff --git a/cmd/picoclaw-launcher/icon.ico b/web/backend/icon.ico similarity index 100% rename from cmd/picoclaw-launcher/icon.ico rename to web/backend/icon.ico diff --git a/web/backend/launcherconfig/config.go b/web/backend/launcherconfig/config.go new file mode 100644 index 000000000..4dca45b0e --- /dev/null +++ b/web/backend/launcherconfig/config.go @@ -0,0 +1,113 @@ +package launcherconfig + +import ( + "encoding/json" + "fmt" + "net" + "os" + "path/filepath" + "strings" +) + +const ( + // FileName is the launcher-specific settings file name. + FileName = "launcher-config.json" + // DefaultPort is the default port for the web launcher. + DefaultPort = 18800 +) + +// Config stores launch parameters for the web backend service. +type Config struct { + Port int `json:"port"` + Public bool `json:"public"` + AllowedCIDRs []string `json:"allowed_cidrs,omitempty"` +} + +// Default returns default launcher settings. +func Default() Config { + return Config{Port: DefaultPort, Public: false} +} + +// Validate checks if launcher settings are valid. +func Validate(cfg Config) error { + if cfg.Port < 1 || cfg.Port > 65535 { + return fmt.Errorf("port %d is out of range (1-65535)", cfg.Port) + } + for _, cidr := range cfg.AllowedCIDRs { + if _, _, err := net.ParseCIDR(cidr); err != nil { + return fmt.Errorf("invalid CIDR %q", cidr) + } + } + return nil +} + +// NormalizeCIDRs trims entries, removes empty values, and deduplicates CIDRs. +func NormalizeCIDRs(cidrs []string) []string { + if len(cidrs) == 0 { + return nil + } + out := make([]string, 0, len(cidrs)) + seen := make(map[string]struct{}, len(cidrs)) + for _, raw := range cidrs { + trimmed := strings.TrimSpace(raw) + if trimmed == "" { + continue + } + if _, ok := seen[trimmed]; ok { + continue + } + seen[trimmed] = struct{}{} + out = append(out, trimmed) + } + if len(out) == 0 { + return nil + } + return out +} + +// PathForAppConfig returns launcher-config path near the app config file. +func PathForAppConfig(appConfigPath string) string { + dir := filepath.Dir(appConfigPath) + if dir == "" || dir == "." { + dir = "." + } + return filepath.Join(dir, FileName) +} + +// Load reads launcher settings; fallback is returned when file does not exist. +func Load(path string, fallback Config) (Config, error) { + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return fallback, nil + } + return Config{}, err + } + + cfg := fallback + if err := json.Unmarshal(data, &cfg); err != nil { + return Config{}, err + } + cfg.AllowedCIDRs = NormalizeCIDRs(cfg.AllowedCIDRs) + if err := Validate(cfg); err != nil { + return Config{}, err + } + return cfg, nil +} + +// Save writes launcher settings to disk. +func Save(path string, cfg Config) error { + cfg.AllowedCIDRs = NormalizeCIDRs(cfg.AllowedCIDRs) + if err := Validate(cfg); err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return err + } + data = append(data, '\n') + return os.WriteFile(path, data, 0o600) +} diff --git a/web/backend/launcherconfig/config_test.go b/web/backend/launcherconfig/config_test.go new file mode 100644 index 000000000..c63bee09a --- /dev/null +++ b/web/backend/launcherconfig/config_test.go @@ -0,0 +1,89 @@ +package launcherconfig + +import ( + "os" + "path/filepath" + "testing" +) + +func TestLoadReturnsFallbackWhenMissing(t *testing.T) { + path := filepath.Join(t.TempDir(), "launcher-config.json") + fallback := Config{Port: 19999, Public: true} + + got, err := Load(path, fallback) + if err != nil { + t.Fatalf("Load() error = %v", err) + } + if got.Port != fallback.Port || got.Public != fallback.Public { + t.Fatalf("Load() = %+v, want %+v", got, fallback) + } +} + +func TestSaveAndLoadRoundTrip(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "launcher-config.json") + want := Config{ + Port: 18080, + Public: true, + AllowedCIDRs: []string{"192.168.1.0/24", "10.0.0.0/8"}, + } + + if err := Save(path, want); err != nil { + t.Fatalf("Save() error = %v", err) + } + got, err := Load(path, Default()) + if err != nil { + t.Fatalf("Load() error = %v", err) + } + if got.Port != want.Port || got.Public != want.Public { + t.Fatalf("Load() = %+v, want %+v", got, want) + } + if len(got.AllowedCIDRs) != len(want.AllowedCIDRs) { + t.Fatalf("allowed_cidrs len = %d, want %d", len(got.AllowedCIDRs), len(want.AllowedCIDRs)) + } + for i := range want.AllowedCIDRs { + if got.AllowedCIDRs[i] != want.AllowedCIDRs[i] { + t.Fatalf("allowed_cidrs[%d] = %q, want %q", i, got.AllowedCIDRs[i], want.AllowedCIDRs[i]) + } + } + + stat, err := os.Stat(path) + if err != nil { + t.Fatalf("Stat() error = %v", err) + } + if perm := stat.Mode().Perm(); perm != 0o600 { + t.Fatalf("file perm = %o, want 600", perm) + } +} + +func TestValidateRejectsInvalidPort(t *testing.T) { + if err := Validate(Config{Port: 0, Public: false}); err == nil { + t.Fatal("Validate() expected error for port 0") + } + if err := Validate(Config{Port: 65536, Public: false}); err == nil { + t.Fatal("Validate() expected error for port 65536") + } +} + +func TestValidateRejectsInvalidCIDR(t *testing.T) { + err := Validate(Config{ + Port: 18800, + AllowedCIDRs: []string{"192.168.1.0/24", "not-a-cidr"}, + }) + if err == nil { + t.Fatal("Validate() expected error for invalid CIDR") + } +} + +func TestNormalizeCIDRs(t *testing.T) { + got := NormalizeCIDRs([]string{" 192.168.1.0/24 ", "", "10.0.0.0/8", "192.168.1.0/24"}) + want := []string{"192.168.1.0/24", "10.0.0.0/8"} + if len(got) != len(want) { + t.Fatalf("len(got) = %d, want %d", len(got), len(want)) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("got[%d] = %q, want %q", i, got[i], want[i]) + } + } +} diff --git a/web/backend/main.go b/web/backend/main.go new file mode 100644 index 000000000..b8c4dc2bb --- /dev/null +++ b/web/backend/main.go @@ -0,0 +1,164 @@ +// PicoClaw Web Console - Web-based chat and management interface +// +// Provides a web UI for chatting with PicoClaw via the Pico Channel WebSocket, +// with configuration management and gateway process control. +// +// Usage: +// +// go build -o picoclaw-web ./web/backend/ +// ./picoclaw-web [config.json] +// ./picoclaw-web -public config.json + +package main + +import ( + "errors" + "flag" + "fmt" + "log" + "net/http" + "os" + "path/filepath" + "strconv" + "time" + + "github.com/sipeed/picoclaw/web/backend/api" + "github.com/sipeed/picoclaw/web/backend/launcherconfig" + "github.com/sipeed/picoclaw/web/backend/middleware" +) + +func main() { + port := flag.String("port", "18800", "Port to listen on") + public := flag.Bool("public", false, "Listen on all interfaces (0.0.0.0) instead of localhost only") + noBrowser := flag.Bool("no-browser", false, "Do not auto-open browser on startup") + + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "PicoClaw Launcher - A web-based configuration editor\n\n") + fmt.Fprintf(os.Stderr, "Usage: %s [options] [config.json]\n\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "Arguments:\n") + fmt.Fprintf(os.Stderr, " config.json Path to the configuration file (default: ~/.picoclaw/config.json)\n\n") + fmt.Fprintf(os.Stderr, "Options:\n") + flag.PrintDefaults() + fmt.Fprintf(os.Stderr, "\nExamples:\n") + fmt.Fprintf(os.Stderr, " %s Use default config path\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " %s ./config.json Specify a config file\n", os.Args[0]) + fmt.Fprintf( + os.Stderr, + " %s -public ./config.json Allow access from other devices on the network\n", + os.Args[0], + ) + } + flag.Parse() + + // Resolve config path + configPath := getDefaultConfigPath() + if flag.NArg() > 0 { + configPath = flag.Arg(0) + } + + absPath, err := filepath.Abs(configPath) + if err != nil { + log.Fatalf("Failed to resolve config path: %v", err) + } + + var explicitPort bool + var explicitPublic bool + flag.Visit(func(f *flag.Flag) { + switch f.Name { + case "port": + explicitPort = true + case "public": + explicitPublic = true + } + }) + + launcherPath := launcherconfig.PathForAppConfig(absPath) + launcherCfg, err := launcherconfig.Load(launcherPath, launcherconfig.Default()) + if err != nil { + log.Printf("Warning: Failed to load %s: %v", launcherPath, err) + launcherCfg = launcherconfig.Default() + } + + effectivePort := *port + effectivePublic := *public + if !explicitPort { + effectivePort = strconv.Itoa(launcherCfg.Port) + } + if !explicitPublic { + effectivePublic = launcherCfg.Public + } + + portNum, err := strconv.Atoi(effectivePort) + if err != nil || portNum < 1 || portNum > 65535 { + if err == nil { + err = errors.New("must be in range 1-65535") + } + log.Fatalf("Invalid port %q: %v", effectivePort, err) + } + + // Determine listen address + var addr string + if effectivePublic { + addr = "0.0.0.0:" + effectivePort + } else { + addr = "127.0.0.1:" + effectivePort + } + + // Initialize Server components + mux := http.NewServeMux() + + // API Routes (e.g. /api/status) + apiHandler := api.NewHandler(absPath) + apiHandler.SetServerOptions(portNum, effectivePublic, launcherCfg.AllowedCIDRs) + apiHandler.RegisterRoutes(mux) + + // Frontend Embedded Assets + registerEmbedRoutes(mux) + + accessControlledMux, err := middleware.IPAllowlist(launcherCfg.AllowedCIDRs, mux) + if err != nil { + log.Fatalf("Invalid allowed CIDR configuration: %v", err) + } + + // Apply middleware stack + handler := middleware.Recoverer( + middleware.Logger( + middleware.JSONContentType(accessControlledMux), + ), + ) + + // Print startup banner + fmt.Print(banner) + fmt.Println() + fmt.Println(" Open the following URL in your browser:") + fmt.Println() + fmt.Printf(" >> http://localhost:%s <<\n", effectivePort) + if effectivePublic { + if ip := getLocalIP(); ip != "" { + fmt.Printf(" >> http://%s:%s <<\n", ip, effectivePort) + } + } + fmt.Println() + + // Auto-open browser + if !*noBrowser { + go func() { + time.Sleep(500 * time.Millisecond) + url := "http://localhost:" + effectivePort + if err := openBrowser(url); err != nil { + log.Printf("Warning: Failed to auto-open browser: %v", err) + } + }() + } + + // Auto-start gateway after backend starts listening. + go func() { + time.Sleep(1 * time.Second) + apiHandler.TryAutoStartGateway() + }() + + // Start the Server + if err := http.ListenAndServe(addr, handler); err != nil { + log.Fatalf("Server failed to start: %v", err) + } +} diff --git a/web/backend/middleware/access_control.go b/web/backend/middleware/access_control.go new file mode 100644 index 000000000..159d60c3e --- /dev/null +++ b/web/backend/middleware/access_control.go @@ -0,0 +1,64 @@ +package middleware + +import ( + "fmt" + "net" + "net/http" + "strings" +) + +// IPAllowlist restricts access to requests from configured CIDR ranges. +// Loopback addresses are always allowed for local administration. +// Empty CIDR list means no restriction. +func IPAllowlist(allowedCIDRs []string, next http.Handler) (http.Handler, error) { + if len(allowedCIDRs) == 0 { + return next, nil + } + + nets := make([]*net.IPNet, 0, len(allowedCIDRs)) + for _, cidr := range allowedCIDRs { + _, ipNet, err := net.ParseCIDR(cidr) + if err != nil { + return nil, fmt.Errorf("invalid CIDR %q: %w", cidr, err) + } + nets = append(nets, ipNet) + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ip := clientIPFromRemoteAddr(r.RemoteAddr) + if ip == nil { + rejectByPolicy(w, r) + return + } + if ip.IsLoopback() { + next.ServeHTTP(w, r) + return + } + for _, ipNet := range nets { + if ipNet.Contains(ip) { + next.ServeHTTP(w, r) + return + } + } + + rejectByPolicy(w, r) + }), nil +} + +func clientIPFromRemoteAddr(remoteAddr string) net.IP { + host := remoteAddr + if h, _, err := net.SplitHostPort(remoteAddr); err == nil { + host = h + } + return net.ParseIP(host) +} + +func rejectByPolicy(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, "/api/") { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(`{"error":"access denied by network policy"}`)) + return + } + http.Error(w, "Forbidden", http.StatusForbidden) +} diff --git a/web/backend/middleware/access_control_test.go b/web/backend/middleware/access_control_test.go new file mode 100644 index 000000000..259fd4a4c --- /dev/null +++ b/web/backend/middleware/access_control_test.go @@ -0,0 +1,86 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestIPAllowlist_EmptyCIDRsAllowsAll(t *testing.T) { + h, err := IPAllowlist(nil, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + if err != nil { + t.Fatalf("IPAllowlist() error = %v", err) + } + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.RemoteAddr = "203.0.113.5:1234" + h.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) + } +} + +func TestIPAllowlist_RejectsOutsideCIDR(t *testing.T) { + h, err := IPAllowlist([]string{"192.168.1.0/24"}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + if err != nil { + t.Fatalf("IPAllowlist() error = %v", err) + } + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/config", nil) + req.RemoteAddr = "10.0.0.8:1234" + h.ServeHTTP(rec, req) + + if rec.Code != http.StatusForbidden { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusForbidden) + } +} + +func TestIPAllowlist_AllowsInsideCIDR(t *testing.T) { + h, err := IPAllowlist([]string{"192.168.1.0/24"}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + if err != nil { + t.Fatalf("IPAllowlist() error = %v", err) + } + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.RemoteAddr = "192.168.1.88:1234" + h.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) + } +} + +func TestIPAllowlist_AlwaysAllowsLoopback(t *testing.T) { + h, err := IPAllowlist([]string{"192.168.1.0/24"}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + if err != nil { + t.Fatalf("IPAllowlist() error = %v", err) + } + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.RemoteAddr = "127.0.0.1:1234" + h.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) + } +} + +func TestIPAllowlist_InvalidCIDR(t *testing.T) { + _, err := IPAllowlist([]string{"bad-cidr"}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + if err == nil { + t.Fatal("IPAllowlist() expected error for invalid CIDR") + } +} diff --git a/web/backend/middleware/middleware.go b/web/backend/middleware/middleware.go new file mode 100644 index 000000000..de9e6d870 --- /dev/null +++ b/web/backend/middleware/middleware.go @@ -0,0 +1,70 @@ +package middleware + +import ( + "log" + "net/http" + "runtime/debug" + "strings" + "time" +) + +// JSONContentType sets the Content-Type header to application/json for +// API requests handled by the wrapped handler. +// SSE endpoints (text/event-stream) are excluded. +func JSONContentType(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, "/api/") && !strings.HasSuffix(r.URL.Path, "/events") { + w.Header().Set("Content-Type", "application/json") + } + next.ServeHTTP(w, r) + }) +} + +// responseRecorder wraps http.ResponseWriter to capture the status code. +type responseRecorder struct { + http.ResponseWriter + statusCode int +} + +func (rr *responseRecorder) WriteHeader(code int) { + rr.statusCode = code + rr.ResponseWriter.WriteHeader(code) +} + +// Flush delegates to the underlying ResponseWriter if it implements http.Flusher. +// This is required for SSE (Server-Sent Events) to work through the middleware. +func (rr *responseRecorder) Flush() { + if f, ok := rr.ResponseWriter.(http.Flusher); ok { + f.Flush() + } +} + +// Unwrap returns the underlying ResponseWriter so that http.ResponseController +// and interface checks (like http.Flusher) can see through the wrapper. +func (rr *responseRecorder) Unwrap() http.ResponseWriter { + return rr.ResponseWriter +} + +// Logger logs each HTTP request with method, path, status code, and duration. +func Logger(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + rec := &responseRecorder{ResponseWriter: w, statusCode: http.StatusOK} + next.ServeHTTP(rec, r) + log.Printf("%s %s %d %s", r.Method, r.URL.Path, rec.statusCode, time.Since(start)) + }) +} + +// Recoverer recovers from panics in downstream handlers and returns a 500 +// Internal Server Error response. +func Recoverer(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + log.Printf("panic recovered: %v\n%s", err, debug.Stack()) + http.Error(w, `{"error":"internal server error"}`, http.StatusInternalServerError) + } + }() + next.ServeHTTP(w, r) + }) +} diff --git a/web/backend/model/status.go b/web/backend/model/status.go new file mode 100644 index 000000000..325981502 --- /dev/null +++ b/web/backend/model/status.go @@ -0,0 +1,8 @@ +package model + +// StatusResponse represents the response payload for the GET /api/status endpoint. +type StatusResponse struct { + Status string `json:"status"` + Version string `json:"version"` + Uptime string `json:"uptime"` +} diff --git a/web/backend/utils.go b/web/backend/utils.go new file mode 100644 index 000000000..6fa734aeb --- /dev/null +++ b/web/backend/utils.go @@ -0,0 +1,61 @@ +package main + +import ( + "fmt" + "net" + "os" + "os/exec" + "path/filepath" + "runtime" +) + +const ( + colorBlue = "\x1b[38;2;62;93;185m" + colorRed = "\x1b[38;2;213;70;70m" + colorReset = "\x1b[0m" + banner = "\r\n" + + colorBlue + "██████╗ ██╗ ██████╗ ██████╗ " + colorRed + " ██████╗██╗ █████╗ ██╗ ██╗\n" + + colorBlue + "██╔══██╗██║██╔════╝██╔═══██╗" + colorRed + "██╔════╝██║ ██╔══██╗██║ ██║\n" + + colorBlue + "██████╔╝██║██║ ██║ ██║" + colorRed + "██║ ██║ ███████║██║ █╗ ██║\n" + + colorBlue + "██╔═══╝ ██║██║ ██║ ██║" + colorRed + "██║ ██║ ██╔══██║██║███╗██║\n" + + colorBlue + "██║ ██║╚██████╗╚██████╔╝" + colorRed + "╚██████╗███████╗██║ ██║╚███╔███╔╝\n" + + colorBlue + "╚═╝ ╚═╝ ╚═════╝ ╚═════╝ " + colorRed + " ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝\n" + + colorReset +) + +// getDefaultConfigPath returns the default path to the picoclaw config file. +func getDefaultConfigPath() string { + home, err := os.UserHomeDir() + if err != nil { + return "config.json" + } + return filepath.Join(home, ".picoclaw", "config.json") +} + +// getLocalIP returns the local IP address of the machine. +func getLocalIP() string { + addrs, err := net.InterfaceAddrs() + if err != nil { + return "" + } + for _, a := range addrs { + if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.To4() != nil { + return ipnet.IP.String() + } + } + return "" +} + +// openBrowser automatically opens the given URL in the default browser. +func openBrowser(url string) error { + switch runtime.GOOS { + case "linux": + return exec.Command("xdg-open", url).Start() + case "windows": + return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + case "darwin": + return exec.Command("open", url).Start() + default: + return fmt.Errorf("unsupported platform") + } +} diff --git a/cmd/picoclaw-launcher/winres/winres.json b/web/backend/winres/winres.json similarity index 100% rename from cmd/picoclaw-launcher/winres/winres.json rename to web/backend/winres/winres.json diff --git a/web/frontend/.editorconfig b/web/frontend/.editorconfig new file mode 100644 index 000000000..a8c0f1ecf --- /dev/null +++ b/web/frontend/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf \ No newline at end of file diff --git a/web/frontend/.gitignore b/web/frontend/.gitignore new file mode 100644 index 000000000..4811cdd9b --- /dev/null +++ b/web/frontend/.gitignore @@ -0,0 +1,26 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +.tanstack \ No newline at end of file diff --git a/web/frontend/.prettierignore b/web/frontend/.prettierignore new file mode 100644 index 000000000..7040bf59e --- /dev/null +++ b/web/frontend/.prettierignore @@ -0,0 +1,5 @@ +package-lock.json +pnpm-lock.yaml +yarn.lock +routeTree.gen.ts +src/components/ui \ No newline at end of file diff --git a/web/frontend/components.json b/web/frontend/components.json new file mode 100644 index 000000000..9d5329694 --- /dev/null +++ b/web/frontend/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "radix-vega", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "tabler", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/web/frontend/eslint.config.js b/web/frontend/eslint.config.js new file mode 100644 index 000000000..bc9c64344 --- /dev/null +++ b/web/frontend/eslint.config.js @@ -0,0 +1,31 @@ +import js from "@eslint/js" +import eslintConfigPrettier from "eslint-config-prettier" +import reactHooks from "eslint-plugin-react-hooks" +import reactRefresh from "eslint-plugin-react-refresh" +import { defineConfig, globalIgnores } from "eslint/config" +import globals from "globals" +import tseslint from "typescript-eslint" + +export default defineConfig([ + globalIgnores(["dist", "src/components/ui", "src/routeTree.gen.ts"]), + { + files: ["**/*.{ts,tsx}"], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + eslintConfigPrettier, + ], + languageOptions: { + ecmaVersion: "latest", + globals: globals.browser, + }, + rules: { + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + }, + }, +]) diff --git a/web/frontend/index.html b/web/frontend/index.html new file mode 100644 index 000000000..d3bdd90f8 --- /dev/null +++ b/web/frontend/index.html @@ -0,0 +1,18 @@ + + + + + + + + + + + PicoClaw + + + +
+ + + diff --git a/web/frontend/package.json b/web/frontend/package.json new file mode 100644 index 000000000..ee46cdcda --- /dev/null +++ b/web/frontend/package.json @@ -0,0 +1,62 @@ +{ + "name": "picoclaw-web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "build:backend": "tsc -b && vite build --outDir ../backend/dist --emptyOutDir", + "lint": "eslint .", + "preview": "vite preview", + "format": "prettier --check .", + "check": "prettier --write . && eslint --fix" + }, + "dependencies": { + "@fontsource-variable/inter": "^5.2.8", + "@tabler/icons-react": "^3.38.0", + "@tailwindcss/vite": "^4.2.1", + "@tanstack/react-query": "^5.90.21", + "@tanstack/react-router": "^1.163.3", + "@tanstack/react-router-devtools": "^1.163.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "dayjs": "^1.11.19", + "i18next": "^25.8.14", + "i18next-browser-languagedetector": "^8.2.1", + "jotai": "^2.18.0", + "radix-ui": "^1.4.3", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-i18next": "^16.5.4", + "react-markdown": "^10.1.0", + "react-textarea-autosize": "^8.5.9", + "remark-gfm": "^4.0.1", + "shadcn": "^3.8.5", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0", + "tailwindcss": "^4.2.1", + "tw-animate-css": "^1.4.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@tailwindcss/typography": "^0.5.19", + "@tanstack/router-plugin": "^1.164.0", + "@trivago/prettier-plugin-sort-imports": "^6.0.2", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@typescript-eslint/eslint-plugin": "^8.56.1", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "prettier": "^3.8.1", + "prettier-plugin-tailwindcss": "^0.7.2", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.1" + } +} diff --git a/web/frontend/pnpm-lock.yaml b/web/frontend/pnpm-lock.yaml new file mode 100644 index 000000000..8e89cbbe5 --- /dev/null +++ b/web/frontend/pnpm-lock.yaml @@ -0,0 +1,7981 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@fontsource-variable/inter': + specifier: ^5.2.8 + version: 5.2.8 + '@tabler/icons-react': + specifier: ^3.38.0 + version: 3.38.0(react@19.2.4) + '@tailwindcss/vite': + specifier: ^4.2.1 + version: 4.2.1(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) + '@tanstack/react-query': + specifier: ^5.90.21 + version: 5.90.21(react@19.2.4) + '@tanstack/react-router': + specifier: ^1.163.3 + version: 1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/react-router-devtools': + specifier: ^1.163.3 + version: 1.163.3(@tanstack/react-router@1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.163.3)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + dayjs: + specifier: ^1.11.19 + version: 1.11.19 + i18next: + specifier: ^25.8.14 + version: 25.8.14(typescript@5.9.3) + i18next-browser-languagedetector: + specifier: ^8.2.1 + version: 8.2.1 + jotai: + specifier: ^2.18.0 + version: 2.18.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4) + radix-ui: + specifier: ^1.4.3 + version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: + specifier: ^19.2.0 + version: 19.2.4 + react-dom: + specifier: ^19.2.0 + version: 19.2.4(react@19.2.4) + react-i18next: + specifier: ^16.5.4 + version: 16.5.4(i18next@25.8.14(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.2.14)(react@19.2.4) + react-textarea-autosize: + specifier: ^8.5.9 + version: 8.5.9(@types/react@19.2.14)(react@19.2.4) + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 + shadcn: + specifier: ^3.8.5 + version: 3.8.5(@types/node@24.11.0)(typescript@5.9.3) + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + tailwind-merge: + specifier: ^3.5.0 + version: 3.5.0 + tailwindcss: + specifier: ^4.2.1 + version: 4.2.1 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 + devDependencies: + '@eslint/js': + specifier: ^9.39.1 + version: 9.39.3 + '@tailwindcss/typography': + specifier: ^0.5.19 + version: 0.5.19(tailwindcss@4.2.1) + '@tanstack/router-plugin': + specifier: ^1.164.0 + version: 1.164.0(@tanstack/react-router@1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) + '@trivago/prettier-plugin-sort-imports': + specifier: ^6.0.2 + version: 6.0.2(prettier@3.8.1) + '@types/node': + specifier: ^24.10.1 + version: 24.11.0 + '@types/react': + specifier: ^19.2.7 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@typescript-eslint/eslint-plugin': + specifier: ^8.56.1 + version: 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + '@vitejs/plugin-react': + specifier: ^5.1.1 + version: 5.1.4(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) + eslint: + specifier: ^9.39.1 + version: 9.39.3(jiti@2.6.1) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-react-hooks: + specifier: ^7.0.1 + version: 7.0.1(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-react-refresh: + specifier: ^0.4.24 + version: 0.4.26(eslint@9.39.3(jiti@2.6.1)) + globals: + specifier: ^16.5.0 + version: 16.5.0 + prettier: + specifier: ^3.8.1 + version: 3.8.1 + prettier-plugin-tailwindcss: + specifier: ^0.7.2 + version: 0.7.2(@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.1))(prettier@3.8.1) + typescript: + specifier: ~5.9.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.48.0 + version: 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + vite: + specifier: ^7.3.1 + version: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) + +packages: + + '@antfu/ni@25.0.0': + resolution: {integrity: sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA==} + hasBin: true + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.6': + resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.28.6': + resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.6': + resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.28.5': + resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@dotenvx/dotenvx@1.52.0': + resolution: {integrity: sha512-CaQcc8JvtzQhUSm9877b6V4Tb7HCotkcyud9X2YwdqtQKwgljkMRwU96fVYKnzN3V0Hj74oP7Es+vZ0mS+Aa1w==} + hasBin: true + + '@ecies/ciphers@0.2.5': + resolution: {integrity: sha512-GalEZH4JgOMHYYcYmVqnFirFsjZHeoGMDt9IxEnM9F7GRUUyUksJ7Ou53L83WHJq3RWKD3AcBpo0iQh0oMpf8A==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} + peerDependencies: + '@noble/ciphers': ^1.0.0 + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.4': + resolution: {integrity: sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.3': + resolution: {integrity: sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.4': + resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} + + '@floating-ui/dom@1.7.5': + resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==} + + '@floating-ui/react-dom@2.1.7': + resolution: {integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@fontsource-variable/inter@5.2.8': + resolution: {integrity: sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==} + + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@modelcontextprotocol/sdk@1.27.1': + resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@mswjs/interceptors@0.41.3': + resolution: {integrity: sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==} + engines: {node: '>=18'} + + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-accessible-icon@1.1.7': + resolution: {integrity: sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-accordion@1.2.12': + resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-alert-dialog@1.1.15': + resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-aspect-ratio@1.1.7': + resolution: {integrity: sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.10': + resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context-menu@2.2.16': + resolution: {integrity: sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-form@0.1.8': + resolution: {integrity: sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.1.15': + resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.7': + resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menubar@1.1.16': + resolution: {integrity: sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-navigation-menu@1.2.14': + resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-one-time-password-field@0.1.8': + resolution: {integrity: sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-password-toggle-field@0.1.3': + resolution: {integrity: sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.1.7': + resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.10': + resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.7': + resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.3.6': + resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toast@1.2.15': + resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle-group@1.1.11': + resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toolbar@1.1.11': + resolution: {integrity: sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@rolldown/pluginutils@1.0.0-rc.3': + resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@tabler/icons-react@3.38.0': + resolution: {integrity: sha512-kR5wv+m4+GgmnSszg3rQd6SrTFAQ/XnQC/yTwIfuRJSfqB12KoIC7fPbIijFgOHTFlBN5DARnN0IVrR7KYG6/A==} + peerDependencies: + react: '>= 16' + + '@tabler/icons@3.38.0': + resolution: {integrity: sha512-FdETQSpQ3lN7BEjEUzjKhsfTDCamrvMDops4HEMphTm3DmkIFpThoODn8XXZ8Q9MhjshIvphIYVHHB7zpq167w==} + + '@tailwindcss/node@4.2.1': + resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} + + '@tailwindcss/oxide-android-arm64@4.2.1': + resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.1': + resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + resolution: {integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.1': + resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==} + engines: {node: '>= 20'} + + '@tailwindcss/typography@0.5.19': + resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + + '@tailwindcss/vite@4.2.1': + resolution: {integrity: sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@tanstack/history@1.161.4': + resolution: {integrity: sha512-Kp/WSt411ZWYvgXy6uiv5RmhHrz9cAml05AQPrtdAp7eUqvIDbMGPnML25OKbzR3RJ1q4wgENxDTvlGPa9+Mww==} + engines: {node: '>=20.19'} + + '@tanstack/query-core@5.90.20': + resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==} + + '@tanstack/react-query@5.90.21': + resolution: {integrity: sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==} + peerDependencies: + react: ^18 || ^19 + + '@tanstack/react-router-devtools@1.163.3': + resolution: {integrity: sha512-42VMkV/2Z8ro7xzblPBRNZIEmCNXMzm2jD68G52p2qhjXm38wGpg46qneAESN9FtTQeVWk5aSXs47/jt7lkzmw==} + engines: {node: '>=20.19'} + peerDependencies: + '@tanstack/react-router': ^1.163.3 + '@tanstack/router-core': ^1.163.3 + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + peerDependenciesMeta: + '@tanstack/router-core': + optional: true + + '@tanstack/react-router@1.163.3': + resolution: {integrity: sha512-hheBbFVb+PbxtrWp8iy6+TTRTbhx3Pn6hKo8Tv/sWlG89ZMcD1xpQWzx8ukHN9K8YWbh5rdzt4kv6u8X4kB28Q==} + engines: {node: '>=20.19'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-store@0.9.1': + resolution: {integrity: sha512-YzJLnRvy5lIEFTLWBAZmcOjK3+2AepnBv/sr6NZmiqJvq7zTQggyK99Gw8fqYdMdHPQWXjz0epFKJXC+9V2xDA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/router-core@1.163.3': + resolution: {integrity: sha512-jPptiGq/w3nuPzcMC7RNa79aU+b6OjaDzWJnBcV2UAwL4ThJamRS4h42TdhJE+oF5yH9IEnCOGQdfnbw45LbfA==} + engines: {node: '>=20.19'} + + '@tanstack/router-devtools-core@1.163.3': + resolution: {integrity: sha512-FPi64IP0PT1IkoeyGmsD6JoOVOYAb85VCH0mUbSdD90yV0+1UB6oT+D7K27GXkp7SXMJN3mBEjU5rKnNnmSCIw==} + engines: {node: '>=20.19'} + peerDependencies: + '@tanstack/router-core': ^1.163.3 + csstype: ^3.0.10 + peerDependenciesMeta: + csstype: + optional: true + + '@tanstack/router-generator@1.164.0': + resolution: {integrity: sha512-Uiyj+RtW0kdeqEd8NEd3Np1Z2nhJ2xgLS8U+5mTvFrm/s3xkM2LYjJHoLzc6am7sKPDsmeF9a4/NYq3R7ZJP0Q==} + engines: {node: '>=20.19'} + + '@tanstack/router-plugin@1.164.0': + resolution: {integrity: sha512-cZPsEMhqzyzmuPuDbsTAzBZaT+cj0pGjwdhjxJfPCM06Ax8v4tFR7n/Ug0UCwnNAUEmKZWN3lA9uT+TxXnk9PQ==} + engines: {node: '>=20.19'} + peerDependencies: + '@rsbuild/core': '>=1.0.2' + '@tanstack/react-router': ^1.163.3 + vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' + vite-plugin-solid: ^2.11.10 + webpack: '>=5.92.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@tanstack/react-router': + optional: true + vite: + optional: true + vite-plugin-solid: + optional: true + webpack: + optional: true + + '@tanstack/router-utils@1.161.4': + resolution: {integrity: sha512-r8TpjyIZoqrXXaf2DDyjd44gjGBoyE+/oEaaH68yLI9ySPO1gUWmQENZ1MZnmBnpUGN24NOZxdjDLc8npK0SAw==} + engines: {node: '>=20.19'} + + '@tanstack/store@0.9.1': + resolution: {integrity: sha512-+qcNkOy0N1qSGsP7omVCW0SDrXtaDcycPqBDE726yryiA5eTDFpjBReaYjghVJwNf1pcPMyzIwTGlYjCSQR0Fg==} + + '@tanstack/virtual-file-routes@1.161.4': + resolution: {integrity: sha512-42WoRePf8v690qG8yGRe/YOh+oHni9vUaUUfoqlS91U2scd3a5rkLtVsc6b7z60w3RogH0I00vdrC5AaeiZ18w==} + engines: {node: '>=20.19'} + + '@trivago/prettier-plugin-sort-imports@6.0.2': + resolution: {integrity: sha512-3DgfkukFyC/sE/VuYjaUUWoFfuVjPK55vOFDsxD56XXynFMCZDYFogH2l/hDfOsQAm1myoU/1xByJ3tWqtulXA==} + engines: {node: '>= 20'} + peerDependencies: + '@vue/compiler-sfc': 3.x + prettier: 2.x - 3.x + prettier-plugin-ember-template-tag: '>= 2.0.0' + prettier-plugin-svelte: 3.x + svelte: 4.x || 5.x + peerDependenciesMeta: + '@vue/compiler-sfc': + optional: true + prettier-plugin-ember-template-tag: + optional: true + prettier-plugin-svelte: + optional: true + svelte: + optional: true + + '@ts-morph/common@0.27.0': + resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@24.11.0': + resolution: {integrity: sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/validate-npm-package-name@4.0.2': + resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} + + '@typescript-eslint/eslint-plugin@8.56.1': + resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.56.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.56.1': + resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.56.1': + resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.56.1': + resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.56.1': + resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.56.1': + resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.56.1': + resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.56.1': + resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.56.1': + resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.56.1': + resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@vitejs/plugin-react@5.1.4': + resolution: {integrity: sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + + babel-dead-code-elimination@1.0.12: + resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.0: + resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + engines: {node: '>=6.0.0'} + hasBin: true + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001775: + resolution: {integrity: sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-es@2.0.0: + resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + diff@8.0.3: + resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} + engines: {node: '>=0.3.1'} + + dotenv@17.3.1: + resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eciesjs@0.4.17: + resolution: {integrity: sha512-TOOURki4G7sD1wDCjj7NfLaXZZ49dFOeEb5y39IXpb8p0hRzVvfvzZHOi5JcT+PpyAbi/Y+lxPb8eTag2WYH8w==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.302: + resolution: {integrity: sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + enhanced-resolve@5.20.0: + resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} + engines: {node: '>=10.13.0'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-react-hooks@7.0.1: + resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react-refresh@0.4.26: + resolution: {integrity: sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==} + peerDependencies: + eslint: '>=8.40' + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.3: + resolution: {integrity: sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} + + express-rate-limit@8.2.1: + resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-extra@11.3.3: + resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} + engines: {node: '>=14.14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + fuzzysort@3.1.0: + resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==} + + fzf@0.5.2: + resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-own-enumerable-keys@1.0.0: + resolution: {integrity: sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==} + engines: {node: '>=14.16'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + + goober@2.1.18: + resolution: {integrity: sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==} + peerDependencies: + csstype: ^3.0.10 + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphql@16.13.0: + resolution: {integrity: sha512-uSisMYERbaB9bkA9M4/4dnqyktaEkf1kMHNKq/7DHyxVeWqHQ2mBmVqm5u6/FVHwF3iCNalKcg82Zfl+tffWoA==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + hono@4.12.3: + resolution: {integrity: sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==} + engines: {node: '>=16.9.0'} + + html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + + i18next-browser-languagedetector@8.2.1: + resolution: {integrity: sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==} + + i18next@25.8.14: + resolution: {integrity: sha512-paMUYkfWJMsWPeE/Hejcw+XLhHrQPehem+4wMo+uELnvIwvCG019L9sAIljwjCmEMtFQQO3YeitJY8Kctei3iA==} + peerDependencies: + typescript: ^5 + peerDependenciesMeta: + typescript: + optional: true + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + ip-address@10.0.1: + resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@3.0.0: + resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==} + engines: {node: '>=12'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-regexp@3.1.0: + resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} + engines: {node: '>=12'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + isbot@5.1.35: + resolution: {integrity: sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@3.1.5: + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} + + javascript-natural-sort@0.7.1: + resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + + jotai@2.18.0: + resolution: {integrity: sha512-XI38kGWAvtxAZ+cwHcTgJsd+kJOJGf3OfL4XYaXWZMZ7IIY8e53abpIHvtVn1eAgJ5dlgwlGFnP4psrZ/vZbtA==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@babel/core': '>=7.0.0' + '@babel/template': '>=7.0.0' + '@types/react': '>=17.0.0' + react: '>=17.0.0' + peerDependenciesMeta: + '@babel/core': + optional: true + '@babel/template': + optional: true + '@types/react': + optional: true + react: + optional: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.31.1: + resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.31.1: + resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.31.1: + resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.31.1: + resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.31.1: + resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.31.1: + resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.31.1: + resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.31.1: + resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.31.1: + resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.31.1: + resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.31.1: + resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.31.1: + resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.17.23: + resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msw@2.12.10: + resolution: {integrity: sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-treeify@1.1.33: + resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} + engines: {node: '>= 10'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@8.2.0: + resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} + engines: {node: '>=18'} + + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + + parse-imports-exports@0.2.4: + resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + parse-statements@1.0.11: + resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-plugin-tailwindcss@0.7.2: + resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==} + engines: {node: '>=20.19'} + peerDependencies: + '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-hermes': '*' + '@prettier/plugin-oxc': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@trivago/prettier-plugin-sort-imports': '*' + '@zackad/prettier-plugin-twig': '*' + prettier: ^3.0 + prettier-plugin-astro: '*' + prettier-plugin-css-order: '*' + prettier-plugin-jsdoc: '*' + prettier-plugin-marko: '*' + prettier-plugin-multiline-arrays: '*' + prettier-plugin-organize-attributes: '*' + prettier-plugin-organize-imports: '*' + prettier-plugin-sort-imports: '*' + prettier-plugin-svelte: '*' + peerDependenciesMeta: + '@ianvs/prettier-plugin-sort-imports': + optional: true + '@prettier/plugin-hermes': + optional: true + '@prettier/plugin-oxc': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + '@zackad/prettier-plugin-twig': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-marko: + optional: true + prettier-plugin-multiline-arrays: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-sort-imports: + optional: true + prettier-plugin-svelte: + optional: true + + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} + hasBin: true + + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + radix-ui@1.4.3: + resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + peerDependencies: + react: ^19.2.4 + + react-i18next@16.5.4: + resolution: {integrity: sha512-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g==} + peerDependencies: + i18next: '>= 25.6.2' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-textarea-autosize@8.5.9: + resolution: {integrity: sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==} + engines: {node: '>=10'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + recast@0.23.11: + resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} + engines: {node: '>= 4'} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + rettime@0.10.1: + resolution: {integrity: sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + seroval-plugins@1.5.0: + resolution: {integrity: sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.0: + resolution: {integrity: sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==} + engines: {node: '>=10'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shadcn@3.8.5: + resolution: {integrity: sha512-jPRx44e+eyeV7xwY3BLJXcfrks00+M0h5BGB9l6DdcBW4BpAj4x3lVmVy0TXPEs2iHEisxejr62sZAAw6B1EVA==} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + stringify-object@5.0.0: + resolution: {integrity: sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==} + engines: {node: '>=14.16'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + tailwind-merge@3.5.0: + resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + + tailwindcss@4.2.1: + resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tldts-core@7.0.23: + resolution: {integrity: sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==} + + tldts@7.0.23: + resolution: {integrity: sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==} + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-morph@26.0.0: + resolution: {integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@5.4.4: + resolution: {integrity: sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==} + engines: {node: '>=20'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typescript-eslint@8.56.1: + resolution: {integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-composed-ref@1.4.0: + resolution: {integrity: sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-isomorphic-layout-effect@1.2.1: + resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-latest@1.3.0: + resolution: {integrity: sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + validate-npm-package-name@7.0.2: + resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==} + engines: {node: ^20.17.0 || >=22.9.0} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.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 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@antfu/ni@25.0.0': + dependencies: + ansis: 4.2.0 + fzf: 0.5.2 + package-manager-detector: 1.6.0 + tinyexec: 1.0.2 + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.28.6': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@dotenvx/dotenvx@1.52.0': + dependencies: + commander: 11.1.0 + dotenv: 17.3.1 + eciesjs: 0.4.17 + execa: 5.1.1 + fdir: 6.5.0(picomatch@4.0.3) + ignore: 5.3.2 + object-treeify: 1.1.33 + picomatch: 4.0.3 + which: 4.0.0 + + '@ecies/ciphers@0.2.5(@noble/ciphers@1.3.0)': + dependencies: + '@noble/ciphers': 1.3.0 + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.3(jiti@2.6.1))': + dependencies: + eslint: 9.39.3(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.4': + dependencies: + ajv: 6.14.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.3': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@floating-ui/core@1.7.4': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.5': + dependencies: + '@floating-ui/core': 1.7.4 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/dom': 1.7.5 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@floating-ui/utils@0.2.10': {} + + '@fontsource-variable/inter@5.2.8': {} + + '@hono/node-server@1.19.9(hono@4.12.3)': + dependencies: + hono: 4.12.3 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@inquirer/ansi@1.0.2': {} + + '@inquirer/confirm@5.1.21(@types/node@24.11.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/type': 3.0.10(@types/node@24.11.0) + optionalDependencies: + '@types/node': 24.11.0 + + '@inquirer/core@10.3.2(@types/node@24.11.0)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.11.0) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.11.0 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/type@3.0.10(@types/node@24.11.0)': + optionalDependencies: + '@types/node': 24.11.0 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@modelcontextprotocol/sdk@1.27.1(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.9(hono@4.12.3) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.2.1(express@5.2.1) + hono: 4.12.3 + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - supports-color + + '@mswjs/interceptors@0.41.3': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + + '@noble/ciphers@1.3.0': {} + + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.8.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-form@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-menubar@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-one-time-password-field@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-password-toggle-field@0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/react-dom': 2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/rect': 1.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toolbar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/rect@1.1.1': {} + + '@rolldown/pluginutils@1.0.0-rc.3': {} + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@tabler/icons-react@3.38.0(react@19.2.4)': + dependencies: + '@tabler/icons': 3.38.0 + react: 19.2.4 + + '@tabler/icons@3.38.0': {} + + '@tailwindcss/node@4.2.1': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.20.0 + jiti: 2.6.1 + lightningcss: 1.31.1 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.1 + + '@tailwindcss/oxide-android-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide@4.2.1': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-x64': 4.2.1 + '@tailwindcss/oxide-freebsd-x64': 4.2.1 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.1 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.1 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-x64-musl': 4.2.1 + '@tailwindcss/oxide-wasm32-wasi': 4.2.1 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 + + '@tailwindcss/typography@0.5.19(tailwindcss@4.2.1)': + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 4.2.1 + + '@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))': + dependencies: + '@tailwindcss/node': 4.2.1 + '@tailwindcss/oxide': 4.2.1 + tailwindcss: 4.2.1 + vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) + + '@tanstack/history@1.161.4': {} + + '@tanstack/query-core@5.90.20': {} + + '@tanstack/react-query@5.90.21(react@19.2.4)': + dependencies: + '@tanstack/query-core': 5.90.20 + react: 19.2.4 + + '@tanstack/react-router-devtools@1.163.3(@tanstack/react-router@1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.163.3)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/react-router': 1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/router-devtools-core': 1.163.3(@tanstack/router-core@1.163.3)(csstype@3.2.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@tanstack/router-core': 1.163.3 + transitivePeerDependencies: + - csstype + + '@tanstack/react-router@1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/history': 1.161.4 + '@tanstack/react-store': 0.9.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/router-core': 1.163.3 + isbot: 5.1.35 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + '@tanstack/react-store@0.9.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/store': 0.9.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) + + '@tanstack/router-core@1.163.3': + dependencies: + '@tanstack/history': 1.161.4 + '@tanstack/store': 0.9.1 + cookie-es: 2.0.0 + seroval: 1.5.0 + seroval-plugins: 1.5.0(seroval@1.5.0) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + '@tanstack/router-devtools-core@1.163.3(@tanstack/router-core@1.163.3)(csstype@3.2.3)': + dependencies: + '@tanstack/router-core': 1.163.3 + clsx: 2.1.1 + goober: 2.1.18(csstype@3.2.3) + tiny-invariant: 1.3.3 + optionalDependencies: + csstype: 3.2.3 + + '@tanstack/router-generator@1.164.0': + dependencies: + '@tanstack/router-core': 1.163.3 + '@tanstack/router-utils': 1.161.4 + '@tanstack/virtual-file-routes': 1.161.4 + prettier: 3.8.1 + recast: 0.23.11 + source-map: 0.7.6 + tsx: 4.21.0 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@tanstack/router-plugin@1.164.0(@tanstack/react-router@1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@tanstack/router-core': 1.163.3 + '@tanstack/router-generator': 1.164.0 + '@tanstack/router-utils': 1.161.4 + '@tanstack/virtual-file-routes': 1.161.4 + chokidar: 3.6.0 + unplugin: 2.3.11 + zod: 3.25.76 + optionalDependencies: + '@tanstack/react-router': 1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) + transitivePeerDependencies: + - supports-color + + '@tanstack/router-utils@1.161.4': + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + ansis: 4.2.0 + babel-dead-code-elimination: 1.0.12 + diff: 8.0.3 + pathe: 2.0.3 + tinyglobby: 0.2.15 + transitivePeerDependencies: + - supports-color + + '@tanstack/store@0.9.1': {} + + '@tanstack/virtual-file-routes@1.161.4': {} + + '@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.1)': + dependencies: + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.0 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + javascript-natural-sort: 0.7.1 + lodash-es: 4.17.23 + minimatch: 9.0.9 + parse-imports-exports: 0.2.4 + prettier: 3.8.1 + transitivePeerDependencies: + - supports-color + + '@ts-morph/common@0.27.0': + dependencies: + fast-glob: 3.3.3 + minimatch: 10.2.4 + path-browserify: 1.0.1 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + + '@types/estree@1.0.8': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/json-schema@7.0.15': {} + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + + '@types/node@24.11.0': + dependencies: + undici-types: 7.16.0 + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@types/statuses@2.0.6': {} + + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@types/validate-npm-package-name@4.0.2': {} + + '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/type-utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.1 + eslint: 9.39.3(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3 + eslint: 9.39.3(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.56.1': + dependencies: + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + + '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.3(jiti@2.6.1) + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.56.1': {} + + '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3 + minimatch: 10.2.4 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + eslint: 9.39.3(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.56.1': + dependencies: + '@typescript-eslint/types': 8.56.1 + eslint-visitor-keys: 5.0.1 + + '@ungap/structured-clone@1.3.0': {} + + '@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-rc.3 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) + transitivePeerDependencies: + - supports-color + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + agent-base@7.1.4: {} + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansis@4.2.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + + babel-dead-code-elimination@1.0.12: + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + bail@2.0.2: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.0: {} + + binary-extensions@2.3.0: {} + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.0 + caniuse-lite: 1.0.30001775 + electron-to-chromium: 1.5.302 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001775: {} + + ccount@2.0.1: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@2.9.2: {} + + cli-width@4.1.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clsx@2.1.1: {} + + code-block-writer@13.0.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + comma-separated-tokens@2.0.3: {} + + commander@11.1.0: {} + + commander@14.0.3: {} + + concat-map@0.0.1: {} + + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie-es@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cookie@1.1.1: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cosmiconfig@9.0.0(typescript@5.9.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.9.3 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + data-uri-to-buffer@4.0.1: {} + + dayjs@1.11.19: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + dedent@1.7.2: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + diff@8.0.3: {} + + dotenv@17.3.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eciesjs@0.4.17: + dependencies: + '@ecies/ciphers': 0.2.5(@noble/ciphers@1.3.0) + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.302: {} + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + encodeurl@2.0.0: {} + + enhanced-resolve@5.20.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + env-paths@2.2.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-config-prettier@10.1.8(eslint@9.39.3(jiti@2.6.1)): + dependencies: + eslint: 9.39.3(jiti@2.6.1) + + eslint-plugin-react-hooks@7.0.1(eslint@9.39.3(jiti@2.6.1)): + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.0 + eslint: 9.39.3(jiti@2.6.1) + hermes-parser: 0.25.1 + zod: 4.3.6 + zod-validation-error: 4.0.2(zod@4.3.6) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.4.26(eslint@9.39.3(jiti@2.6.1)): + dependencies: + eslint: 9.39.3(jiti@2.6.1) + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.3(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.4 + '@eslint/js': 9.39.3 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.14.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-util-is-identifier-name@3.0.0: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@9.6.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + + express-rate-limit@8.2.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.0.1 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + extend@3.0.2: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.0: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs-extra@11.3.3: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + fuzzysort@3.1.0: {} + + fzf@0.5.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.5.0: {} + + 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 + + get-nonce@1.0.1: {} + + get-own-enumerable-keys@1.0.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@6.0.1: {} + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@16.5.0: {} + + goober@2.1.18(csstype@3.2.3): + dependencies: + csstype: 3.2.3 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphql@16.13.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + headers-polyfill@4.0.3: {} + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + hono@4.12.3: {} + + html-parse-stringify@3.0.1: + dependencies: + void-elements: 3.1.0 + + html-url-attributes@3.0.1: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + human-signals@8.0.1: {} + + i18next-browser-languagedetector@8.2.1: + dependencies: + '@babel/runtime': 7.28.6 + + i18next@25.8.14(typescript@5.9.3): + dependencies: + '@babel/runtime': 7.28.6 + optionalDependencies: + typescript: 5.9.3 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inherits@2.0.4: {} + + inline-style-parser@0.2.7: {} + + ip-address@10.0.1: {} + + ipaddr.js@1.9.1: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-arrayish@0.2.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-decimal@2.0.1: {} + + is-docker@3.0.0: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-hexadecimal@2.0.1: {} + + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-interactive@2.0.0: {} + + is-node-process@1.2.0: {} + + is-number@7.0.0: {} + + is-obj@3.0.0: {} + + is-plain-obj@4.1.0: {} + + is-promise@4.0.0: {} + + is-regexp@3.1.0: {} + + is-stream@2.0.1: {} + + is-stream@4.0.1: {} + + is-unicode-supported@1.3.0: {} + + is-unicode-supported@2.1.0: {} + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + isbot@5.1.35: {} + + isexe@2.0.0: {} + + isexe@3.1.5: {} + + javascript-natural-sort@0.7.1: {} + + jiti@2.6.1: {} + + jose@6.1.3: {} + + jotai@2.18.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4): + optionalDependencies: + '@babel/core': 7.29.0 + '@babel/template': 7.28.6 + '@types/react': 19.2.14 + react: 19.2.4 + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@3.0.3: {} + + kleur@4.1.5: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.31.1: + optional: true + + lightningcss-darwin-arm64@1.31.1: + optional: true + + lightningcss-darwin-x64@1.31.1: + optional: true + + lightningcss-freebsd-x64@1.31.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.31.1: + optional: true + + lightningcss-linux-arm64-gnu@1.31.1: + optional: true + + lightningcss-linux-arm64-musl@1.31.1: + optional: true + + lightningcss-linux-x64-gnu@1.31.1: + optional: true + + lightningcss-linux-x64-musl@1.31.1: + optional: true + + lightningcss-win32-arm64-msvc@1.31.1: + optional: true + + lightningcss-win32-x64-msvc@1.31.1: + optional: true + + lightningcss@1.31.1: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.31.1 + lightningcss-darwin-arm64: 1.31.1 + lightningcss-darwin-x64: 1.31.1 + lightningcss-freebsd-x64: 1.31.1 + lightningcss-linux-arm-gnueabihf: 1.31.1 + lightningcss-linux-arm64-gnu: 1.31.1 + lightningcss-linux-arm64-musl: 1.31.1 + lightningcss-linux-x64-gnu: 1.31.1 + lightningcss-linux-x64-musl: 1.31.1 + lightningcss-win32-arm64-msvc: 1.31.1 + lightningcss-win32-x64-msvc: 1.31.1 + + lines-and-columns@1.2.4: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.17.23: {} + + lodash.merge@4.6.2: {} + + log-symbols@6.0.0: + dependencies: + chalk: 5.6.2 + is-unicode-supported: 1.3.0 + + longest-streak@3.1.0: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + markdown-table@3.0.4: {} + + math-intrinsics@1.1.0: {} + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mimic-fn@2.1.0: {} + + mimic-function@5.0.1: {} + + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + ms@2.1.3: {} + + msw@2.12.10(@types/node@24.11.0)(typescript@5.9.3): + dependencies: + '@inquirer/confirm': 5.1.21(@types/node@24.11.0) + '@mswjs/interceptors': 0.41.3 + '@open-draft/deferred-promise': 2.2.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.13.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.10.1 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.0 + type-fest: 5.4.4 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + + mute-stream@2.0.0: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + negotiator@1.0.0: {} + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-releases@2.0.27: {} + + normalize-path@3.0.0: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-treeify@1.1.33: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + open@11.0.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@8.2.0: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + outvariant@1.4.3: {} + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-manager-detector@1.6.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + + parse-imports-exports@0.2.4: + dependencies: + parse-statements: 1.0.11 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-ms@4.0.0: {} + + parse-statements@1.0.11: {} + + parseurl@1.3.3: {} + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-to-regexp@6.3.0: {} + + path-to-regexp@8.3.0: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pkce-challenge@5.0.1: {} + + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + powershell-utils@0.1.0: {} + + prelude-ls@1.2.1: {} + + prettier-plugin-tailwindcss@0.7.2(@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.1))(prettier@3.8.1): + dependencies: + prettier: 3.8.1 + optionalDependencies: + '@trivago/prettier-plugin-sort-imports': 6.0.2(prettier@3.8.1) + + prettier@3.8.1: {} + + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + property-information@7.1.0: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + punycode@2.3.1: {} + + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + + queue-microtask@1.2.3: {} + + radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-form': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-menubar': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-one-time-password-field': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-password-toggle-field': 0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-progress': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-radio-group': 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slider': 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-switch': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toast': 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toolbar': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + react-dom@19.2.4(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + + react-i18next@16.5.4(i18next@25.8.14(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3): + dependencies: + '@babel/runtime': 7.28.6 + html-parse-stringify: 3.0.1 + i18next: 25.8.14(typescript@5.9.3) + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + react-dom: 19.2.4(react@19.2.4) + typescript: 5.9.3 + + react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.4): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.2.14 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.4 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + react-refresh@0.18.0: {} + + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4): + dependencies: + react: 19.2.4 + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.4): + dependencies: + react: 19.2.4 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.4) + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.4) + use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + + react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.4): + dependencies: + get-nonce: 1.0.1 + react: 19.2.4 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + react-textarea-autosize@8.5.9(@types/react@19.2.14)(react@19.2.4): + dependencies: + '@babel/runtime': 7.28.6 + react: 19.2.4 + use-composed-ref: 1.4.0(@types/react@19.2.14)(react@19.2.4) + use-latest: 1.3.0(@types/react@19.2.14)(react@19.2.4) + transitivePeerDependencies: + - '@types/react' + + react@19.2.4: {} + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + recast@0.23.11: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + rettime@0.10.1: {} + + reusify@1.1.0: {} + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + run-applescript@7.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.7.4: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + seroval-plugins@1.5.0(seroval@1.5.0): + dependencies: + seroval: 1.5.0 + + seroval@1.5.0: {} + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shadcn@3.8.5(@types/node@24.11.0)(typescript@5.9.3): + dependencies: + '@antfu/ni': 25.0.0 + '@babel/core': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) + '@dotenvx/dotenvx': 1.52.0 + '@modelcontextprotocol/sdk': 1.27.1(zod@3.25.76) + '@types/validate-npm-package-name': 4.0.2 + browserslist: 4.28.1 + commander: 14.0.3 + cosmiconfig: 9.0.0(typescript@5.9.3) + dedent: 1.7.2 + deepmerge: 4.3.1 + diff: 8.0.3 + execa: 9.6.1 + fast-glob: 3.3.3 + fs-extra: 11.3.3 + fuzzysort: 3.1.0 + https-proxy-agent: 7.0.6 + kleur: 4.1.5 + msw: 2.12.10(@types/node@24.11.0)(typescript@5.9.3) + node-fetch: 3.3.2 + open: 11.0.0 + ora: 8.2.0 + postcss: 8.5.6 + postcss-selector-parser: 7.1.1 + prompts: 2.4.2 + recast: 0.23.11 + stringify-object: 5.0.0 + tailwind-merge: 3.5.0 + ts-morph: 26.0.0 + tsconfig-paths: 4.2.0 + validate-npm-package-name: 7.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - '@cfworker/json-schema' + - '@types/node' + - babel-plugin-macros + - supports-color + - typescript + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sisteransi@1.0.5: {} + + sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + source-map-js@1.2.1: {} + + source-map@0.6.1: {} + + source-map@0.7.6: {} + + space-separated-tokens@2.0.2: {} + + statuses@2.0.2: {} + + stdin-discarder@0.2.2: {} + + strict-event-emitter@0.5.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + stringify-object@5.0.0: + dependencies: + get-own-enumerable-keys: 1.0.0 + is-obj: 3.0.0 + is-regexp: 3.1.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@3.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-final-newline@4.0.0: {} + + strip-json-comments@3.1.1: {} + + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tagged-tag@1.0.0: {} + + tailwind-merge@3.5.0: {} + + tailwindcss@4.2.1: {} + + tapable@2.3.0: {} + + tiny-invariant@1.3.3: {} + + tiny-warning@1.0.3: {} + + tinyexec@1.0.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tldts-core@7.0.23: {} + + tldts@7.0.23: + dependencies: + tldts-core: 7.0.23 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.23 + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-morph@26.0.0: + dependencies: + '@ts-morph/common': 0.27.0 + code-block-writer: 13.0.3 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.3 + get-tsconfig: 4.13.6 + optionalDependencies: + fsevents: 2.3.3 + + tw-animate-css@1.4.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@5.4.4: + dependencies: + tagged-tag: 1.0.0 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typescript-eslint@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.3(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + + unicorn-magic@0.3.0: {} + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.16.0 + picomatch: 4.0.3 + webpack-virtual-modules: 0.6.2 + + until-async@3.0.2: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.4): + dependencies: + react: 19.2.4 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + use-composed-ref@1.4.0(@types/react@19.2.14)(react@19.2.4): + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + use-isomorphic-layout-effect@1.2.1(@types/react@19.2.14)(react@19.2.4): + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + use-latest@1.3.0(@types/react@19.2.14)(react@19.2.4): + dependencies: + react: 19.2.4 + use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + + use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.4): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.4 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + use-sync-external-store@1.6.0(react@19.2.4): + dependencies: + react: 19.2.4 + + util-deprecate@1.0.2: {} + + validate-npm-package-name@7.0.2: {} + + vary@1.1.2: {} + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0): + dependencies: + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.11.0 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.31.1 + tsx: 4.21.0 + + void-elements@3.1.0: {} + + web-streams-polyfill@3.3.3: {} + + webpack-virtual-modules@0.6.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@4.0.0: + dependencies: + isexe: 3.1.5 + + word-wrap@1.2.5: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.1 + powershell-utils: 0.1.0 + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + yoctocolors-cjs@2.1.3: {} + + yoctocolors@2.1.2: {} + + zod-to-json-schema@3.25.1(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod-validation-error@4.0.2(zod@4.3.6): + dependencies: + zod: 4.3.6 + + zod@3.25.76: {} + + zod@4.3.6: {} + + zwitch@2.0.4: {} diff --git a/web/frontend/prettier.config.js b/web/frontend/prettier.config.js new file mode 100644 index 000000000..492ef1dd7 --- /dev/null +++ b/web/frontend/prettier.config.js @@ -0,0 +1,17 @@ +// @ts-check + +/** @type {import('prettier').Config} */ +const config = { + semi: false, + printWidth: 80, + tabWidth: 2, + importOrder: ["", "", "^@/", "^[./]"], + importOrderSeparation: true, + importOrderSortSpecifiers: true, + plugins: [ + "@trivago/prettier-plugin-sort-imports", + "prettier-plugin-tailwindcss", + ], +} + +export default config diff --git a/web/frontend/public/apple-touch-icon.png b/web/frontend/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d881c64aff4b51594db0729790e18b92ac9207f5 GIT binary patch literal 24799 zcmV)`Kz_f8P)_ulq}r3<2QEu3!sBV*`z*YBctSPou{CqcMqTDq;bRLM$;V zYAh(lipGXY6Og{WzjE)L|99@&UA7klS!M5U=H8hzXUd)V=FFKhci$RxU0pr!9qNHD zK!1k@v+GrWE$;*V@LlMEE>1#-jK6z{@zX#uIjc$E8YTycL90}bZVPLPMF~gniw54YyJ*_Z~hWn%!o(9bg!B;XUD<&WY|aFM3S z5XgIC%ybd_Reanp*1i@>%lLBWS{Vc`Ru%qi~owj zuLLkCav_))GMJZ$V9L%vy6G@dh5l=~fu~3NmsPI4#qsbm3%i!! z9SE=`4NA4z2yNSi7S61)sRfTa^gOebv9cehCdGne#RjKQA?q#-pgx?aTI zZaw!Pk;Klvo7@?`9ZPuK&h%CE#56KRafKqiL}9=>>b^sL3xZ+u|U zwCMATrcHiq=_5D&Ogs!1*z3siCA|pDA>xGZ-r+}AtK>#!yi8|o)OWE>6|w%i8H+0b z>kt`50$dSE*k3Pw?E0@-dfzXmURAz$`ppmc;n>O4LaruX0rQ7B5zcl@y{aB8=xxVk7v3UCATRxk9-C89y_KW@*Em$-}ZWr;-Vf|mQZV|z_h@}1F>#P!u z>)D*~EU#(zW+1w7tnJXfD4D5 zdD8%fJC*AK^fxdZ2L1)CCIU)F58f3^W?oyn*+S!M#~)t5+u*aVABJeO*|Ayr$PMpv z19>R~F<7LD!BwOeF zXF5qczO&5;R05F$QZHzirpL{HQ{krfA!kO93Wq}OpoMd-LUhn%%Sc3gk8^yj%C}^ z>oYBXFdBo8Ka&=ty5Sfxm-IdR@+`(XjnkLxG>!Q#HY5}`0|YiWaib}2!9sl6BA7({ z9I7TdCJLm}#c>|pd674=xqew1`(!HVPk>S%z^>vZPNnsPngwGG9ceNmMUq15zMrSU z&ELI5`gltJ86NDvLY_aaG1uZp@ex;s$7<+1<@njh)`UC7Lp!fiG~~P52y395HU%ie z$4czJJC}Vld(ootq!s5RN5d%@F;i=s#pb&vjNK7p1A3z0NiNh;=#%?iby;HQ~JKG53W1R&mQZHyn4*wLPRJirKW2Ll?!@br*mK?dKb7ek%Gbpd({ z55xAWu}&;LN>w9!$J3w|hg7tM`-BM-9AE5_sbul|kg3U^qSU|7oOzlDD-YJt!hRA* z)r9LFzQ`O#ScVl5oRXf_{S?NVbvvJa%Rp{MFRe*T7~i~76R!6V%d3b4mHrX&v01+a zWVxH{x&Xb&UDv*hvw48xTHFm)IDoJLrcvkI6|G$Fr|rH!b=1QH7O|f$dFY0vLAZsR z^@S1`i6GfxEC4~hYuU7$R#Hdc29iXJ9&E$Zp=aIn6HJ()`aOv$M>AbN3Ay*fBK56@ z#dAFV{v6F*KBU?LEEa|nCfp3mRw41-wo1L*RHa`&aw8?Ay^jD;lU=rT@YpVA-gNS? z^M04ppd)ygGUTl2J{*+W0MmnMaado?a}2iZJUDuK7pNWN+IjedXp@hJx@WC^kW%%J zOeYX+gwMN&rY(GJ=-JU*hMqO~2G%<*?BZhqjsz?pSjhV=!zy+?qi3Xeba=Yi}6>&;|V|x zRZv-x`L35%Yx6}*SERDEifI9$iL5?enp39(lODJQ_uiX~CCaX4nZF@{I=xk?dI;Ne z9bj%2;4IPuU6^IO?^!c(N&M5=jUSU5I4&SiHG}xE9`nW;q;ACs8?+T`--fEzPea3! zn8Z!iO9X4Bh0PPlSR&(gEB1M6r85ZfpVI3Qwac#gfRgW=FaLSndWKtd+m&Tj6PU}D zNq`=b?!H+6a;BEQ>GRX!1hgHj2?D8t*O9IfE9AEd=rDEVzHQ}QKow-!!_h@cr$t{~ zJpIQ1E_*Ec-twoT`j;zhWyPqxVq*onDWiogPdc7TdW~*%-v#KcGXz$b|1Tbku37wW z^w}kk-gwuN={NrU^T(p|6;!iT;&!lIl=x~!Q$WKQX%o;LEWQDIN!iMNVlh;S7WjPm zgHcskG`iJ&7oflP5Oj_LL_C7ZQauJ$VyhOCE!Qw7avBR)O9|`pxhDW|O-d(lU4ZTc zgRnW8;KQi|BmgvkT>35JqS25M`x7mk`g0;8Y0ql7v-mDRcb1X(n(d%Ys|PE#rkJ#v z=VpHlUW74#3;{3S+E-fodqgI071ssmttB#F*m+zRpgYf4e65z)kgFg|l|kcnDzJI)yJqb^klOP1Ql-il9=vx_;m&jK z0(9pY3+b`{MY-J%Jbz@5gC-u?W6Zfn1>ybAY?nT4Bt)r9#Uu4Ts(z$GoAK_sqJNvX zE4UKSH27*pGTdp*b*Qsk=l!$$n2CFXylCM{dLH#=38XY{)h)0I#zsRJa%>ugPsqlo3p44%t(99>PL%nq zy^3+QZn#Fw<7!i9yi>U@KySt2$QyJ1h@Pvn@1x=P10y(_O=(UV=Mbi+J7Qk%xi;rM zmD)8rEZH|YBTg3J2jfXoN5h{e7qJWJ$9jyJcw|7Se-p_8XNHo* zT0LaZAXNJHNJ~zJBTtIOjSxAU3Ho~)*?`TX_OKhDXTibW(?kBVy$=5ISf&eoof~}C zZ+0=p{4zzbrC~pG@$5}F{7L2CviB}PZ`uBBw)7qcP8tJ?DMZa+F=G1n&co&B9EZ0a zy%|sb=`#HB>T_}DRp$iZw;#C~S5F#;fqi?xh%gIw5pnMAdFYP`#Er!pOK`%bzHb&A zd|T1)kV(CJtj>8%M6PGX{ylQDWXxVeaT)8n>&mln+r_8hjAIVOkb!*>35PRS+GuP2 z$Ed>}0o43)?AU0?5${}B(I08_X&1czvq!IALEcX3x&ZwR56kukPRJlKf6HojAV4v8 z^lq5;+n?d`^Twf1&u%0)biGNtWa9Cd`kS9nV((I$xFbAoW{-U*DadMRy%=y{;li4H z>d29VhwDUeqGJq>IA8?si(Y_7|L>Q$^8E4m(eVf2{8JCdAFnwVGw%EquDjqww(VhpaJe=iW*bW#j785DOb+Rlsa2`e{zPbSY4G&JtWM3hG{w1~Suw75wdBs^6 zF?0urkjl1{mQ~{QcRt0dZ+?u@avuJXrYU<4+Yt}m`cn+su^%bwWOqr!FJY}98a38P zA3zKQ6$EuzUeDZpA)-Is6&^!1=I(1J;^0xcB78(^&eg|eey>dZsa>n)O4Dbe8(FymR5rP$NXv-@uz>8 zEg9(%+ca3IKl@wWo|iv7c;uB2j^F2s z>3?~qddNaYGFMXC{fof5inuw}O~i)t{lqZ_CmuGc4ovsXd>L~-_yV5qGZ_en9B$G= zsDJrPv(_S$D#L-?*cn40GVUj0%4TRWF&ZE#OPuf#89ld?oPIlCSz#Pstjx#!|1H8_ zANwySOzPWx#~7rAc^0FY=;I0fl>?w(?>u~O^sb};UMx=a8J_k1dj_uXKMaVS{1$XV zCvRh?Y~Jwbj0gcdV(%*+nOHVr=R2IN>?a+^yd@_56dd^l(K?JEmfFT!pRzQO2%@>! z8R$>FRU=8N0gt`V(4@P*undpQnh#YGlt-Wco&OOkt5RUxd53-=f;9NpBZD}r$CO(peR(`I49rFY?`zy1s3fAM?# z?Xj0p72h}^fE0kKOF%C-=^&X55rmFL8~D=77?u}QA-!i`q~-R6aPL^jdVf3h8H_pW zMjY|uTXFwWvsqM1J+8HVv&n9Ctm>RE|9M-Rf7jTc!mSu-zVo+YgF0K)dtCiU-doJCI9xkHTMCJf!&cy+`{i`1a2nby~ml!ME>Ms;~lxUL3ib%>x_ z7A0p8+jvX59^U$RDgHL&Wqk0(ih!?n5>!;h$e02jL>N}bXFchxl*V2N7`f{p zWM`!ZEK66ehfndT5fj>|2Wz}N-bv2_SSD(&?96nWf68Gb31VCYUZ%+~l_Vb`8ciwo zDMF{er+T>4H*?E3aKelk&fb?i(0z|99zJg5RnuQ`B>X?X+$ey4F1a0Bz##99T&$!B z;)Ls{3SLXq!lP~*D=SCE`h1jpwF;$Qt%gTyhBCU;O=r#dAD4KI0TPlD+eWJ1o)x{;K+)#kB)oW3@Y7MFi3X!O+gr)kRB{^vs5Q0*}|9SlbyuWan1_O(R z9(;@+yANvhS53&FUcBdOOo?8Adnf-G&;9L6{E0`?Lw3{!0>K9Ix<5aT zbp@rG1x|VfjML`43wUd9gZt&-?Bfr>FMo72&O2o+M(#EUhLh|oO6Bt>-%GuHNs86l z2aEcbe4dJYd+ys7pskhRS3J;fugf1g+0V;*nI~;e62xOTTzT}(_l33axq(Z@VyG@D zLD`yhC|S7*73Sc$V5qLIaaMJ#ELcf z_-N5e{O!?~a6Bz3@3U;GGI2I$dfx#Rzi;uElR8k}H_h7oAbvBg_(p3PcIEv!Bd>Yn zC+SXjh7$_u7ifn<;Gl)eQV&j0ji{vBP)-S4v2H!8i%Q_ebi6J6Wa^p9PJK2NX!9;2 zh;-`#aUAHz?)f?Y!+AH{gL`Mbg7x_&#L~th6y)m)N^t+bUd08`DR_bY^;^h^AT4AXt z*XQn-tR{@;N*+%z?FAAf~)R)gyB1J z;F*(g-Ct(llOk8ipL%k$%t|DX13(91AF%Wq?tl-`be7qxKlI7A1vz6petz?#C3xsRbMW9Z zbD&}UwnGy|Ou(Dbqi^s;2QTZpRFMiH4j>{cxTf3z;J0l2uPn@=h263aDVsa(ZPMCs zR{Ea(`z-xo^npLTb;R~3T)JPMLnn>tbJ+O@4LJ7K2Mn5U>)zW>hz_CeF@eIQjlwp2 z@^>20MsTbVIadTUyf*DSm8|iK3Y4#3j|y5RR2sJm>w}D*+aZ*Z4KYoJLusM(Y@`#&%InZl(y>}n+{vmkw$oEELmc~h(Y&cw70#P_ zec`+Z{!%n=%3GLqcj{a{#opjP&uf)M$S__+j%x5;g>=t^v1hb^y;9~Zu!#=r`~+p;PJQiZBXbIiZt!PEmYAr zU94I$VD*zqind0TTi-{5=CtxPYoTVgKrbe(5nIbqN@(l3LArvwHXIN-cvHYCNHYl7 ztbRKpv(NSfct2$H=!eYS+as&*j>zDVbtpX(%G<%>19n|a+}OtBb%O2#CQwLI(O{s7 zi$=|$3DJIoe(;;IJDm2LKfAg8z6d$ao16`l=r{?Gy_j>TTU{}bdx`BB@5{|@9}|l7 z6JP*Kl=rIB@L33LL4?6-N)8y75y`7N&#I+NEWqQe2J9DB2iIHOH674lI{UM4~-aV>bl%zu8`J3phgS=BbA5r%P6ra#Nf7%a@uKvI9($-_zKBLbS(@pE`fBVP_sed^H67lRuNbS*MfO_qZ`8 zqr(XZ=>R50`iRI0#^8@`(tCH%4{rI@*l09_&2W9I0X>$TuoqAH(sH{0M~obedY9uN z7lMAO6IYkee$3N5mxmekSUMcE)Rz^!F0BRiGyAl7D7`+dGlaEG$*PhDP2n_L2@@)- zB@P@P?Z4v>Zn-=pq5p}PpOLQnjW*C}X}+Wi!_`UEAmfCYZIc}FXZyj(UCS4S-lvK) zX{WELim<>3Srq0-J4vLXwrmj1w-rcLt|zPLvlx_Y3vgL!_>VbVsTFyg{$; zoj=Y;QiZ{-fDA-CFF%=pPL^7B5tdpT+2p6}Ax8e}Tle-sW25PtTFkc+(1RvEkiK}w z*m+_?f8mSwrw98~ysGkC4XdwQzkYD#`VHdgNA;8>O6#asiJYh`;epC(BnVhv4^wJ& z=SGEjO!HEIJ<=%Un~!R{W2GB-!r8=dp)+ntRe2)PdwsX7#o{r6zYkj&p>Ma-zCcZ% zTB={VLbW?{Q$}r58Ex^Um3PGLDto;=B zL%sB=-mQ-JwKbit@s4=QK~gCkD_)Cag-W@0NO#udR`Wc9&yLi!xp}SHo68E|H@8u> zgrV3-$afxr*qhmZE$Yarc4-D5ma3L@lkixk8!LwwuSzBBo$srgL!T2`qaF0sR_t^Z z@uOsQ@`x2PZhWHAal3pnVQlsW`u3Lltpju{-M>s6xe_AbCNC*Yzgik4b{)8DY3+IX z9JE*wC31NYC0wFj7NG?q9cmMHBAiF80F5p;UKtE}BXX=F!cast`#|NVSN>OY@#QstdkCbs;_bBwoA@aZVQIZQxWwD}S7; zg4Zr!3v0D=J;8qAvfK++%)IWoHUEj$&61Vp9=r3Ji6{JO&AH>A%gfIFY=dt;d-sHc ze)-!FF|oH1}B>4@a?qD^=Q9&YqUTDLw(&)W{^-TDUUsmf~Z9Hw|_KKz

@$2fPIk2j)o@bav*INd{*~ddd;C|AU-!x4Sr;c(opW-}HRp~$ zboIpXQTh);Cj2?X{1HZuCI9x}P-uHr|0my1f9{=Q5Bc6F6NWb!|C-W+hY9YT{5I;s zd%}*vPmM9xb10np&9&^{s-j|47ngwjzFYmY_J(!f;$<6N^MoeEG32oo`85iSTWq5`dG9@h#>E5+v06LRCQDV~bU^i3O{rMf){gmLIFmU-gG@#k#Y;e0=hU_QIcp z)=oTOY-V}*Bge=+EdDSN^yHHQnfFGn2GF(C-sJslPg`>FYh%aOAAlge(*t_1%N{yN z9Oq#`a!~J5Fjo~6B3WJUP{WuJeUG|4tA?$p=Gpv%U<;N=Zv1SGil9Mz8WS9DSoD(rw$%&&CHt_q& zYITKdjljsu9@)_eIe&o2cJ=GG)GaD_e4VJ^KEl>dL3U*{ltYMfQ4A=Tvnm(JOX+CIv-fV-?I~JkLNEqw?Jl_m_ z7@X~E&pBbgNXR_H@_)gSy3@DIu_#cQY<6v5|-BEin2_B(5R_*_ABa;POCR-3I&`S8rh1FP7y1!3`b1q<|xM`uaH!{M*is zdaKVJe-P`K#tArrWpx;LhV5g%22MnJ0_OS;kJ__#RYr#kZcySDjEwrpy%|Q#c><}| zMvJPF66jx&v`{s9o&9tZ$|JGL4Chvxx`BovdFo7&rnTcy_wm zg5sAOff0z6mT@=F)tmrnvB8w+1qB7YtdSD(G*COdL>zJ2mu(%GbC9O`vgueisMPhE zoCtO#6<45_uA{kY4XS8Dt0G;cu^#Gp z)RlczZCZP0R$_@3K43Y*3TOr8u6qu{wb%R@$B#PmcGf83bhL$r>KWOF5MDca1L9!IC{S9|pPW&NAJc2u{AIR0K4 z#?ucjR7wX~RJz4W*HggxLENFqEs~WNggbJx?7>83bNfvq36c#&O-`CAgviRa%2oZ{ zr%vn=v~H{lsvid%RMyc}UQH)(kn#f1SxPaVYJ^H+q^DufPCFoj5_hMacR*&3?(hlb zNNzUv7%>#-8R-~2gcwNAfFDUiI5!s~_Zo)o-MeAe-G`D!DIE$!Cx=cFtsqqwTD{1# z5We{d7TlZE4O>BJ)y>JIw`;4J+L36qdt2Z$1`$_eLKI-I1*XnGwy@Mk0pE3@c4h@r zJKB}O0k~|Di!$rmMar)dE~PW4Y|J7Ck?fujr-{`^*<1C2#H3!_9Fu`{5CxXcelTvW zX%o=+8W(}X+?1R`Zv?*Mz~lS4R2_UuTbJPV2uRPf@c6z-0yj=6ok-7w%k@8?lRr+t zD?Q04wg2t7XRC!Xx?s+|!RM*Dw(REw^C_<9liYlc z_EJS!F2Y(EVq@$HM~uqpR1$jlglJ^%OCCJF_9X?fhz;c?un1m4Pw#3|T7NR%_XkPn zR13SnTF=+hFJCugnvES3&glU)(P^|JUL~r2x6nc!>p^0OMlI7>wO!5U7IXs8S%TFg zn3L3dlLV}rnSn$`X5c*&0`Uyr67XD+e1ck~w6B1+6ckMN(${)=7Gqh7PoU}M&&kcf zuswIh{$oaA%mI7jkbQ=ufA2i@A&^dqz5j?IICSK29JJ>yj1!_BmKtgimaQO$<3j#wA)lh$HMZPc?~hi74J-nO5@ zO6VZcz8mEgVt_5; zBw6hxtMzA#zT~)5KelQ?DYby=LVQSpIG+AvlvK%%Y=%)x6;=Ok`BFigo#h(?OPzW9 zLEB^au$@RYD65R2sH}>a(b<62;}!@GZ>~{rh3> zk-NiZU8>&b%3uS4Z3H#h{#N^Y9>x(Jzui%DxfJ`Z=heiv+(RIHbP7OEm@vcPa(xmY zIRGNYI7cZh+ljBD4pt}inAW1BBu1R}TD9>~I@Iy8%R{M%AWVC+h@_&;-+h90m9_RM z25uC1XTW;L_|`uvnXEe2w{jf)@xJFCM#7QYB< zzUtPg1PrTT+vzNIS)X9`DV;s)&I5K(i)55s$?3CI! zpdLPv;hKJDh^{)W^PYiCb=m=KQbL^WIwj=#O%~?5qDvn(q-9 z=UaOhN>9W~H_#f=a;G_z%uYD1aWOu@Uqb&eeE)Zz8#^Ei$(>isy#ADxGq1n-%Ndhr zET1{~^_4RxFIfNlT^|>|G37J$Me`o|bi->?KP;Sc|2svmPx){D8~0DmpFQPYEZouu z&V(eN@AEkcs!MMM@uc8kiJVD3H-Rp}qI@;;u3L}%`~s9$#o)7Shw4U>mJ^?wM_2Xf zTI+{UWWX*f=)Xf?k$Nro7~dWjbV9vbbq6-}^|EaY60I8}&4<`S{W=Ap2Fhy8ATyv#=(q(ZllOuL234qFJ7tV6+tK?dt;oqu-1O+1Ha=6 zaH>J*kmdin;p(rR{LQ;MiBRvd7mfrKiKo%!E&ihEHkf@?}(# z3J+mI$pk=CC-k>N2n5S~eyvm|ukxq7fzJtInI2TYB}elK^BW#QIo!<1*Uo~;ut!pNY|L@wwfGyaH>w0h$B*{jbT_ovkpk3W6&=_d{(ek3S}xAiYgtIU6W%AEW+w*Nng zBu@0<9Y%aSX~FFi)7$S@B%L(`AeZ~TR54CF<5Zk|+KHGn{wVA=XdqX71YWcO^u!<1 z3BGQ9dLgS%Z)ElDoAUkoAgk6Zjp59`eGulR&r%A5b+Es|Rg1657PFC5A&{4ojY;Fa zk8@8s0Y5t96dZrj@yN`}gRT}5a=5pk+H5&BjAekC8abU9Nu6 zS;blY$AnFeS@GoLDa)U}v;;Fx^9bO`y7P}8L13Oo06&?R9baxD=4B&tEwdj1!C+#k zmssSn4S^5BKrcXcq;NY_j9e_@{1X{=$=dVAf3j-gxPK7jXRJEwguND@d2B8awF>US z*B^Yh@QwRVwVr=4K|LLSz7vBCwdvRj&=YVw?>ZE{diO#eLD-{P4kDD4E@z5I9~8qz zAr!)hz4yeKr=Nm}6UJlGDaX?vhe;>b^QSPZ^dF4F`DgwBhaGho(!28&IccC}0PtC+ zmJKoJLDOK5?zzaz>y90E+yMi27yv?8`*!`f{^h}Y_>keu*2HOI@@v?meO*18l-h)l z*t&p@M!6Ssj;d>xwTP9~`&L8jF<#u9WQx13WYUx5^XXx&k3?xcb!INDN@&ZAdDTMB zSZQ~;e9g?8XrTb?gbB{tN#nkkmlb>1`p&@~z}wm4T_DDtU>gQly z1vT-kRR$Z*rEBuBbk#a6UAYo>F}}kwVQ5o1lSJET$TfvR5hrX zwDyqdCTnZVf{LvQ=>75s=W+1&s%@j2NTB{_p#sU~!2CIpU9_`jllNMh&j5>LMKQc& ztTtO~PuD`Ghsw-2Zt~`;M7EqnOU^#I&zh{*JwD_m4#p2HuoL^c!LGcuLfe@}aLbNt z?lH#5A50|t+3IsoxO>%)kKLY=DZ=CN*oN00npZG(;ExElNi0(>BtDfB`pmG30Cs5s1kGvJ+nieOrt0^I?xpAzfT{z`EC@n$M zDi1|1`t<7u%Z(d7iL2`0vE06LXm>PcXB za*BXBg{limU$}A&o|*GD{xf?Ho_po>z(4!i9Mayx$IDj2B`lQ>V&LmW&k!2yAa)|l zw=9blYk~BRf=huJcvj~N6DgY0tHTV0Nom$4xx3UX%_g=%Y?mEd>?*dpjgq!tvV*WL z!~L}ikIRu8YeDJ?h6$X9VC1uSdBGh#)^>q5Ew$X~LQehKoIEhB9uzM_fFC8P2YukeLs zsAos{B&6tU0P9lSJW2f=1idBbEnmipANh3VOes&g%!L*&mC$^@7Smfw@YptOM@#8- zbRSE%Dxe|B0xHCTNzRMMVYwvLHLhVm*tbJ@c=C#$OlAQ7TMd(&#w_qzX3fdnhesWi zRa6msm{^!VPUh!*D>|EkWe<-2B);+sD<+OV(q43ITG88&loj?Z|2+YH8QWjnu!#cp z0dpD**l8z>JM~nYcEUKEcGUMVWZ(cwVnBfhUcv%|AfQ+c=oE~wNf&9cYB&@cAwENV zI`J4H+V3h%D2kOYH7Qt#I#%gf3ChUH%EDuQUHoDeZdE|1#WNRDr+QW!>(aVmDGfB-oVT=Q zBrA*3MQ{l$c0n7LSUjzIsT_>RA16Y9)~4AYUF9ngEb{>aYBMJDagS?L*YObDJLiJ&B4=zIug3iSx6{>yhq zX+b{E^IU*H4**Yo3KN#e>BX>41}#IM(3ykO70{Rt7-XfVBa=4h%*-t0_3DG%p4>l+ z2*<70nl(AnbI>5JELLreZ_OE>V^yIX4`En-+l|i6F_aHBOK}RfgqUVm+)_o%WZ~ zt7uC7V%2IaTCoy~sj7VZ!TVUdd<8@+_5A*G`!AEx_uS(Ogr*P88F)_&ZcoO-rycUb zTeAnxo*gqLIfW?%Iq&Uo+LAaAc-p~Q=y`Sx`>`FYTYzQI#0S#3Au=ET@w{bTA~DfR zB%9q422~S;Zk*%FXAp1uwy0$GKUUPTc?JhE%!4lv)f^axJEIde>fAMhb7!nM`-B}B zMwWP#Z1_Kh6=e{XM3B}~>s3OllrQBG`26?Z$Gmsm!aMWk z;#(v0S~;iidD>3hW2=m z@8PK~)CM!_T^^B8=~e@DPA_|K|94AI({_IJ!HR$R*1eZukG_esz6(E5*C$QV8xz=J zb4cTZcRKd;C_XsW7W$9htq!;nzCmBtm^SKUTTevD`K@kZRb_J05_b|Zk8R9m17IuQ zpkU&=aP@(XxdLw^ACTlyo#e)C=UsNgp<};~Lk~X^ha7P<4nFdj;Q76=N8s>-4#BQF z?F^5~u$G~d!r`O=%cZD>#+B}>BqmMxG<<4WstrMn!>Zy%j)shJUW1_65&zf!oi+bS zz7LzPDJlQu#Ib4Y|F%}x6f^Hh*MsG=O=qz*k?WRhH9$LJ_D=7X_q*PQo_{E2M_sYr z9emN(z~Mrp%jFTbr4HSQ+;Y@M-G6h^ziT7oF#5nHl5PO%Ao2~}6mVZ{D8lN35)_nH zK$mYX}%!Za+Tp!oEj*;8^_nfLU)n8)Yd*usoLeg0NNAVLyk0vvX=#7$bIADzEuR@(2iOZO{8w0~ zjvXH<=T`IdFcoa`&hAYZ?sjV#d)u->3}@5Vxw6V?v~#0Tm*?vQIDiOKca3v0m2v4_ zUt9+D#T8ZDw|UJJZ4C^r?3Pu1E;~ZjkIAkU$N3$2B4-KIenWS~$it7p0S6z31NS=+ zy?gh8f>}Wcc7TEzG_mydu%ZI8gAWVXCnJ+Cp69;l~ z9607s>_^GI_xF!R?sfy9by=;{b+iV$4Le^HM>e zfNlr72W#dlpV8aa@Lb_`3TNjaty|s*EB3#=4mByCBz97+{HY7TT@sH{SvJ% zabqCjh`DOrx#Nc8#vAQ+BZoW-*98!8cDNM`l z4xbhfO{=2%v1H{6EctR77A;wfFBW~y z;#GWf$Zhb$hC9eq9_p2SQlW02Tsf zM=F0DpMUWgK416|mVCGXpU<0(;uT*~K~ydaSexm_(~r`&Huhgln2?d4m3gN?>OOO5 z%+p+4$47-vb7}QyBL01Anyr+*gd;p5}%gixsZy>sV)^ zwf@a+vZ`h4N2-7ZcMB=Va1zzEMC03VZMgo^QcS9SHsQA0f>b|yhr=P`AnY{X-P)E? zszLcBCi{}A8y;dv&pX!PYhv0`CXzytL zs(1`57k`GepMHe3AAf*C){Ckt>f7XC`8olN_$LlI_M$W`rq;7b63VPTkIgx^me!u9 z&wi++de#o!$}HFUOD$jXJzSf*g`S~Em}-CofcB_Y>N1v=+Y91^(gbpZg?YJb{h}MH z@|Qnu6P4!>dlVl?wx>A)lqS6Fx?f!qI4w>Hp94|EOuB4{0QP+3|`BglW&#I<;;?sctAmCFpLpkd3uKn&(PA0XMuaZLPU-cPG{m7OMkGkxhUXGDRMerlm==dv=Y-P< z>b}q)91jSvv;#H}u|#e(l5L_Y;6!)!M7=(H2p1sqRe4N5&)Uhz$ri*dn)Su9;WLO-s z&g=ElwvycqUyoQ}0eng!ut^H0rey-EdW18(LqwWOZjr`; z&y6S2BlQ_Lehccyn+t14mZ+s682RQnzCO)y3^F1i-4DFU((<`Nu?HAx!Nn+jOK4sU zlEv;uUACu&^Lv+lNm=ke;o$4?uljUs`C@FC_Y(5wzXY#fJwlY;`YNh!=m-2Nr2*|r zfrgbWJx@&Pk~8uRw5jJPmKAui1`x->momyI4)MWVeg7n zD|^O^3&e}XVA&x)JEpo&$J1xOusa&JswS@Li1OD&+xf70j`$6vI5!vixhX4ay$F@i zYWZlnMXl#=PikIH=JJDIm_H^Ni}X)Q+D?>Wr@`7k5^A)gNzaAfRx%75 zjT+-denmNSIOFx$7;D2-#S*A$x9*Hfch%LmQJHN5dekM;wi|i%v@`s?>_1gY+G~+W z+aGs=}2Z>lmX+*Cwvspx9KXmyVgUO*4ou(A+0ckjWgzx31hPln?2h- z?1kCO$Gk9i=D{z%dFkq$0b@cDcMn)O&Ok0ASpO;l)enmc%BsNQ@{nu2J!^{=$D74| z^~nbXnjwHg++c@jhI1&Lh2YawXQPC+0BI^DYH7M5S1U;xrEiEnNzki=7V5A{cM|c` zMv#^$El-+)87%dmV#SyrB;5x%-8KLBmz+Eu7pzZS8A4Ty7QD~SDMf~>7O zErS!GMW#&xw6)#D_50N?G;Yl25WA{At+j9kJGO!wMGjDFY@3v39FyH6J$Ki}nNOQJ z(>vgqx7Q#1;=DOyUYK`Jq#*HYn%+(}z8wSDt7xbG%JaOTwfPFi4qC)9DP$U*g@R28 zSmAIBp9)^z+CVz1)b(_I@;w+z@1$-ZndVnj&W41V%*mM!oCMS#$CY_6&%O2CW>6C4SJe+4UbziC&?*xFOur{Af^+VfLk zfOIxQK}Omxzq@-K4}zicq~_J*qaWL%gII3gF-+>t}2n z7Adrl$DQ+Ga<9Uf>$fGV+%j$Ed=34{u1teGh?})2X-3D>3qnQbkN^9dT z;vdkM#r53>SXr_$y`{KMFz}E7)ms46eC@N>oO8m-i-X_nXsHDa>9p3Y`Pg)y(ST}E zcc8ElY!E7EppYrXuoHg6UON&vl=BpYIr#5pflu=9efM8j_E}WKoVD=7!mS^_v z`%Aa7e7|^6VNWj+69s84vxR{ZiNHwWU3>+OZ?>}NVsmPa%cmsPg(@lZ#KYQQUC`yBIRRwTS^^~700w z4Hz3mR4u7b5=f)JI|M`vArOt5tSsszGU=ZxReNa*h(Xt)G7%GWqrURi}Qdg85dQsX$Q zv{NylYD(jT#_`mkTPq{4diaOj=-uX4Ri;<1TT5NIsJ(zHv=&do$HZ`Y#^Kq62CYf& zo%fhmRdGe-y44|7pAlj-a9YCTlobnH+UwIKrM(3^=ai3 z#^LD<Jw)zy5zPR{kC)=>@J6slTb97AyDA)Jv_$KH-v zTgTf>fPLT{*O*>b9j3KIT3xal=h3+Cz>7*NKud#0H@l~#RWKYO2DgR8`B!>&=8J14 zjlX8i`N!>O0YM8_{_6vuphk4Mb^Pzc+S|!Uv!8`<5*kY05EGq>GGO6bz~}G5;dm%L z0IOx8^crSou@Z|QY-^sB%q)*|@@;f1%-&q|#sgo~l1!zX1GRS5654oroH)fi#Hy$Y z=0h9Yh*`0}O=j8aNB?I*Ra?xOZidmeY@N`r6Qw12YN4>4xth?5_n7v975@iIAG`6C z%pC`Plre0?F*cGm&&FdXRIFK*u2v4qnNGg??#pmi&TVOVJq~rND@1qN9E7@bf3EkEAYb(*iUpPKZY8=cTEvLOeT-w03n4`Rbfckd&5H2B5Ze)Xg4nSYjW( z`Tl&~20yfL)x>d6tv+YmiEAgGG)O_&6z>Ff<~RQKH`I z!arr{Kci0%JMVWnAxCaC#@wGQFYi;GzX8oiWi57vNsnBc-mCZFZakKmtgPf}W*Zd^ z=%z6g3L}!9iEvsbLgBOk+)WtUN7%9N`sKqKvq~hcOY{qk>FtSoz7JJDDtOSL1Js^8 zUy~=(5jjyDd6EL;1t&Xx@9K%;&#V0-+QwzCS#WPv{u}pCb<*M|SR}2En^Qc}V z3?;ThuTnUbuWA3M=q=vgvo5Y*KKsGoTN+(GXaN5cxijfobuKrgr9AP}RZdsCR+)(m z3c8n!kJI*le%?P$qdBt;{szg0I_uSmvXWdD(jcLkNWyyl75muX*Dil7x_r;eAMTUY zd%M@Im5Zy33UXpR^H=j-<9zJ4bsvEX*xR})%)fbes93<3Wkn8Odtos_y5HPd-P=Rzw8{*I6Zw&<$4LgW{h27kw?eb*u*6Ifma7xv2v(Cx(0FU*@u zQiq+17ZuMTm;%Fii@7aIxEc86(%ms-=FHQ)J+69Sk5I_`i(@-7R+KNQVlCGiUjDbkANlH`|NS_Tn*&|~8qtVQOa|tVA+iqZn z4yzu_1Lj3J0bN{L0hi`6rG10mu&x{fIn9X7S(_DqXZ6JK(beaic*L?FkMGO2@zu$p zE*kZ5?@y8iuipE9;pE4FR}S9)g?V=!H)~d$v`um*!#KBF-~Tgn zYr%XpaS5KU-oA8Kdq1L4e~+uD?i51!VM^PBs`J+&Nn0j3Wt-&U3=oT(GxK`C*7h9ku%f->X*5&nSuC#>6<5VSIZZ568G##`;f;yJq zn7236=H&}Pun#BuWO4k*(>=^uJ8}FUR!$tZFNZ|Xur9m)jVWIiygv1ggcmv8hohgP zM#-Yzi(5XMbu8zNgjf9!EVws#$*+EN!cgnrVKO(d#3VVnRkbJ}kc+wBXycPyYHfB2 zZ`X*}@tN607w!Mz+z(mq*K+2gX;)P4V~0<+zIP30Ep2tu;N>jr9EsZp6wG^H^1z_5 zANjZSyuGUn@{Q`eE!6I_kqJBFwv9v3isNjb`?SNnXAc4gZ9OneRL z!D2`9O)m+FytQW1xcAmfJmG@nXOA2F$%F~n98~g<1F)rY|6W}9`UQ~01ONaK07*na zRDJIjyng@9`F)Na>iKp8(_dq%s$njlf#d6}G+{Zjbnc^EX#xH9RwrU^Wa9QrgHMT| z`mr92s5IWd4WF)DNdmjkc(Hh>)GcEBzGVJKjkU+D$R4ok{yYWU_qq9>jF`DFLHgHr z!ICkPYQ)kRH~k-H`svns+{Id&XOLv0-?dh*S^89TgMQl~kg^>YaI-S2Snp69xGLkMZFh=fl5_-TeytWECu~+VIL0{oU#y3-!`0 zU;NKq*L}F6D)vH2dBxkbT6|ViR3y|L4?^ zPm1|w|9zjIcNUMYU+Uu72DfAtC>WWsgsOSJT~Y z89(9kw5t5|j$74+>M8iclE<%K7IwXfzMB}`I`}P;G%K;}79BgPE{!|)&+8(!0kL~h zZF`i1P$yk#llaV{?)YRD?FmJGXXLysH9+T%6m!qtrnpzl3+~krr zTSgI}Kg6C2*gJg*&F!D;|I&)HkDoxP9cfU@!f2xK^#|WAe0|3|ob<$rCM2W#^ecO^ zKF^S8FKrl5k@4HISk?vQ)rF_Ru}9cg)q&wicw`(hA}+E=C=%Y=Bx3u=L-q*c#7@o3 zD*nm7`_79VFzd~!`#(2tj(%6eHf^!aX#i+8-maQ?SM`b~uUot9spz65k3>~cW6#SU z-rsS|ZHdxi$7|!zg)Mz*$y)!hJ%2TI7ZJJEjm5aSkjlcFb3P@SrEe~+mS%0;zK;dT zf?E2ZWCf{kRu_nH;C_pCm5{6U?jft%QnHUXVsojrZ)9uh0ufI46`TbX#o%albk zZ&?$nh>aw;kK`c={MK}1>eYU-?Y)4lMT6L-CS`7F+5(Y7i(l?v4(t7kz-9FfScqwR zQtz=pKDvh53gYzcaBp)rI7aIc;|=o`CN93row8NV-s3$4c4a+?lkB z4u{3?NE}~o@!Ga<1#JSF$T%iZT+++7ZcAh{?Q+j}Pk;&=)svF|Y^Q(0!e@(4!LGhV zhMEMwMne0r@A_hNQ^;o3B-T8L*j;)ZvZSvMhr4wtsv*~Z2rMRdA3L#oePaFaV1inl z#PyV1Q40h+`}*l$Cmf3>kxz4$n)xcLV{pAXHR|gW`?d0V>{qTNMQ+l&%}IAo?ETm? z!NJMbDs(GnZWGXc!X58cR}n95wbahwLGz-q6CX$?^F%+9Fn&_8*K$6unwGX&>DvOE z)pQ;oX|l1HM-j*3xo=iNJx>5eV8UZP@jE?hPT2F`Z+zUI&oWuux5BjwXrwuNxm6WTTW@0F-LrIBV5{yX;#_PA zLND36Zmg!i_6(YqRa5_R4~cbvlT#CK`mhKr8NMU4nkLp#?6Dp{wvc%`jY zW~+dXm6z?h*}TOA5-~_5oo;{ zvD8Hgx}l)G*|pw0N~q?gsz{$iI6SE?QyYQNFTMHszWW_{HYNM7$=Zeio*ZA(`Rtn{ zrF4uscO`Z0vWhAKy&~9HZs1AWx`Gm{r9@wwUkvps-PaeEK+_bwCFPaDcP3S_I8?Pr z66_Y#*~Bh(tUYSqS@X`?@3}YUZ;3;VuleemC3{mq*DE$wUb>io)f1n}dgO*dD{uOV z>eYxwsp6W0$?xb!&vIZIGH#5i^lmM^meX<^e0s(eobN54Jt)D!8OV6Ej$qHsjenI} zqG|`Qv{@z+#mY~+9sILA2HWK%8uj;ke(s<6N=LH}{geIHm!Ec(`i(HLo%S8y+AFx7 zIe65p`LDA5_8X~rD^=1epugP3otsR=Px6d^IDz_0SWK}NFB52QS>Mj3e~RI$%Vz$r zY&eEz8pv*Z5Aj=7H!OS}b%OS35)#8LR8h% zLp18Ic=EQjUp^XrXUStX{&mUYlP_5M_~av(J{~=A>5Ll>p+9BmjOf!W!ee0Wn@Y_~2Yb^qT^- zbXl?EF*aIrI@S_bQ@WmpjhZ#@ao@UQIVtZI!5X%Y6Rj!lH%#H{+fsiJpo ze@1=%6j=E)v$a|u)3>0r%f;d^MsTK$q#Z{c`QESdP4O0N=oT+}vjKYH-+oa+-T5)L z*t3*ZzRMdU4IQPk{`plyVzV)ymJ;80HI3FR`J)Dm4bkF2nW&K#yf|8WkXRnvTfPkp zN4ysG@s_h~Oo;;ifzQA7_P+biyDTfq9|o~^3efU?V`J9M5wd{H_hEmKXS?k>djENs zjDBwZbNfH_N+Gu4YqJ4b`_PjtHJ9nZ29Lbi)q%R8DJkzKlBBnO*plM3aFcNjTEg=m z`iG#s$NoGU0s>meEql*3i6B)dvw}biH;X)28Qel@hsY9zzx&KLi}rhN{^jDCp%(Zl zz5Xc7Cw%I};-f=FHfGkDt$#GCznnCk%iqs^;|$yP`MHN2@cjHIhR&K5*ETfH)K>hv zn7&y7y=vyKt6k5#NC5qTOX|vAgFb7osm^mhWH0*N&=zFO?x1I;DRqF@g%F$Fc!*y$fthG<&m&>QR8FZ{2&oVRU zTXY0LGSVYdq72qbVC~RwOP(9UR`v0g^0kLPX6BpK`~T;SFZX|O-W*EqzwJ9~{`vbo zKX1Q%GY9lFPPo5u{Nc2g9OxN2%=6_iG5-GIV>c%h-p;IxZO2J5lE6KKd44?Fss75kg zg+#0hmin`WeCm3kyRhKiWUmZ6*b2rmAB7U{ExT1~{&P>YCJUs+0~k#Yyf2&e1EiqL zHg5Vov7XS4{?D7A(^~S@fzQsJec%goXYc#$oA2!V!n`knv;}SdLQoskY_rK*2GCU% zvtcP&*{!}yEr62R)9*Gc!}Ok;tO0KY67g#4%P}a;_iI%YyFn86q~dw^u4EAc$BPQE z1KPM)I`wz4%H})%A6eDo7UF8Wv9~~x75i#qdRP40-2+<$&?}$2z1SD;Z`1_?@tlms z5w9pEcq#D0pq^7X@$Je}*p@%nByb{{@04SpL7P$emQ6PbfHShu3TQyPn^T9@rv))~@kw z_7m{^cQu@>D1)1bQN8epC&E{WfPQNC7D4tq3-dz)ZoEl`PR#F9eqP}lQ&e&WjO*q+ zkPj%ks_1?9Ka|`>PVo*3?I%egSrv0_*Fq^(=16 zAaPi$cHgE+U6gIA2ev4n^{>WP?d{xAF#EoLY~T`K{PMk}o7}szqi+5i4?OG&PO*Ug za5*2T(l}W@v>u*H#k%g>at~}#KnEQg-K=`5EuMYl0Nqm0bkV<+J9jr21?WyYG+hnbl;5mF7XSbNOi4sR zR37L8^rrM$SDsGW16_dbv_sR?uubWK%?;>HX+u|@t{&KIJ_6vXW~5p~?RW9`?U;_cw?a0s;sjCn=`k1$k}=_ghmc?X%+2 zx5kZ!6HiYxsh>7P5e4Np#%bwsBrfZAd_@C4Lw@`?2is{mhro1eG%diazmn5ZzYtAj zFck8a!u*a$4+_hcZ^dJc43E3nH=A^qw6{fXO^?%svT`@2EdCZp26VXW5Bgwez`vSZ zI279LAgITNZ;yA^aGt%8Fy~eX=SMrJ=RyCCXVCx643PYTgu|@Op{mV^`1)#terx~} zpnS0u1u5Yeq|f@+_bBJweL$nM4Q_d?^Y*}r?BKD^=ExCycd=Z9Lbtve#*eKmJ~5U) zlRNJJoQp8&TQQ}GchXT}(-T}dObpz0v2=R8X9MbncOL__>jXBVB9)eNU^;z+CO-47>*4TxvSzZr zp(tGL`0f7%jgM9~BtZEEtCv7|+0-p^eD-Sav5dK^$Munq^-IK~Jbmd^0Nx)t>=B3P zDc(?EOT>Sw6Yr_U-*)&1S(RVay@K_PT~5K#Q`Y{~V$!`&PT(6a*>(Gp4oefS>BT!< z!SZB|%i?OuJ4Vq@3WyJ+#!lG2aYVh-9v0p_zR?GYr@LBa;cPaG8>!)sD z-9?`}eW|GU`sS6{SKe5-)@x$bpjA(;OVg2ad&JMMf|@ti%|_=BN+0F!hiKtsC!wC1 zMeP!kKB)AL9U|*@F^HErxvW`mjzDCBSx>;PrKYW!4h!fBfzX+O%GFe;+JU2S}ytTnQh{5(!Z*;j`b9E~Q9r5yWwJH63+5^{pJYn(-KRV+k8H4Ahj?F4>o7HTk#(_+J_H7KLH5YhZ($>+ zkRjNAP=eu?svJ0Bif(+Xh6=<@0ad&$)v6U<>&juefF-#XK@p_vMz$$&7o0}1fe~}3 zd3+1KQoB>Ip1xQ7kyu92kP^&N)5rdKdPNWnMk-95F2jBKw0jpaRjp%pPdMTml(^Rg z<2mMeJ4_&bgeWA_=U}wtGSYRTS|D9$Y1qQo*y%K;+d4c0SE0j%;GgxB1r~^%FcmmY z2pBQ*r5t!VbiOj8t7G94SPI&4L=)LRRfWRgOZbEu9X=TNf|OpVP$C+X=Y&5W2L}Q4 zH;PRAi~cOvU-~tOmhK{6b0ofnI&|p`{=Yrh(!JTMuJ9|UlyUTaP@z>_OGwzjF#8VX zKSYsxK4|NeMc~31CfXKFod~Q1dT))jWYNG};^3D+*d9U=_an<-qcqzF|I&Vr=X`d; ze#VeDi2IqxO~*n-f$1PmBDk6hp6Uu2-5!y=V1ToJSX%!Qh32Vbpu2 zQ;ctQ7U*n%Va3uKKcu3|6j{SH{hG*38xbUK{qZg!Y&!Znno;uJuq@a@+N$hZ^AlX>e(+YWoZ$>GD`Dsl62t#u#2{_U@xF+|O~&|@Y^TAq zprkWZ;C1e7HKBVS7)y};&JA>(V-)qmLs%rL4j6l^b{H|@=LhW{&@__p9V`>cC|)49 z6D|mS6u++79lMd%nr$SvuvAF2$Um|a1g$r(;Mi6f9j}rdg*PM+v3s8C?!jg8T7q3x zPUxQ0)MgH-eTb$EbjVM!MBBmSk?&=jo(0WHxdJ}?14sTfZm#S`ZlQN(jxI}! z8Vc;2paNcOJ;Lg#DO(0Tk)wggrT5SN;1GP4u;;U=f`sF?>%6m}oD$S;T!X+9f(v9iNNmU$oXD+q2b$fWjb?N~kI;M|*Z8UL$wkMryu3O&s)K^JupXUTD zd>4_J30&$n298Fjg|JTFA7tp!45z!RG6L!Zdas@)-cJ(t?|bgMiBm?;A-md(H(VAa zd8_-0zV6U3|NCDEN2IQP^6&Ii3mETvj9+BXt6Q$A-q`V9PmgOJ2#>p#VX%~Iemoqmh)*<6dR{N-Il!0!zun1 zIUYSxRt(_UJYtTU95L}W-r*9T;Yh%XzUL&V?%Z<`{2Y&vRlFhR5K1L!Slz55wI35?Fit7g_ef8!cgt*K*MGc)7q$n}%5A zC%~6;C`~H7Z?ivP_G_M@*AG_Eb+xk&^EIa+`*Gr!9Y8wx1oEjn{rvDnj$5iVp&V%;5$yk)+^K#^YicJ zcWhz9E@D)nH}21i=9SSg1(_pHzS@CA@@}fF37n?E1cbYi#;OjiUJlAAyFfMPZ;7 zFG)LW8Qc=nExtvZWon>GjYSrD0p=!{FU|yr^?Z&kpFtkZt&;h1^T^W?qlP0LM{x`v z?FHK;-|%6zTJAmKPoT%(qcyuqHM940S@KMO1zT<3w_&K6M_Cmu%^*hpfx<WWmGtolZ{_iKU&sa2(wo%u zeWL|z#7m;(>zCz&DT3uozh!X{c!NXKtkf#Gl!u23o6chOh?D1O&j$iIh$yHxLd2}L zBEJF-9eUHl7n%<*<9sW;LA-T9`ihEk<(`$=($>z`EAi4TjUN{8-1oDYt`fr2S5Nnu z$5-qAyOWSYGwuFf5iaT@?m`}~zkkwf@>Pzsf$l|~J3IJI*OlKGIzRFuXy5Fm*^(iC zSQi0ns-%90@boK(_D_AQGWc5^juwkym7bP-|C%JOG<6Z%$^(6%ddkQIXH!|yXoK2! zS5TC_>VbQrAtsMDCwz1poj+2I!sxy0!RW&}yEoa?DGS%xKisQoQ%wx-?qh#H6F&Fr{9nAe3{0m!<&gR-;zc+mmdo$Kvr&|NQ zuEWBXcUF%QgsY-@^SOQzZu7Yy($fYK=x-RnPrPUtcYQxT36RolDOrC0UeQBTC0vV3!N>cYEIa zyU!&xNNnN*X(~J9ZzP!!L6DYXeGJ&AL0LC&S!vf)ehywZiburKpaJp1najh%z!VDB zYE=({qm4_WpK+poN%E)Rm|l!>$)T@)?$As*1P{#zx!#vB~kq0YW$>H^8iHcWuNFw()XUb~ppcr`A@ zQ|aGyEF)I(%76UWS5vio!qbE1{30?O9cdl#Qsm1IX3BIcsz^1u3gb6-SvulKFAfxZ z1^6|utGVKZcFREw6s4GlEg&ijdfz|fD2*!1Y?z0tYTFl0UZra$Ztj^ziMW}R;kHCf zFaUXGk-^L1Bj?@?iXaqgY#Z!?<2`@8Hd&i_gt@<%UPN(ZJV8L5dmI|#l{D7ss99B_ zK9Ol!)`7LwOx`d{Br_SfX`l$K04?aPVqKEl;K3)~^=+2KqEba|BpXL>auy1DWrWoV zR!41);RF-lS6rd?_1xF$@~1f1Be{tfDV}gnSJnkb=B%>qT2_kcO5Kw%zcJS_4daF# zu!^?r_OI}|;=5_+?!+ZL3k5D2zVZYZd@C1+X>C|cmd#e@+gZ+OQ?_rnngI=ps@vt= zP^>IxJYoMQA~*8J+nNS5au$46mjte~mt&ToJOMow1V5>Vw)zZ-{Y}u+6O`H^smlBN z;!6e-3Jgb1PuQ}sY^SNOvF=ox?@S;SjCzDJV|`A>6s|!$#X*h~gpY|v)9;hvy~fF!(d0>U~H>{j}7^^T_D*G*b$s^y>;fkq*UwvBixmFB;%nr>RCuX#zXio^L`Hn z)v?O+G?VcgXV{g)GVDhL31n85)abA&1)M~)!%s;c{&?K1gcMH)xu=8_Qm8Uv|C8HG zDhliEz`cwPXQL1>qa2wDgg|z)d|S`kX$spVG~Mz4^8y_7G$E-^gOkvzG+DI_CGr+7 zaUGYP;AFi8sjlnepovnSNQ?EoDxmqi(d-_Qc%c~kOx-IWH*xp3`jUgDZQ@f zd6Rdl(Z*}C{jl3yi3KGp4R&+)v#W;DFd=3y6bjfYDaN98uh21O6q34!K5@8cabrBv z+W@xY6vgyfrjOlaN%lR9`fhu#{!yT2wI7KgqOKl!NTeGH?evT5H-nCsV>&2eF2|u2 zF`QUU`cn7qHs|Aiqbu@$c>D}cixyYek9#XY^C(7>{eJHocXynLAU`g$;K+b_12!!1 zYMD<#0Kw8eS0|pLi)@JNcnMw?A8}HxX_lb23}{v^vnbzjqa1oB!K3{cAf}mG#5dmR zwgMSA@BC@KuEcPeiwu@k{ysG5BH?oQk7DQU#d-Buto6DTt(Flr-s(;2C_7|+njbHN z#JaVcpDMC6DO)C-$@p>+n9f^mchmZI(-t{QDQ>P%L2CjO$#wR(l8Ai2f28|pQe%KL zE$A!2tsC^VUPZe^s%o#J8%R!qG}UEvH4I9;ZvpCjD_zTdaJulix6WIX)%(!7C`YjG zQ0sVAq$DwfJbs-Qidxx7K};JH3-KukX1 zo53=914B@$%F4@X8j6fMJ{OejqDg`PG^6+b0|0lhiJ?7oKoYMUg)X;GQ+I^Uw ziw_aa(NYakczB>5A-f+UH&As$F4RMz1w`r;|CpRiXTV6T%}@~6t5r+daYjH(P#MKR zOb|X9&QvVHDX#Q#wOOinU|0*xdU)wO@HTz`>{p)>lzwz-Z|~LkIuyY|YL^j^?k@%f zK;m=l&zj9GWYjYjHaaDE`umU-I53w>sHQlqMBggvtEi%jdgmqq6~uc}zE7yOyKs#2 z*o)iPKIWml4 z%|@E|$~MP*oVDI*|EY^OR72&{@?E+Tp%=}o@y*VK02FeMXf0RTgLg{25%{>|-00Gv zcQy*vok8>2uWe%I(cdJ2;9?11g$#FW>YO_Q1>vVy%FLPD!n&CL7@Y zH2$@xZ)KXu`nQ?>5+;7Uj4~Eo^HRUu{8t;57WB25cSuj=LQmRF&RZJzZ$(mKBAt0; z;);0~hoD&na(}vrp}lb79w`W}t^y{Bl(Ep_$Ii7F^p0t#lwlsV?L3x$l=jvugmK_! z(IJM<^#({N=u$^SA!IgV7wYs((QV!`mn>VoA`ux)q)0H4WR~wqZ0u~m{in7>d_)>f zsn-u;)HM~1ZbI#u_gsnV*$yBtbE5NRD3AvMl46^>P(*F$Az(B*rvTQ)Jc2ivOjDDl}DVEZTYc&rn1CO)m7NtKw}`C-7@>G!!e`PU>8LOqkG5 z3Q{{q%7P9AvT@vP{kb5DJD2-^1A;8!}NU;dfY{e9)y?p`1;5 z2CM!E8Mo9g7<^$~vw}&vpK2?9?Wk#{6}aJXRR;D}LD0;NLQ~F;kx*%4;m|CDIIF}L zq%)>?LwW%!yZy?6y>%Or6^i`RM~=eYG#WLslbM! zg`+q++E+|J`>gkwLPBf;8hW-(PCH6vtwM0x~J=h6IT9 zL}C<^ok?q98Lar$N1zq#e&>+KK`44V_32o#Vv5<+Vq0i(u?DJGd_#;=!0(F-wAXek zr5ONKJ?d;75edWp3{Q|MncheO1pT@`Y!u5GfF%>6!C|K&>i-L+=x1XA!|nHmtU-yC z-tWeQigFRii$WrVLi9!m6+b(#)&6}D`NKk^Z$$7uxaue;PK46gt z?KUn~P)?m`P+E3{tfSVym*|XzD))NJPEW3au?x)2o>kPl?Y_YI@~Ifv`|v$!=b8?0 z+RAUthhmF&+>FM$`bqfuf?X+4GhO5n1}7jSI|nO6kg(!gQ=2y%p(do2|mEu zyl)<8q!Niqaaiyyd=0SfN_1npvufs$3N1>uf}ycnHYa-B)6=W3JN8-6KD04%qL^EC zMxsjPa`x0&A7z43&bn;6U-Mjfyb7u4k*ux6jK3DY+3Ol8lQE17z35Gr?VTXnR#f~s zbgkYz24fr?e9stqvKw-?O!~Y?e{4dv#bGEL*T7f%?6WIrtnK@z3G~4{n@ZXtvihP% z<^nDW6kz)ie%Ju6-wt|h?&9}T2ZZp&>9chXm1w9c{0UBOHXufX;ecEa8HQLYqbWlA z5N<(Kh=(-dTZ9Eze{VNMqi3MWPIWJj@hn=cJwpmoYQH)DE-%n$9MRp4llJJe7vO}+ zS}YR4e3QaN&f0MIBycBo;p9gqNr)dv5Gz3&$(;bZbfO7;Yg*J4t%ch3BHWkIwVOiI zZXj@-I~N0oIPD{SN6v?It}2b=#0bvQsYq=a#C(*6?6g#x$$6B$5?h`Ta0DaY6(h$J zzBlXZKPVM8?(ys}2sJvw zTGN5|MCsJUH|b4~SVzr1AKR8-P6oG4V@Wy^`=e%bBY<_w&-jVAxxU4|_c38p-0uw5 z`mz%qRp;-n23?u67Xtx&O1}LYwsl&C4)cpEo_x%v=?{JAGR#(3j!9eX7=flOmO^(c z^sxtgcXQS>r(CwBh5M~=Y)pLZ*kM4f1!@*|$sOAA3`vfN|L2E{k%-gHwvAg$MP{R| zc2T8z-RRoQ?X9nbJ~s(Q4dAeSHWI6X7LHbMfEz(vg@#^tUwOk%*KF5OZW-*Lv+q$e zq9ZyNkkwLD% z_0OIyId0Y7N|Oz;!OB^uOzPPlzr~qy*^|alv9k#02qHBY0&w?p<_|}$bGG>A-sm%* z^CepkQR~Xb>t>_tr+4Pel)c`xB#%WdoFm`BE5e%lpATEjTI9iHWW(3_4HT8rxNZRISJz+>GeFQOy zOI(g6j$T5`>7xO8GsBsCWJ63IqtbthQ|!EosWP4e%Dt-=TJ1+GQ$79RYyiWjQi2Dn z%S!7wh$$#BX{m*S^iZ-38XVaPqOH0ZG4UtOHECs%dZ8`R{LoFj^fJO!v{tUs}6DGIT9<< z;%kplr1}@j>Ey&sM>4T=TlX)-CC{AKTkH<*d%NA{OS#NTAU=?i3KyV=tI-|gg$Yy$ z|7Zm&=(R_PC%X$JU5?5Rn%=_pvm~yzn@N2!u_d^F8H%DvC(F2iW)H|FK?L!$wOo^Y zoBZ~pbv3V$i?w3ynoBp0kBHV6V;i*%s`FCd<)TI#Un5JSCwCrVe>!|xlEDp8qez_- zyer>14smW!)JgJqI$jM6@?3o>l7v|;Q}8KE*GZP*ciuyDgA!w&gS1}8!lWyB4>F7I zlyNS4-(UCr+QTFKjjU2r(E?r9Qs$Z0U3YAtTMPu)m{fR=;*fS?i|aBa`#xJ}(7py+ zY#gdU8`|z1D56a036u+|XRwjhc~=YsS$Ao4~PN)fUikz(4RAt9sr!mcs zwTHs^++hR~MT}Un!BNRsvUF3s8b&FaQAf?neE#FRMyYKFVF@@e0UHxuj!tMklbnSB-Ea zh?_Uh-LB`w2>S$M%nfpVpFGZrVs687YQt1PJ!#Xx#2E*u)x=- z6yt=P3fJkE#xt&tP35xLOfFAQKcUE_u{KKe;#_jfVc+?~bS6TC?gv^*>xFB;*7AEk zF2ZxZ!yHTn?nX!}z?4gB7_{?uP}6sLg?6Gqk!ag!L{QKoXykDdhD~sUS&R0rA9NLx z!6NH7-bbUw2*1L9Z6|tLOq1sgwH4BlzvqT|k#^{BHlM28|FpJv$x~`n;(>~GxGHWs zyQXF-v*eFOkJ%}`0gN2mY=xXwq5FLNMe zux|h5TVLy_oW+Dyx(~bgK*}7-7K@gq#$tdWK74>WL3BV>qmf)54wP9uc$Yk3>0^_T zHiI`44exO~F)S>=>cWkOrBP@}GF;Zm+iguaFYM{nn$@`wqYR_lw6KqM)CVlD+(S=X zzS#z&m~4h?n_@B!>#p{S4}P9o00rU74~7s~+%W`hdEPG+a?N556(*qq^+o?V%*oSK zRn^mn(i%5<#y05RlGp*k`8`3cQBZzEOJ3Ey{{kJeiL^Qk1%D`dgz;7xR0fW) zo)XF%s^LdvW0Y5At6;?B?7(}&I*H8G8t{=>FK6>tnv79N;4uC1{n0n47{DoPt@yP8 zX;^53?lZ3*xZZu(rr#s0I3viZs{TMA&F|q*yb2&s0f-AB4YVB(fIq|PWj6`AuHx|67(aLuXrP(y* zn>mc>tG%bdhcy*L5{vepLeJEhMtmDk|Db)ak{Z^~^50@)BOD5Wx%>rlrA3g==QBG; zBHdZ%RW&x%dB$>pw6Q5pL8iQaBqe~(mz}m%#73c7<#+O=R&BL1EBhAipw|YTT1QQn zGLuG`AOOoVX`q}{z}trmTl}5OA zcpkp5V`y$5>i8pq0C73HdgYA z{oh?QbT%2d?)@v5(KAZIaphTtaa!arVcn#^+4Ywm!2DK~4Y}!@7*I`=rh&{vhlT~L zS?b3Ekfh8@0byvBD1jKI0L6-`a-p8JOl=TBDi>ip*LWNzkims=?;~Cj7w(v!_&|T2 zZQS#glcuL3%cA7-*JjkWBu>4W5N8g8T(Ochb-D*Ux>IHQgtXnK!o1nL7G`AcF4kQ3 zdj+x~8_A2=ubenQghZb7GrQ&#&#Z|DJk~nVL7#1krba>IoTz5K!E_{jFLYN@9>td4 z&qQl`OUB)79YYFYqEW+9wyzmA3K)AsXNTB|*56RC7)1|h;YQ*U-jwJrE%WY5s!Ad3 zoZmOtM0mtl@<5BpI!jHiP1s(F62uvU7w@xwh?yqwe%)8dHA)9W6fhH|F!DUJfes|L zbks`K&u15=2kZgp8A#HTj!AW|M2=E@rYV*w$=}pH4f8dAZQkkboF+=(U~dSXUkvHv zBAsU}J3cpawFg8*plQ-`^~<&gZ?qkNsUjg#gz-pQU3242E-7f}Hve;1bU>!%qLzBLnyk3!7R{-L zh}cNw{-jn)3WQ{=!#U`psa@m@uyNYeL7nA1R4YwzQ3W;lwV|6uGR8F*#o!6po6cTH zY%=PZ^^bn+*z%5GV;rrLcjLM4F?`yR=V(<|YYLtl7#+~_s~+$>pYx*Ev7*nagz$UC zLmtXkJ#fzPDCs%1I7|@s=BS%==0qU0*aIAr|61~Ro!PBBfSOdD{0;bar}9HgVWa}_ zwQBFTB5*JbIU942*^T@*MsLs25YsC;JFGw-UwGHPl@Ll=zGR!GpIK7`JvycMo`TVm zJq!qyNPe6Zp|i7;O#5wWduGH`IP+2ldTrJL zQ_THeB1K)Q5R)Qk`HcarXk~QT)dJ}uf&#D=B??g_`oy;eIJAE6Cm9JO6_q}LkCr>- z-yt?sN>>X8eKD(;S^V-TRd_pU-EYj5;oytH68sh4za}W3JQoLMuTI!>^*d4mdEfHu zQHcZVnw_E(D5e#c~Xx&ee_$Hd{kPq0bJ`bWvNCzd>aArQI8;OGq(f`@O zFzPtRcLY6=cj%mVW9r-<<2(K1m(f{5I3T+H6dmV>sdFc#qN>FK$j)6oDBq0hCe}Y$ zeFVV-xIJHbd<8zBh;%@GF2c0doC)Lx`KN9RKZ^W^nA_c+|HJ>!J4xd9%*K1qe`&|B luUhQN#Qz%D|LGcN2m_nt&8?~k@P985a#Bi?)#4^0{|9EIi=+Sm literal 0 HcmV?d00001 diff --git a/web/frontend/public/favicon.ico b/web/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8b46b4b26f06958ee60c0fec1aa409f559fa84c7 GIT binary patch literal 15086 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 literal 0 HcmV?d00001 diff --git a/web/frontend/public/favicon.svg b/web/frontend/public/favicon.svg new file mode 100644 index 000000000..e2f412b70 --- /dev/null +++ b/web/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/frontend/public/lark.svg b/web/frontend/public/lark.svg new file mode 100644 index 000000000..0761f278f --- /dev/null +++ b/web/frontend/public/lark.svg @@ -0,0 +1 @@ + diff --git a/web/frontend/public/logo_with_text.png b/web/frontend/public/logo_with_text.png new file mode 100644 index 0000000000000000000000000000000000000000..70f26788c1193244073ae44565efe611cbaa81f0 GIT binary patch literal 8979 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∨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

X9l zPdCOnHdKH(lAE)@ZY=x-o>vl)fKSSm9RBc@M^1JxLX7YzG!2GqT@I)1m&wd>Uene=8{+N`hpkm*7MKc z-RGXf>Nj75@Dy42F5qCW=rAYfHGTHNbW>P$?oquRcsEj!DF{vItRl7Sr>qD8Bc6GL z@8GwFZ;@rOZ9taaBQyZ8zCX?>32rDKp@KdTCy>iwni&e&XUi%-nHn!I{+snuKcxHf zHk+YqEf5Rk=(Bmt1hQ9g zP2k+3!a`FrA)d_13IEbfrD|OsY`L`3p(xi&6uuNSMSXOS!f#xPnCnt-1%_3oc+!M2)U5PC-7g~HHyihO+@tOwks3r6b^h(+P@P&2^# z9U2u19jUbSwg(0L*ddb(TUdA-(La$AJ{uw#e<*`?E;U-(H|>x#Ww8aXPwYNa<;cf5 z{{5@C(fu(&eOc+P)$13amQ!jluRB8Bdb7_1B9MJ~yEWy-?v0jw9&p550LWhf@MMMe zEP`vDvgTxB4NY5fOv`wT16Gs0HECIR3sT|;Lc#VGK-nfJl#Q-O}ZeKnO`Hfc0kpaAqJd&v%b3#e*Ed ze1t;qDBvlcC?~l_OVW=|@|+-Mn`$RsdLo92;|Vx<}JhygYYLM{p?-8E0tXGX}PpGj}2@qs591togMR6?u)!oW{tP z{oJd5)Gm%AKV`iI1Q>P{{e6uhkkbv}o&z8pBqF*IzRqsD!!ypauvSQSDPQ~@;`!jc zonYX#kS-Qz+7ZfB%#xs*3J{}DoGE{6wiA*riEeIdW z{;{eA;<$gJwM{ytb?6scx-|(3qgwcaJ_9iJ;G^Ks-BdToK%jUZ3^|J9D@Wd_-7)Hz zLhO3osKc7#WJ46nJU+d*e~H1q2}|qIynZ*BgWNuY5zOrd4yvC;5$VZJ`k}b;H^v80SW|vR zdpF1J-aOklvXto#zaXuut>b#2$xyJ!whO^P0FIO0P#C_nDnXpW-Pj@u)Ty8839*Q^ z@zgSbecRz^>r7y(t5LD+L&WJ8rL4fXK?Xdi;i z55hPBX!%$yhVtbf!PeJ7>lN0|*7)@Zoqvv(ALng3$KX2d%=rpXeE!rtC%^BFBG{FA zn;mrLN0p$Xf&z&H_KwMW9L48G>g;-bBmHK_^izkgT{3&uyaG*`Xb!$KQcDuS?D&}0Crli3D;J{m=?P}E^G-qqQy@t zGZj^9Tg~$U6}U|KTN4*>9k`BU%`!z}QC7_bYpOv(MJPPoMks@x1>tha+ad~|RoBB! zCQ!TlW2}GuB~(&CYu0>@Al)JrP<=NQULSk~U>jT)hKk_h3{YKFfy&bLs4Uyy#~bmr z9J|Vrb*Nmw1{EupVf}w!Mm?YEa%sI{$dC-e-ok$589P@Z5x&(%vaO^5RwiHkyWG7lyM4&sm)y1gxS98Sd&10n ze?H-|dmlEv^4|&uLvQgVAM+CNpF~SG91vZ%HYZV4g*bN_^v5rj?XejYfM|dP-B2!s z{wucNvQ~yQFJ^71l<9WeAm;l)Pl&&a^?)OSyqpkEm+*I1tozeky)xELh$T|~7O9d@ zYzKs3chR{2XIliXQvvE6DvG)yE_ZL@6wV3?X*m@^bdl<%U?`-zd!vxD6cJ@WPzh0- zLdnWcvF4-yV$FwdVguufQz&1%3dQfgjty@vLjoYc$N43# z`?pOpF*Z?M)w{NMbzc4YVmHN3Y7}UgK%b;-YE4tcW|{)n0Z%PB?nd+(m`YKf7pbPAYGwkb?<)@3REBg|NU1 zK80Yt2%=2;&8g$TPen}~4L-VyxTDpgY8{TT@AFwD*7|;UfTyA z@Tf31?%-JE#4;*Wic|3D(<533L~CKVvy zG@zOu9V)=s$fKb>FTH!>gxIK=0oc3g^ZnbEzzdEW_yb>HhB$H{91NvU=b0qeUeS7? zz#~>(iuw)fQNM02;uU2u++ETBZK}B;VmKYgNvoaSfZF6?X{{_fc9V=O5Ee3)WKA8s z&ypoc4$wm4I$Q^k#t+rC^@zn2etcWq0a%f8gOQ1~g!NKhmih1x2DvxXrkPQm54h3MahDEtnETop#~4)MV_w>tHiNYd|5 z`AWsUWz`m~u5}RnwEWcve81mXE5*6d>6D!_ThV2eOt8M%j(;?j<|QbjGJbc$6Z8E3 zNFz7i*`FH|tz#1}y=%8{G|nHUS&uWO-K107pl*p4U_`$u-*2(C=E1y7ZnZ5b+fI2D zmMWgeCMiH|Q+=@<5htg2ZfL(uT=to@Bzefc=C3;NUyJ5Xp1bJw0Zz}Kh-Xg;SmzM# ziCtY?8(on|c!RrDdI2q8_3~dU`QD$4Z*lmh3_{_imNff{(?Hh%Ddwqvpij&9bT~WM z`WmFTE2lGrQ&R~y9)n9m<=H*aauLWBrH3EbR_rRv!PDWL_$RB zBp9d4y#ktwK7H;jYSNp=>f_c8tpPB&py3G*M1|_LUVPSlEQfB9fv& zlvmZl<3MLYzC|RS;2YT#+-2_EIF(_;{?9IW=b*WZe_4aNed}ZKCpWI_h33L{gOC=) z#=hf|L4_6i9t0ACG6l<<@Et>3G|QS5h`;_H#1=dWRFqQq6I6mYP*sNbE6*XeXb!M` zHPXV*DMXo&uPR)+zGiPBcqq47B=r}lL?Tq866rVP=&A*wGiQS=3Eh*i@?Q_?-`~Fqm zlyA_q;G+N#&HiIh)ev^N(1HpTz%Z@`NWtL@r!#=W>LII_A+Wv}L0WuWLns7eF}Uki z!R1a;fHL5+Ka_`W;xY&XtNYyb%yj+h7HmK|xT)y^If4lxWE~9KTFF^B7E5GRhG(iD z1{}F+@3{-O>(_RX77DQYr4J56AaoZ5$D5Srs2?Atl~i6c9%!1a+7>M(bC2$EU@cMK(tLu!R z;CdddehP|rn}px8LQ9e1qh+W8%oXbv-1R{tdd1nt>)+$Qg2XLdfJTBgo|Gk2^Kg>0 zTpJWE(R`aVG@xfsc=}~MENz+DD#d{;?$41@REn*4b3~E+prOV-2QzYp~XM7e#6?qsrovx)Z*qW zBoIRWfd1$)TsL%Uft0`171IDnJIk*jk<&Km=hgBKK^IrYqv6He%+zh};tUrOv+ zz8i%Q-o`SPD8S(>tNS8!W4zb@1yjxSp|NnN01jPQmt*cw*nQxb`LhKTL>TuM)SQty zm~1ThL1AMJ4KYI@f5Z)^=tHG3pUs%kGic>;K>A@buPW|z>7rL%pe%~6b+|sF-~YP! zZ`*zeUjbD3!=dmj<9Rzr%Sxe^AC%_yi~6~i_f>4zJ*?~y>EH9@ZaeQ5blicDv$JlF zm6hd1%SvF`nWSqT;(?r;m3ceuWYqHOLRuBgaUJCL?15ljH-J)$rc0}7N^Ew7rAH=f zyg{uC8Yi`YxHkx!9b)k$N-C<6qF`)x;VcVq2mzp5PewYjjXK8Dc6(QkJ& zUs$;>+m@vpgT|W!3D`a;G?M6(Bp+I_ztz8as_AuS9+@9Pw_gZk7&hZlm$S688g<-; z&a&9YfSmJL_aRx&?9P-(BoGV*3%Q6rDq6Y$NuKbr(~!kgZD41^np3@8b78^AAtMBcmnjgBf{QR7$}8Lr%W|o;98m=SgF81rf(VUY~vlzTnlvfj61` z#J<|d>Ck!eQa0f|O8l~o*v*Tn66ipsHl5`*k5QJ?0?8riSPIkWTt3YXhw)+Y*(cny zddAfK+j5cgyPL9??tZ=W@FBN^YMrB9$DKqWzsvDVmAX5=zaNi7_xyc#N9BnitY|X> zht*zNKL1Yr!MoM66&DnChsgEh)mPIU0p$vnT`Y4j^W&nUhS$_g?nMHFI-&4YcED-hH6s$Xw&44y!L&7l>DI zN@X{+=EL5oCtRI3WLUznI5tsJDW;7VRJ`Tl3lO+&5aFDBgu?l710l$k%?d&-It~*r z2%y!q{|m3K7vNd~SKF;JvrOthsXG*>|IByHveXwQ6Sg_TTQ72&?ufj6P(kc1D#Oo< z&pK)1+OrG$*+``26dH+`;w7`{H@tY;+hvCiIWHUv?n#0l!`N52E?6%XDYf>LeX`ul zU||4xCMD)}J}r2yDlE*6C1QVKbS$CWU@0@Yw$gsAt&c*}?G<1J9`lz&?2jEc_Jihs zQib@egOjSSIVKgYV_$Yq#v;y2?-mo9@%iI7tet%Eqq$zM+!>A={D(~?$3*EqrFb-+ zi3{JYo-cG|_rXJ+cb&kG5_L7Xt#^Z(m(#q~3uec45e(%Z5Db%42%8-y^y=n~$y7>) z#GhmvAIg-MEzaXwpuDmc^*o~IyJlN#YtnI=l&_KDFW{JGjg382yyA_kiqAa$pbdqE z1;n=u5h}yK{<^B<#aXx5+?=CeywfOkzoHnwMkR=Cb`J|aH;#8*<-EJAv?862BriS# zBByJ*EzrJF!LQ(^f?EAV2cI{vU6#DoQd$3$f;nm5XBPc!pQn~=kqb*ca8lLL%V_~V z(?oku{ZbXvF5LE-E+NoY&>^_ z+OCR{Xn6A2CINe(%{(Bq(^! zY@1xRZHD$xUlBQ6#QC`mI8P)CV)NI`IDTgFxu*{xV#W?-7=TBRG zUj=e*9V|*%rSAP^Bw%Nq+4sP6HrWkQHWQy)u+G3n7_TI3anN_uExS@4m?gPazQv5T zPQd`*xsO05P#JE5Nqw+(#tBcYoH6yJ73Uu{P+b%Y2pX>?vy!@SE`9m#c~pqgP;YiN zB3A(NE{#|=WY78gh;#Dfi9rf<&R$T0&ALPt$Cgt zTKmH5VY~}gK6pjNlt?5{RX3s#u6rv5H77>5r?snsW*0T~b0#B!{(}tyayPAiuE(u6 z;xY#cw^!f(Y(|pIWfd!SpjcU8t$kCI86XD^&P61`_imZX!Lre4=vhwxvzgQ8h%hg! z5a$k&S~neRwkNAHUjk7c#r?Q|$m5|zc!5(8yM-sur>&ZC{4VzV^ZgI$nzy0k^}E-V z&Aa2qc*0Jx)^}xYf)$T(9f4QR8+y= zIm_(oD35B^@v(3{!8(4!?bOp<;~X$)?xHIWoU=&fiRi5t8U#V?iifX$)i{YWV9mv_ z_5*A60L2!wF+cCwbMVVrWqIAG$y|y4%1b8mV>~Zt)AB1Zkb=m?6TIEt$<1@W3$f!9 z^|kDmwq=WaVR#smF5cFCV|fZzD`lmv5c#HYAIHWiORPqp~ys8!&|H2Hn9s|l3M)7to#JSCZ^YpsJx_j5nIDTsJ)Tw!` zsd z^Q&s>-ipQ&pLoVD7eKv@k66zV#{Vs__Ytn;Ofb!Xccg2~@k!5{URNKydcS8DJ)@s9 zkd9Td#8wVYbAF#abi;aXYyNfV?siWTjSY@@-9|5c56;$@SN%@oAQR& zuP^REP=&IIyO%w5)e}pHCB{(bX3+J4ZDZFb-3Dr0tbOaRbjnzt1W!ctyM9KS2RqR5 z>fm{jnE*JpRd-zB%!!1&8eW%)$wD zU&c&&=H*fQ&RIB`adl_*Vf@H_=Pa5|i0uFDqKEgH^UA9epIf~2faeyM(;%f9d2Xxc z#+PeeLnLB8d-8_${{NNRfKMi!v*_*|>bpJd`RG2q{YW6co_PLkK?HD`wPsYZjU6JN zoHs7K+FnaAKOHOus7a=E|M%TAzU7?*%E8qAeXMMm#`?JY%@nt3?@SgLondx(8o?+qNr8m!_BFxECYfF3GPAJYT%xjZ4>` zec}Q7fgU?Tn%C@q;TefE4M_96w&I-)pIHG~wQ{c?-aZlzF6Y5jAl2RuO}ZRcvACm0 zk5wy8w}L?;a#k`L4Oj|5Iy>`S-5*tg%(-bS-7?O)wjlvd7xGCu(dsU;Yar#Y9rl@4 zIewDU?HKFg=VaDmK>f^y+lu_b?^=`kA!#!yQ3KWHKVm%LR3)gau0x!DaKqbdKBH}4 zA{UT@M4VrF);@+Xii*#gdeyo!r;fz`(3O<}IJr49y=0<)vW6Q0z7)381A@`ZICp&oa3+9^Rfv@m)Q*AZ_rX%rYfmpfugh{_bYBjD_5J?J4!CJRNum+5^D zK5ySlg%zhKiu*nL>cWtbqc~%%RovDBLt~)sP$}J_^3Jha)ZVMP5DbLK6bSZ7Nq|Do z9{Ki_*P#N8{n0(cMC?g%E_yq%;(T>Onj4q>BeG%di|-iixHwlA%Emv{Wo{dRWuQNe zY$LlF+Jf#>Lt0+9MsyKOU1&gUeB>Hq$CU<$>0+y`1+B}i+^dI7zL=J7W5MPzp=yqM zcHzgab&eJ*|7D(@KJu}3f~CUi`d|aM1k1P=uC9@~N||10>(sdUT-c9>J2@+@XMZ{< zH1L~4?)^vE=5y)1B|21qP|!IzSyR))OT;^T3ySA|qdo9gBw`%|!xsa{NwJ&?IOA;w zogq4+(ZVV))BugrD4WHgX8MUuZ~9)l^u&q+E6!h-ooN;T1pCE8Rr{lZ>#6-0yysXs zn+owl2c%n+t?w0$B~Z%QfV(E(?g@13&`?3Ph~XD3WH>{`5()4?&u|y(nacY5(>_^Q zbF*+(|AMSLU2zqZwi{LWsB3I$pLk7mPTNF{Iif!;jTFy{)IU-8;3zoG(Ut?~scX|n zlHsW|sVLp~40)|l)-)PmYh8nvt@U*8nsV8i5|nKj>KN&kOw}!9Dzgh?(OWFOIbzC; zcHb`&ftmR1f_L-s3r-VjX8{=xgoyrPLHi=Xb%FjR3U!S(lvSaE+o(D-Xxzemf=dy}H_A z@Vs{E(oM5(zze@j3oX_$hY>f>q`YR^rNm?i>iTrF_-!oaH7J0Etl2QI=5vS?H0Evc z6f;2J7WC`Zr=APhCCGFM9P&7_4(NO zW6&+s;*tvg>CX;aNb4!H3az2FR$5h%CSbKzfwxAS(imf*Wm29&f{IobjX@8m)|FLa zE%~eP*G1#CUdnq)kkYNo+50@dq>2`XoXGHhFgWeKQ=JpdO~vYZTRjs)VE_OMnMp)J zRGZm@XjV31#TG5c&b9mf=z(r7pbEgU_032!M0JomIQeZ}$~Gtft@dk*7c0H>nGvk? z6365(Em)T_5dB6aBN>r9Q$dd4Gpq2mq7M@J`IUdBP|Og49uPK8OIelQQXQYlzMMy9QhmP**LPOwzAXHl{T$4hoh?)~J#;(ec4_%lavG7Ek}V4v{5 zeFRz`tYuTLe7`=p$NqB{&z>~r^>s||P{C>UZHtP$6%YUN)uoSIbA~l_yKrCq>wq}} z#$0Tyx!ibWrnTm~BKXOQyCX5>#odyFt+7rj7S&{%V2y#torX3ET4i|jhZ|7uiEGzZ z_ZUJ?3nS3ptfd#%4w}^FZ zb`VG%3%w4DU&Iq~(7tmP{`%~Bdh$)|4j9^3fMy-Cyy(WNWkpvnS^99~v89h*^Oxn1 zM1Hrt=$bz+dn9u2(uc10t-q2-tE-08ZYV)L7gm-Mp}0(7DbRXGkSVV<&RPSTo+;F7 zE!^NOEsHr-D%?X`)3!)k$@9}Dse`Vwm!HghlcRPz`0UO8iv^9mCOk2(a#A#SQ@}cf zu=dvi=!}`p?gV$XM`!A}2Ka6Ows7CM3$H);g_oBTPkrqK4g89lkhKD2+rC&?<%7xk zXu{|YP_k`9=+6)o>}>cNDvPBuF;t7iT+HK^Tu?Y0q229ES8Pc6r@T;{7LTr*rrLxa z27$fX%*`T0*si_jF8E;Xev7WQ2<`cK@S#U-xSh|m0#un})E^C%mE5Mp@i3*yQhp7a$90Z;NFvZbtsLy|-DEv0<()j82Zc3*>niP`udMy1@yx)`= z+~Y^bTbE)xaC^wLDcFEOb{jIeu-#dui9CylD#G4#U;SX8ISVgMxR`3KIh%cQGhs1{ zZyjZ@Q0wp+`}=9~jflZnHtC$0S5WbtiE|e|Fzz3VxBT4q{kV?{fp#m{HQ#z<-zUEo$Oy#qH&~Bi+RIUUj=y0P39lxuf ze^UB%(=uBV1h5A`y>R7za~D66S5Wb@U^sZJh)gu}#RcZ~6x?})V%8ZK_7aGclR6`zHc&I`{8y*!S6mPwxNk`Jd`mE$O;K2R+cSP;|f`Q}&vF zTvl zq8rf`#=7Ef>jhFq*8&O~^E1bClc7{LbeT(MfmOkrzyl$# z?0x1ee)qtCE!uFt312EnXRmCVD1g3n9+HpIO1-CXodpaJZJ0);&z`NN zw9+N=Qaq?h#*oy%3mU7V;;0KRQRe@xTW;Q`K*R~Txkr%a?u7P(piWYzLW!K9r(b-A zHQoV4gNe9S+5y?t&z4BU>_7L_YF(i2|LlTyCeL~K)k)7RUNq_1MX!&4YT^5n{yBe@ z3b|n0e2294-)Q^bxXl#cgQ7^Sg?A0R!6P7*@L(w9rr}5N6l!>!2~xE3iFh4_J_@z? zDdsDk=4&|%EOb+$zv-`!x=&u!lQ`Y4@4;tBp;W}yC{ZfmuC&JffqM2KXjYN=;yh&+ zuMds#-KNYWUD*G%9@u6IpdFPH&v~8#{EEiu<08eQ`ed{o30nAgtd>Gwk5noFYYeRA zVwrbZIJLkvEVr8FisH4-Y3mo=>M!JiK^afK{y^e|&{0@TuisYy{nLP@3ufP1I)CWb zjOQJ|w%o|Vx)5E#_-B^8R>sKL1Nzroi+PbQH-eqBql9g$03Yuit%Egp!KQa(;x$#Y z@=-2~DPuIp=}s0s+|mUg1yb%K`Z6x=j!t@^y-HrX^VFIpvr5x+fOzJ0EA{~@hbjr$ zxvao`6pq)m`urhc%3i+f)sp#l{ zRQ2-TeE+?9r>Z6Qti498eX%({Lp*9kUR%AmNOw&#WD_M~m(HCV$J{?@Ti8V6y21|E z1KUsm(tWwS=-QQleJB;HtF@l@xV4y;@InJ=-6xjJpZ!Bx;zvs7&3>bN-W{ve&rg?X z`(8f(u2oJ>@|%>{d2Lfkg_E-XVLr)t?+RT#&_)kza|O_PJ>>t{qI^Kf{5!u{I{(fG z8?5^l3#b0a3pt7I$8rYwH6Uw14I?xLk3RjK8(yC6pF=bvyPjP=kf{g06a`pabV;=M zsaczGDx0aPEpe+C|2`T{*8GA>aJsefN9w@q)Tob{zBMIY`V69Fk*-u%4{Xu{Uy1^3 zQtMVi(8rH*R+(Q~T6MV>iyuM3-@iwG{-u@k?ylTQRdliWAMb(fS^=8I=7XX{`Ku39 zS5ok^Sw}RL>x%5~J@CJw0G(|ZyV|qe^gx#aY&S#I)zEFx16>NREqb+!=XTQrT?(+> z3{_V{w?z+hDZsYq)h?dfO%HS_z;-iK+ebsUw^nv3!1gv`UG3efJtt?ls1Oys{tc0kBC*+wQyfM+> z9pJ(8;m-oqc-KQPN?C>>ak~v?LQ}L#u=9 zP2k`E(f{uiV|0irNaI?lf4(+GuR}lfQgz-q{uh>*8`FF{84CW!SCK!~5XtxoCwKNGzw(gkh6 zMF?xDpF{uU08b1-8`>Bl*KYOGzii;0AlO~>a6HM(w{Zon+TY<9gbC{|KZIZ zECLR?kKJEM1HqE^%Kk8D`YY)RKo7b=keX}o9|eI`!Y=@|2(i4aTz_8hAGNu|{Csvq z31rc*%lh|{Hp~|%kap`r@{Lyo|BCwcVUqx2yLDOPzYpPT2CR{Ap{^gUe?=Wx0EucY zHdOXkRCphN0&oXGIwvi@K068_;%p!$dUU+#{oKpH+m{*dkXU)2$#LE1u` z1=9TbGWstoVSG@zU}1C3%;o<95*U`o;o{(9*JzuG|L0gOu{th2y5&|Dn@*0pE|@V*D>1{6BOvqfG|?L$~w~otcIFzo-E7 zCknpbL}e54|Im4W&;3(iG5(hh{vW!L$fouGmEQpN>y>RX_#e85f9T9CZU5?Jh5`h5(Evny_O=eISl{ca zqyIsH7ubMY&+HWcixy;EutTv{&c$B)i-N9W5cs@9@~UnBix$x+kfFgHM>d-Ns{ydD z@BoAF;|MIWJS-{cg`r#N0v zsK%N)-Hw@Mp5LJm`*izTQ>XSyxdCyPD3Lo-O2Bs zy%{+O8^kxAzYTR@o{551`b12xbYpz2hh z;aN7)n@}Gf!nch7koG;A4;@BV(5E8A!r+B$4{0N7O-$r_oZPbi(KoiNVK5V?fC`$+ zSEwCbV87t`skjX_$LX}7D7Gs{+onjQNVM;b-X_=NtINRzzsHWIPf_Fmb9)}+I4`#@ zR=fpQ`w@7Jf3aom$|RltmMV6@j{jxc3&xw7>|%a{y`BghY6?So*P=YptxkLvykHMM zI@efDJ7SkIoB1-3@vHizh;Bi>N*)r9XFGpHEs}N(P>#s?`S|&q;{BCl@X@$sF1oPg zvkkYhex0#1jJR|0s=vVcqxw|0t0PUMD|xq1EQYrV6ox4?DQ2I4VBY%?Ck$-fB9_BG zw5C2rxPpvjf}BIdJr3RB(_*ZUl|A(Z~A4O>lO3hD|zf zFfMJHPsZ;z9@YGGT>S4s61*zUpP4SdQPh)EFC_L`^~bfOuCfoQr0Oo-SS)S1pqa2o z(06vJKVH!7Z_n@^uJNpSLq8vSUv#FPr5uGv)A9%FGJ(93h%iO3`ysy7Mtw4UqB32n zqw(oW@$9eo8pk3XE=4(~^0>w)-9Ow3NtF2u)o6H7f#W(SM5xb90D%R zC@1>&?@|f)6D3G{y~4x?6mkN3A4yBJAgdhM-uwRX;FKt+ZwYMFI6A$FdX)sbJY=4b zKXf7bbXJS2#k1e~v2sqA%cCV0>=0seYu?Ffi;Bx5-`na~@E*I*jh6t?ceP|3T?4Rf z=xHTQF#`Be(Sy#0Z(O_PT>4_JG6we}KU^bA9d02@MA|i1HVlqjgQMLSG#H+ne4(jT zY&j!kST{N!&ft(9mMOGg9e?QN(HDkWhFzM9j2=-%e|Cf6bn--f{mfn}ydTGET{iRO zgczT2%Wa(!@6~XhZ1npYJgkiZlwTfc0|Tt9wd%Tb%aXCx`4Dmn5nI$ml$?p$=!?t_ zC!LHk+0ABKCNMAX7*-eN9SMjNkKPd_IO6nO4Rqz;p-sK_F}`HYOKl!Fv0!|jZi32# z*n<}Ue5-xqZ@$74{`L-H8`g^sWrU+*xme^_;|7DF90}bpfXynGm-7ITxsda)xO_r!Tg@)+vUH{ zyrY*6d~_wPdUSprw1^)HXs}E3omJ-a)h+LuM%bgr+O^V90h+iH;uF?Fd9cGnPGU2< zLIAyaT&Tvl&iG$~nXwzg5?pu@x^T<}BXg-$mZFTAA#!Ooe`{7)wzU-A6aUxvfRX& z=DGujodHZ&r8GkU;Q}Wk(cx=IC_@M$yfBA0S_W0&v1=;?kcA?5&ZhUvprq1ATUQAu zNHo5;$#n;_`Ba~RJep?)*L~&#-G?sOmuDTiSjWfT9iN}*e9Z=nTe|8@N7Lzocy9PB z*170O9u&JQgu73LF9f__*0Y1g=ej+xt*Raxk8X6$a^d(mU?hyiB1LkGQ`-mS7f3k| za|p*pUgc^&)2=L03;ns3cX|Wn9X=3mZ&w~y+yd>HcKEc z-A6_TlBc9i8q_`hkr^uDq9_L?7$QpfB=k9w1wgb6mmO>ixn1%IzIO?6DFInyJANnv zfBHK}vbp^N2F>#Q;N^F1%q=jxo@PCNjSXKsuU==mH12L=ps!Kr_6%^f)J0AV!Nx)y zWOJVO!?aTEODg4cGVl`cTEoPYJMU0-HKwFRfBf#%SX6w)BU#!L<$6g2!f~W|uwmL` z@rBTR+d~?|lczLdzLOi)4xl4`v(sUv_)6N1?THrU4I4#ZnBagn2$@G6Q51c8)JB*0 z(BT`v>G}N~4jg~}_=oHUceg%^M&faBem7zQGr=&$i1$T+3>U!-pNC4~F>Rrjusf}m zX6`x^JAUJ?C{r~IcT()LK%W2U)#LaDrD2D-4`5I~#Q$qU7py38n>rRC42F&dXV4ej z-EY7{Is5AU(0)=9f!{r6s27KlM5VHt?@b_;J;a}8q_Z$76qFj^=c}GvrQP=ZcwXfU z3v8Up<$wcse!RSKlI~sj$|jG}`zzP#`Y;7Pwv(vTBX)x;CvlzEWw1pAW~|@ILv3hK!shF zj_}ExRG<(_8&zTjU}}tX__!$krD~o#2)EBiKg>qvlcBip1)Mo=U@!M-a z6Gyia?}j-vu>fwtQC~u15#2>W8@BU{H(4J6gD47)Xmt)aQs$G98T$2+2j!577>40g zz}MxB9o=}T-IS-3b}#x)(1_0hWVul58TDeP%Toi0^?a4Y=M~?d(2+F1bAz67=;H~} zKN+4YR|1ks)kP#c3o7t38`5=mCnYfzKoD=geF*JXRe%0IP8!#Pc)MMdj}i(?0C_i# zS-T-`*Mm4hSb`I>QIfYa&zuoU8dKNw#fn?j8vd8 zlsdC$Ucg*a)Tw;yW=~|`YN=Wo>qmmHPFwz}}G*jM4`)`!F_GRgyYN{X|;`a&2BSwn|)Rc$_!HqD9 zP-`_QeLfdvP4mF6~4uUf?d>~h}tL#wDhvv8cFSW1WA2~0sFu)< z@}q^9yzTev5s=+s=;$=CX8R>uEs?&)L+dU*{{|{0elvo+1roST2qwQAWTBOYBLwZ9Q9^0&i3OkJhCfSU1lO zO&mh8fk5z!tm{#`(I^0mHF^@~UFR+iDGx|UYc!8pM)WIM!Su)pnB2aLji z-Qu_}QyD7gq&T{i(UFJ%d=8l*Xg=r-@2>kdpCpu0<`2aac$eqv(n2pdzWMT4p1D6S zueTjL$jr7+DRNntiw)8gM`%HHaDsYyKcX4>5j;%HDt{0y2gG|EGGAo0jHOo+O|!7t zezMiYHCN?aB`)98u`lnIO4tQj&^#eyc7&;zcSw;!WJ&K_$0D!TBBob_#YhD>hH&=v zcX$Ae-_!IzZAzP($J%q0=9NtJ6yI;6O{FX&-lOA6bG=0$T$*w<6C&Iy&R%BsIR0+X z2dCylBE~~E+2nDa!_HC!r-EIP42_}TGq5Pwz%*wiq ze-b6GpxxCUUwm6%#T`+KqU$Y0!)F#bIwK9<=g{=OS%m)szhYDG*w)!inRn|eAXdDe zZ$~TnnUrG;hNrGzgdMns@MT;PtcC2uK{*TbBX*b~$$kp*6K!-EZHk7Z3O)Dl z$$CoAMRsA&xzLLcS5V?^&|gTQSNrDAzjl8EmCfB^i`UHvd*o1V?lh(-W;`MnNvZ;| z``t=Ft4YC@B`b#ckeE>`KZmNQA)(T30f-SRvXkUBVAA(>qba<2M+N?y#V(Pjgl5HC zAKP3lqDSG9B0qez-(1!`HYuv_Nb!GpNjp(9jqEHw`UWwOjpJ1sph3nmyghWn>8vub zr(>ouT+PY8PQe3YRS17d{F>=UI56}-zMNRn)t1a&bOigK&4M2&Ixuh~L2b2tzx+3R zQE100V<7^Z$u}&`)iRC%mO|)+SD5gK3#PN)MA$|`fdWHRXqk|GPjWfZl#Z?%MaBINo8vGpBu@COD?qNach4*3i{UOT9W$ELw-7R^qyd-I=^R{Eul}!Z4 z<60R(Bl5WP&G{TbrMKi*Zf{3G_^qGTYO@_Rz7b>bmCrE!es9=rm)YDJ5KUJ6<-^b` zG&zPKSN51(|7NJ~&9b9Z^=gS$FxNYPWQd*LY0eqRq|kQzPG-H<^k_JsRP97H=*D`i zpSe8W`Lt4mxjO_c{^dBuo1j;o0O1ab#gPH!JM4TeFm|^ag%{lQb~*d1?aTn>7bzmj zhyro+l=&NtF}rzdlGCv`Bmj_9`Q_kNrlj}R4z9IK#kR?3!~!xlg}~673t!vk<12AR zZ!YW7hu`o*uQVK&q}OHxn?FR&gGGqBe);*4G&I#SH*&gB#^grS%qO&{9lw67d$lW3 ztY~H1q@U2MXl@smnD8P+s;Hl7=y1tlo)EwuE+``3kTN^8gSNro8D-0GJUd8Ig!}0N&B=Wb%ip-A;&@eA(+2>$Z{GKC<$)b zp9te$I9BsYS#J`=RP;k9&J!lSj|NT*-yUY^-0xqXrVPyJ9Ees-Mwu)fUVo+A z&du~-FitnfwKhNvMX15s*XRxIBc`2vifBZ_Q^Ls{TAA$r^10G?vH>aDLU^yd*wV%f zjyHU?mt$pR_Rp{4^rgKFLKwMPwWj%d#cft{;)C571@G=mE4%{ z8$OnA=@VSYy_b%hEI&)wl=sXz`jp!IX`!%Y0@HFp-_D@mwRZ+QUTV4F9D!qKbKmJC zdZKL2X%@}@V3`z-t9D_xplv&f}B}G&;x+tlxTtXB0-$GL5Ey?=XBlN1kli74&BSVbfv{1f@QHh!DilyZQ>DT-y;H{Ms;V zMSa>C`N}FWVN-FP3Ee0CpcizA;5-NU0~^K)cxyN^bIiS$%iM&VDylBd$^y-o6Xx!F z(!~Uo{y5^2K$i@Nm*7Y@OPiID{IQT+@>XIryueQKL_MBi&<7_tQHMpRzR2M6JHG8z&k6ha5v`8 z_^X<{SHDC=zt~rU=1*iD*aciBaei;6d5BzOo;{kyA4{V@Cl>HFRb&wNA07o&s3<^2 z@&|{oVAXq%oNr1>BMFYls~=__-hpN|J~8%4I6*n0fr!J!a0s7dq-3gnY1UO%ZK-TM z-GiERaAX$vm{nIc#;>>8T?!Gg6{PFAv&Yo5YnDXqD}6T~t{9s9g<~?USy2Zk-34wB z?l={Zj5${k2DAN`9{ljxx0}cEXxCZ##ODm#5A!riYW^^yTu4i>4QxRsZzJSDhjU}y zY2%z%?4O2!*9CFN`5Q8=AbB3&HL*B7d`xK$- z)D>8v4kt46QylcQs&v{!U|WasqI35Qq@2rp)89K7{8k2j64$h}lQuj2txcK6+S3^G z{Omqs3MfqUiY_s*$?JEy5&#Vx#i<1fh1I-JZ>8CPpnzvs1?bxCXx$gGobaY*Fkho6 zdRwhA9IP!jsXjXw>-iq_sGWud1jsG#h=fH+1F#WRtBXF_z)Z_`RRl`~xD@Ap# zP?Asnc5Wm4)-EI97V{Y*Z?Qpy)}!1xde?Hako`;7m@*exH7A=l1P7(^shbeC+cXQ; zG^Dj7qw{KDf+88L?kVf;A{jH~Ds{71LBM;EaSQXTYBE6a1_oNo7QH91Ov9l=g+I5{%FvpGvV+>rj-(^_!X$=znuqvG>g_eopQ4ysHS`2 zk5}sPLEBQMF7{rOMGZ!J|5H%_NgSq|r=esULDx!?Z8tC4XNVcJs08;+^-#8DMjEuE zOVg2e@2b(j*1CxH5i%*KuuF-8%&p)M*}m%HJFnlHv95M_)5VN9N-^cn2GKV0rMcPY z^N{8Oy*2LqdV}Pk*eB~bec5svtC0&H-VE&i*Wu33fl}{v9h)q9;J){#M0ZUJpWEbS zuNNEJNSoSQT^I z|I-V2`;}l19^&G8!W?ZVzYs8gPP|9$Ru!0%x`8>VgKB9)du#ioq4GjwlYcb#K5Koi zhW)uT1Y>;?5v$2Nmj>zqqv(@@86gFq68ynNrR#!U~IV<#N8JbPb;Pr59G10OmS znAJzQ3t5x`4cMKFu3894jzS^bJbpcMPT642ZFQ~KPD*sf zWveLIq`J|nC=g%2we$Lla~A2jb`I6?MRLRM_2$%XggH}H_A~C$y;Fq?sxyaD#-E9k zE8Su@JJXuLFhlD+MP#(29#YUS zlXUS1RdjDJMcv-6KSyH0&l{uhk1@tsnt*y&YAQMQv{4tS5+)rw6?jp!Z|0m$_Z)_~ zKP)oh865Wq3AyQS&Y%E!aa7zKjx2~ctZgFG#wWG6=Vk=^+DJAZlZ}-}9aAtTlcrcm*oj7*_ zzxCd<$VeRZtrVIQQbWSj73iNrDeFq7b^QF3NQNQ9vbUm zJ5#R{UBD02M#NptzwgN7D`gs@8ketkz@Q{miq|;R^Y}^YX zqZ+mL&zDC8@g%Wu1XzXBwlmCVCj7AWrz^YX2)BoMoDk2L_N!@#vo~73=s*QQ#)6fJ?OUsfMxC@c<-G9bRr3fvN~!iBTV{$??Vh~X zG3xRr_lSVoft)~?C~wC@XJTt@(gyQ#yp=V=SD+b*$%VwdS`kx}+(tck7G&xpcA}c0 z;%os}Y)5?vJ~WW(-{JX@y)A%v$Cjt?%Y-V;SD*=av~xd~#wb+#9V_(Uv)GFqc@ZA@ zxvSln2zV7FNM!wo4LY6-%#QOWv)jgY)#VZahR{|t4-{{Zu$6+dVg9lfYPn*?qi*+o zT{(i1iBI7l8?n@~#+@bGp}XILikjOWQore4M0bjR$~az$P}|`o?eb*WIj73ko9q@U zXnx`=_&Sw9Yw%T@NTgjs060NgK5PZgJ~+HTC>LL@>e?KU16L8#=svhxgC}T_Z+HQJ?Uvi2Zt~&}iuLY?a*KsvVl8gpTn-})Lr1piE zUcZ|2lOBI1k5*_()X@#`u(0cwjaS(&RsZv4_P*Bg8`Dqxw0*oO#x!FXWq7^^)#yh4 zA)fk;y7^x}$#-w`4sC{$&2wX17qZwtOWdAdb6totl!m>s@|zm+BO6VjWgL>-W2mM} zVhI(GhZD2k#5{7a7h4_DQU|QUvyOAlPxpB3#9Z-koNTExxP)Nv2C=60OldHS>X3Z* zkT=vPh%eXqu_T(Z8-1X2%1JX`Pz&y(8&TLj>A|?52YIf=Q=13{-tY{v*ZQ-5Cgh-r z|J)heK5{dcpTRn`S}OalnQ{5ttoW54^Sh-D=^?^xy?KHSgPcvDBXw1nPW}?Zl9uR8 zFsJS~|29fQ|LwMn3EVD8c&hv^ifRdIVR>ICc3ol&+nuZ^OKj#Loob6(zF{UVqoCjK z%obufGvgNC@*%59p1ZVT_33ttd+>C9z2sCeTUnaz8h^1v@tqJw!urT29%hVK$P?R^ z+QC|*gSWll?nd~MA$$8~A@a9}i7=9Iz@K_S*|*qM`)Tq73IuScLxF0}1L=~SgKYv( z;V(%ovWM}}U6Mi5d%>V)^F>s@GhEpdZ|GL zo}%FmrkfZq{;XC=$)TQU@s<;6`&`6lJmz)9>Po+pxp=7OMdT7-xWSJEks@*u3Mj~hb%MUV@FOD1mCK?i$*j`ws|QK< zouaJICPgicEO8h9%yLwOYDKYE9qO?eT3dn}>*xTDcpbM5N94Yn)ph26KG53J$S}`) z_hLm2kb-n7EmrUBs;Kflc`j~(6DP{CBT1a85Xf(b5(pSx`(X{PddD{OUkK@b_*6u+ z0&=$td!?v|u`AXkw{@q8E?m^Lb0VI@C;6C#tQL>2&^ZvUvq*4C-UqIj_XQ6ouHgDs zdQRC~(UQj8pt|@yV5*#ap_^xy&|qD)W3nA_R-}{^rDHK8Uv$lNRZDqF}4RM~zG{xqpC<3pr}*R*iiAmOnI^7jk2bM#R0I>oEdaky-ax z#J}n)&-K57)H{sfU6WtTKjD3rqq?RlU2rFFfd7SS6%nTOF^_iak`DXV(uV&L4gpo6 zMMFMb{0o|037aAIkUq33dd#nVS#QRla&G*J%mHUL0raRwR2Zm@>n$UNK`D14D_%-> zx{!HaFNU=QzPwB~^4>V&v~n1v)D|LnRmqrqkcAG)%dM*_%;iGsns!Tq6T?|81GQ&a zL=fycf#bgnt6zmlpQW+34d{xIr(sxHXb~vmJ6!SeM}ziU$)DlJ`qp<^H}9~!$5&n+ zV8#T@da2;rkjZ@@kDTlY60lGbTPo|iyL{8037@@gntu=Y`=TvYiSXZQr|_xblcsI< zbG+T}UU7daM!$5gzT&?3Gtj!ckn`EwPyR9Im~QPJj*7&bFxXblL(5e6eth@#|!^tVk*CCo}vhkkS$r zHHJ$6QTIC4r7Ud!jyhJVcjz7aWx?XcWMFsvd5?&$be?`K?jM)h@xGLFtmy0(WPo(> z&Tuykw?y)FTT7bny+PT7j!M1tyV!K(`~)0Ef;+@xMnCG_0t*3_V2}MWBD_(%@8Hb0 zhoTNNwET2@y4xdO>Wq6#Tm6PYG&AqVfRHUFXfO;6x$vXtdVYH`^gbpp?tN}uxj!3> z)KFXXmyyKYuoCUo{Ot0Q)upTh^89U-mud#3zHw_cGjJejo!TWmaUIp@Remyg?=I7dp%18kSD zy3pIXVC&&T_^sin`v@SQLptn#ktZ}F;O%$3&aMVbeR!P@T$3)nrVFG?Qss$hz#Uno zurKj>qgC@=u{#8$%9EBHe(shdWYT;)ce2+gbmGWJon68_MDc#ea9e~(#`XI zE3=3nNC)?qUhF3gBv8r^NaN}hMT+I<+!;QDCPG{6D&dO|cpvxdJ3#FVY2zn~D~sjM z(xgHkAr_bkk*SgmH?oXDg27uj?4kJ16#b_u3J-t-*&4`kT+InSl&mN> zA}5%n(XrC$Hdcw&>n)yzyn)KV2XJ5zKsKDj+k_GW-6bXtS6}g z(f$zp+S%<7;<^^2B-|f1@(u9uk{^+(on=>GjQJ4zc6=mKrma zz-yjtut_-VDwqcG^$7bJmg7h7!(@4`&On$Y3>@2gUR7w?ID2rX7mon}4;C^*FiCg^>5d?65pnO03bIUqYu(y^<5u-^1K%OB9Rv;7!76qlOxQGz+%$smiC4Jfv^Lt++TFN6h#%J*qM??eNca zNpL^1KO1MaRR6Q_8MG!=fKl&l#E*{xfzsN2fjZ4t39p&8LbHG!WqlJ=l318zy+YpI z*>SeI$u%AJ6{kM(Ek5uBNNpbt@@Am^s|Wfr3o6P?yRNOuQcJozgcj|)fVnQes}G4u zAL%lbTB(sFLftb&YUVFD-;EQF*+8#9%p`Nph^^fJvt8N?LB1tkVkmL))4YJq)tV?J zY#-$phxF^pfGuJH6*9ExkHrTy$FKV|n`F^iV7o!a;E6^kYy_ z5=Qj?40YC$IENwY$HfTI5);;K;HP@rI?{C{iW^(jy z9JcSDW+k|Fg*KyGipBu_RfbGOm*Upe1J86-tR3h#y@?&@1s&L)=Tj}7l^66d$7i|( z)Z0zMlm^wXk@Lo(;H_krcaX3a0C^KO4eDxa81#csS^;X*+`6Pc0aOj_T~viYh={R( z;za=A1$JkP^HITEYTkhqek!47 z4uQ71hMQ_1=FoNlj!Q;N$(|BUnHOWg-xlw$zUm)$gGM;}3fjEj@dBvFSn3!5^D8st z3}-=eoJ4dOstszO7MCwqb%_DQy8tS+x}cgQt?+$N?(=0_7CgF4E+0g+0D&Hv63T{k z`l$~Ove4A&q&7(vdHsr=M62fd9`|U4k_MTwj2U`wlX=}W(H&U4pT_?p{s1+V>uO8| z-l}tX4|&3n>ngS%Bci?;6$7&%6!;Z*Ln;~^m$amo)Km-n;K6MRvG=E~KIxx*>mbk? zjHsz&JJ17%OjtvN@jKad{)46@_tyi|?E(Fhc92NNcG@~%k=8^-$TC9{Kb(?+t7ajh z=7ePhnoyHH+oL6cS!NR(fk`&5y^m0I()$gj|9@{tagjdfWzR=(e~mZYgqrU^6Eqy+ z^qL6kbwnnO##;SmSb%a*!ehg$b$Wy_E~hgD5N+;4RUBZ*F_CS3EDUCs67jK-r~q!` zmT+76%GCLl5>u9vlf-^JL@;HK7DW6-3UjDP3P2hs>FUa|umf!&i76YuAGehRzqIfL z83qf7$&IyzJc{;kAC|*R6&VS~b&;5R7sGv`*L^|R>%a)mzKt={hw?an5{}9XZTu~o zH&7_BHHIuS6mHg&HAxc)*$Us$EJ3z(KN*{5%ckQ6Rs`|8ARg>zNL;LR63@OS6Zjkt zl_$ur1%R_PZ3eTn<O&&L`c0%^C#<+T`MHL?F*@FCD7!1DrgUw3ng_6|2i(?eues@e$~K7V|< zSsv!3C@@7|qUWVlg zh4oiM2ODD&6iC4PL>5~H%Lq+UbgE)DH(uDj9)bOy2rAMCwiNuIqisv>8zF5pDXAmi z=#Yb;uv|;tCdXj zc`9xb)*-veXd#t|QxCqWaI4uecK_bm`duKa)9o?n@B-Q!8v^0k7!wv(goKvi@wc>} zwp7|277jy1FGJ)+s{ow%YPwXzf&}-*Zf%ld=%RG4TW z#6_hjSt<4`*s|?ma2TUdR)J@^wxve`&U@6Qx9%ESxCnbr-;Od7gmCHK_foidVW#cj zQ5_iET&-=Q%1x0OlzwPQ6E~7Y?FSSe<{OL%sHdrF20!jdkI;rdirg_*nE7)B$K>d# zYP3Jll+E=!lcfs^X?6p>aMKPad+e5vShZ5)tGXPPkA8n2H#=5FUA^d1P9t^d8R%}M1zJ|{ZdxAh9RoEDS@~C^;<0~!LbKTc^71!%fjXpA1 z?BC|;QopN74USKQ2m_y109Fc2--+IwVIRC<+oI?ISlh@XBIvR*E6C_U3Rtm{G0er` z+Xotn?X9nYu7_~!2USg2xUv*sI z*n_axX9J>@AZ1YVdn9Z$j66{ z#+4?*77X&s3s)E?5B>%9_8>u$rYc1U_YUfF!idcv_=P7I&rj9-pCCt?%{Tj}xC$MID1 zy<5xj*;%=cF&_Ty(#U;4J*GHXr2+O+Q{={2I(`gZQ2K2Ih=wFQpsxQ8O8n6y*J6h*5o84y} zj?7a~Iv537<8jY=qr@hZ|0pxI0-8YVJ7tZTtW;Tp*?doKY?)aTJ`d}Xioq)f!LDwx z;BKOWiasoA$PpaeX)W?pk5E(tJ!Ll=!blzL$NK_|!R#BfIJUVF+5ZJCl)=(KNq&h` zdAo*u*9NntWjSJ-tiO%C`GCnUTXiXB#LMkm_KF|Gry?=YAOF6g{vB2zThNAlflwib zgHp$aWdDO;z(t;EU>T!eksbF~r;M9ZT(+8?1c6NiApE}bkeeZL>BmTY2P4|ZF9X-@ zy>E7!OkiBMpe4+8P3L`cyCss|Q;RrLy1JIhg_C|Khj`6m|0;9VG495|XDp<|YwDQe zH38r`d<=1#a)#foGD97xls=E%McsggzhEWgS0oOo$N)M;q=88l-7Te#QOR7%o zxR-$ZHu5ZRO*^$lD~q&%X?>-?;AO%Oju)iE*N^Q?{9d1vUq4t)R69?H& z@xSNx-6?LpN+un=!_5mkW#S=OGVm05h)11sG<|l*%?VV_cgA88xI!a<=`c&j$5d6z zfNwzd^th!Uw_Ja9)5?hscdT4N(HJ03bD;{ zE8p=51ZA{0#5wwDu0qfPJkSb4S+1TQP&N#h7BFKc;$o;jl{08yVmUv+ZU<4IJ* z)TTh7+e602#Wt4kdRI1Myc}&9oE*Ofua)D&;LvtNsqeKo%X41wR(GhR4qYmPKQpR1 zz=r*MHlIQMk>CArSJW#hzZ|wS^U?OGsrFZy!jlEkG`%>7dlrHN1#zzn$H&L)4?J=v z+Rl)ciUc8Vhts{Y@MlrUsH>Re#9gFQJO>atKdgmwTpkU1?hDqPX8ts?q%X&Qq?h_S zPIcrNEm}vPdfQC!2ewZ-g^#I+(PPYODNa*2m$8V()#k&3)2wbS%&Ra*iLIvHcWVzg zY$)Q5fsk)Bn%W?67bv5#G_F`{D4{1dRw__8B*yH9WtJ-8aUx6q>0lQcTmdv6l7`R6 z^F!QNDoLoagwVjV9esAH+@p>o4l={}6Lfk+)ta^uqW*r9kewz@ieu-?P?-!Fsd~{A zLE5_>OcJynjT9L}RR! zNeDd=Bt}>Sc%U*x#JAL19|s~rrU(cPt0Yf;ww{(D#Zyc}Lw=eew_2@%I~lz}6NVOAbNwztA&6`@!-Ql9v7uF->5!w&@47d(pHVCSkY-nylz4$q&BU6riQJU`A5>=$MRPIvx( zSlVj_Xti*pU?koGif$Qa$^?Jl{p_teyx{a85c0YfTbn42WpAZq^ZD($cQ{BkCrYY& ziGe;)gszaugGfVK?{T^I)$7Mj!MatyG5S#GCoHmlSQmWL%H0s)&xrfgqsCg=m|ouS z{V$fFSH3Vx2)cpvshYs;-vMkTfr`JwL>w+Ii$?4rtKC3?_V$vYwKhuT4lh1XTIdgR zjR6y2U7ctRmZRVQCbpoGWY}owbHShX4@uYHHALh$h-dGA@ zRFZ(m9s*gQj%L?8*V zl*K1T!3l$TM)F|QetGB7O6e_7c;o7(%YY=nVbmg*mQL;9=dnel=dz$G((PoMNKN=*s{KcPqVq0-K5-`pR1k9Z9S;25bAGcVXO@^**yze~rx3K<4QO#1@LYagQs&x==GhM{GIh*da!Wy* zwavP>7&xl$H=x3uC1z=$t^Q}?ME1YcP|O6HtLDFKI+6D6CxGH5|0N7E{iyk{^-aMpwpXRjZeXjJ z&I*a#v3x;kE912NFafwcQKa2iY}`H8am%-Ri1?%B~& z2I9(BUIOp)TS6WKE_?&S%-eVGUcbzdsZ{Y#WD#s~j zs9rrK&ePQ}=Yl&8I70?Yk=Wv=i3Gnzm#M9dujw$#7zCKp!dl8j;?NwZtEpyJ1O2|_ z#mOrPWAu;9LE-Bia9 zOT4%2%a&1iFKUAVsO8pxX*md5d*p;VCift%ft|ME_bU0)r-~TPkP#ApCdlk*P;C%| z^A07FaznoxQP_89x^Pxe7(!Oy}YKyVAe z-5r9vyE_E8;0%&rL4vzGL4)hy1a}DT?hZ3B^UZUr-a3C_*RH<1SFgTS)Ie1;>CK(E zXaZ>uDL*?)EPs`KqvkZ((<~38{eU=kZTsfY* zeHr|5$*;ed5E$@sm{lS4)F65IFD6jKfqBEcn^f@cRJHXUw(qGb=#u6fp9~PqIdbTj zi)9Mf@uEf+*Pq|dw&#OfW^-$+A^(m^5dUpIx+d}#LPm=zWu_`Nc=xo_mdD>+#kuW z_zP!iO~>&{xpZH)ZK^fJgVLM)>8R&h&%3{J(p2`q7lcZMs(m3A{c{gKkyHWl&7P0{ zTDJF6td`rgBwN4e=@bjcDy)34qL#C*2ut|tInlHMkWoU5kmB6jY_dBQu%1d*0c zewlvGbqzt&?jPbJH2#}8EP5u|MD5n{B&_4}Z)=yc@)Dg#YkZ=9_^m=Wg(?k)Z@<@h z_lS~op?=?@4sR9L_4seLp+(49))U#8XnXZ(SXtiC6FC~D#Ih4{aXDzq)%HH1PJHd> zPWk4dQyUP*fMy~>h{v*j4miqupSaxj9$KkQ!mj=Tcxn*eO@Iaxpi$W2GZVUV`mFkr z1@{J?5{+tCE1l4U`89s-3ry}62~4j{%2(}IqYHOxIgW}lcuz^YkcpduLVOWCNE_zB zz7zYAx5_Jt;YmFT(@`%L&8lq^FnohT!%lKJDjz^$>USm@xDyHNt!u~p)7%l?yc!u@ zJnjy2-J6%6ykGf1=WJFo@Zk=MguUvC{g}Zm*pwrYqwQ=Gy42+SMNxf?-hN-?(AW3d z#(-H37NceI7rc*T(O=!797_Ka0y^U0>>e=)x5y1}ft}{9%oGf34`Yo=VuEB7u89jEOUjJr}q& za+b3qcK;i=cV)cw3ysqj=kH}Yb zZ&4vwE{dw(L#>e6Z80IA%`@Dldi>cHR&*tc{&UKUMgy;uipG9(V)G{QJoIKzp3!q5 zXa1ujU%SU5a$s~L#LnRp?oyE5Rx7g1l|ZYD%eNTR2v5e{h2qCjy)Qp>Re5>f zc8;PR&yD;EqxLLsM_`IVaX}t$M_Wg!s0v@mm>8*T zs{Biq>N0?3U}EV5m)dVA$Q9aWX68%YGyCmx1LD|E*{l@r0Gk!b=2(MsjBlB|#D_rK zLm8T#1BUc6yaK`ZIP5cGsX7ud(dJnm>&2FI2f_rgAAfdP6z>qG_D2tz$A+Scl8ns4 zxtWxUzAsx?rgxi865j*50x4M#IT)=z@^)bkL=xCdTq(-JI_SfE<~T|XLc{f(t0vAb z|Ju{u*_S<bz}9TWtk#uT}#xS&1=s-umXeVFfjf+vcDNwdqSpGB?w!c?9tJGAviz z^QuB`#C3)DPxGVp%>&Whtx_;v++bd`v=z{kCXt=_AcuNvMJgrbXHpbu2|;IBxU~vF zs`0bFE7N)V9Z$xC|B`FftPoct)D8;fgS@UYS2){yHMFwLLgb|jw7*3bi7<6<94hxk zVW-$_zbn8CaVFKtJK1m9;}f2cg)_3E#tUU2axsOs96I<*$%*}PV5Kf*`N8t#rNU93 z`r>7fy8^Y*foY}O%pVZ|IAPUZTF5TJR`|ZshqFh4j2HSMv=`brIq{b|SH9&Tpd`bs zPWN)F=1iQ{DO`-U(`Ta~a^M^6h-?7$c-R1va!2uZ?dAPb{Hjgh!{%uJUzwBLF3Prs zf0TEY@6;;w@8wh%x|Q3fLKQ1#X?CL|zAqR|zj~2SZ1k;e)z%v>ahknl^V*Q5DoBp+ z>w-!7^>)naSO$bCs~uA}XC=cLr*Zh5PAcSE@1&80r!ztl$v?tnjcv{F9U>r|-)@?r*JtOZ}XUY8$j^$(A&ZtKVwzs5;kj{|$6yt6wW$;rcy z(#Eo`Wu-J#NU2Bvj{h{D&MlYf=gK^!cYt%cdyQJ^Z44H=*}jczNL=Cb!U*lt`bPp6 zeq?N*5Xppa)0Wi_ySmnUb(?ObUoh4bG8R3?90zBU#z98oq=7OvfELZEKs+59gcC;P z!$JWoq!2A22SxFil7%gFRvx)kh`GDRlGoIvg(>6ZwqB{QtQH;`bl0uAvBR24ER$Zc zW4@bq!unLMN)~rJM}wQ3HoEr6OOW^2?aX~Y`~Y=c*%9{Y`fk^xT2qn@Rf8J9nuI6e zoTnH1>+LJk-l_+qn@jN7_1LY>J@zLfy}xevB;!5Zw*$^xwhV;(%S%gE8z9!c;5848 z<)QP+J={1XN{C}N7JK(wvoe+|ZQ)CGRh8d#2V_mMS&HsI`jSv?3%4*T0@st)O;Nkv zCbw%p+H$Voh02kzj1a8W)oo9!@;qYE@9GM~XAKeC6*6!d>2_SuO)%T%fNi$YN~JdB!7S>x+hZktv@dwQ z%!KYj@vlH`1*}|g~ zU6>`UCr|i9sO?e?rMO#em+els@wqEcF^?K_*yYG*2y5HjKQ9hd{;rVYje#TFWayN^VZ-kUC>*#lC zX>-*7h(pQgIHuh*-o8n@H6H;hwMK>Y7O%dVzRX{Y{rk>F#nBey6?lME+oSKmiqIY> zVf_1q2mn}M%ZKc6cAA2(k1%AV0egh9fXhJ|AEpt(h#2##s1$@Gxvi$`s^5eMvbd}) zwwg=yulB9P7Yl6e0bh6sLp``~h@-7Z4nxH@5(ye|O*%#I4tmR>cZ6&`cVg==-TB*< zfS!KdyJla&Br+RLSDdTnXl~HJQ~f?Wdr?adywO+EUyod8jF!>%Ea)Sgs>$CbdFh%e z)et2Dc;M!+yFM$HlRfjmg`DRi<~h0vqBJD!l;HJ=eaZoHlAYo!bX|iK18+jNwyU#y zB9m*Mg;cvAjoNOqp^@Sq(FIAeVbfvokIo(63A5$P_`G72BwFcTK?nM`Hy1caO%;wl zJ|ak5Ta&`3X3hyL2ygNKQHs2#2)6S7j1xTN3Iaieoj;XgAlrur8%GR720iEH1;ZF6 z(jg2#*1n0Cot>FeXnI=t;f`47x*&~ZYTyzuuudy6!@rRLaHTUde(q#3aWysmRx~4Gt7Ogp?aeWH* zB9xod2D`a?jV++uSq%Z*ts0<5ZBr+T3v6Wja-J=E$}u5A1sQV%8+dEPbA^kKITJqv}PDqhg|X%~)to>xOZQq%GzTAXa^~QoEG=BX`c8 zXcflgMsoj8Lioa&1rypiDmtrjCA47!H#@)l@=?!rzZ#*}?dz_^3t ze4?R$JEiK>vfnF{3wE>>>J$V#P4x84j9}L2$fLCda=iZ_by0SUkCi~YUyL^>w}-~x zp4jf{I-v8kWtTVVpVEMX-X?pi(2A%D>P(+o!J+WXDW3}JH!t+c4nu|!cr27h8TlZ} z6s4hgNi7|1icxLbD>ruwim@1sUk=DI6i%`u4tMW0eO~RECNLXKwARZTG)e#Fya>Y2 z=BQY&wPmC9ZFCqW&Sw*_Qwx378r24msrBaG#Ca&9XuL zV0OY>WnSx0%;bL3fz@h6BZJSK=W_!2ELXeQqY2Yx(BZUnFiTbnr1Hp_X*P&q2mEwv{Hw6&co)kF4`|JCzywuG* zH6lwGxplaUeEj)n|BED+yqU`g$v-Lt# zQVrD3d7MSQmUOt*`v^S^FfDsfY~KZt5+( z-l5@lNnz04kN%5kVJ~fN5_b!TW5ElGv@Outz|-Y-)!FqrV9OzU(Tl~PAnBqScPy1=8mSUvZ+aIP0pqva%D?_4|JO&b%MhHzI z`sf!=6&9YPMICwmg@GL>X2Ff)K??@yfA}OE6bQgEi)w-jknUaH-r{#F~;BIQ$$+9xneE&c5(LXXI&Ee-2 zTkcaP%43#zDdbPl>tFs2{rjKsdx4sQr*2HacSr!iH8+JI5>2nS@-smUAF_-U;C%sa zafUZXqZ+Nwbf##kzbHZA#jr{R?V{^SjI<6x5^uBoBJa%FuaJ=ki~KRk#=Q51b7>Q- zKp2of|8dhW0|#P_?+TNf$O5h}1?-PaNd7kHf!7Ik*=pxX2wA zP6E1LvGaob3_3-Udk+JnUY)GM9XH`P6w>@ddd+L_gHf=SHuc+C5lp1bU0$P)r_#j* zf*_0HLN#qX&|3M0w?D->%qd9;=+-#cF;;NV2)OqHwkf(rUoi+gL_e57`zEx7Tvr$F zD%(iyo|^3|s=sG$klL8Z4QSDrm5nJ$;nLRVRPVH3!h$#FqLDwHz+VgAIr6~ur@9qf z#|eukZhJ_wp0=v*xUQ2i1U2SYRDGtLx)cOI;VV2F>(21t_NN5t6Uhj2n8M)^q>(c$ z0HI)&t}jpih%b?ZmPPKg39&#&XieIs8#waW9Gxx^ak14ntwx;Ge0&YJ(1|Zq0lEjG~ zsSYVn8if3w5<_k^4a8@A{#VGM#9BJ^uCR!NjL)UAUk&;!V(NRbF*9EWZy4rVH21Kq z{{&!&HBlz(8tT8g?OUT{ZkH(_N9aQg%7^q#Y*dx!m=@gb;T>#BzCALc#%mQFM*roA z`tG>?Z5{NP+Q4L#*q<&?CymxmFh81q_FkBAL0s+C71R3S$ae>O{SfyS{lx;_{`Fd2o{#YtN)48e*$kJ4WY_t4b8mV zUF;7J?f^VPo#S%E@JYGHjL`A+LH9j+wZV(@Kuj1hE9{xBeEV>Seg~Zi+6a>c|8CyF zmZPZB$5?+qW%;S!;iy4z#KGej95}8KDct3k8!5gC7AJ_0cR*@5tRJ6Xa?X!N5oxdh z?D#Ns0(cr4M4Mc0N;t{frA2n+uW`yU2d|B=C-Vt?XA5p`FTjE6wk8w-M;b45g!ct$ z`GhpKzx+?#S{H#W&^^MeRb|JK1jY6f&Z*6boXjomL_DM1$ML44y({+aXdv(!88)1v zG5=w$Y2%;_xD+8L%JZ<_{!=Ru=sJ-jAasrTw?(tYhbA8JPGN1J@;EjdjNZ|az1%Lt zwfMzcw$=JiwED`n}aJOCg+(jQyq#vmS~x#7}$<|zQx>*8m;L)XyA(RyEgpbg2ka!V$jWs%^bWCu zJJRU#QX5et{kBvwNx6V(-7|pV-CBJug&i7$Zue*-o+AYGAFG^|hs4E`wksHWCl)W@ zA!JU#^T;NJ_Q`13@h7Rh%X2XVx>i*0lRyX;T=WyXEWuL$#7veQ(l{d|01U4j$`j@G zlggR868hg}^7Sg*@}CF9Z-D$;Qb`m)XTnsXX~O!$L?*I?*nGdfttU`~DXEFaxLnAz zf!3|ud6%E!`hl=Qd!yiwp(nVtGYyCDMK+Hbb8K*O@ce@sLPudp*iRilbx*It(4T7$3d?xn^mJB{8jC2WRdb^Vz z4W{`jfP~01$Ild4+&drrDM{sOYk5A^3|KmwyQOY`#XZk|20h#n-=8TXBkB_`)yUl8 zR)l6kyT}=G^)~%+Dpg}c4lmoadtfy#gUjN>w_NUb;B%q(;G}Q){8-#D%+|F9-LZX8 zXQrBGm&=~6$m{R&n>9N~B&!!dYn0cnIi~#{kzO}sSdB&LcUP!b2EX5jLuHC+n18HQ zno|vwZ2&dP3wXB`bdKYjf!up`Rf|TkglSRo&Yyb&VQVYw50FFrz4pW(3kIr!h%fZc zl(0FsY>*O$Z;GN9uRDLQL7#D9h9j3tS9B;duKCqGxijq?SXtOU)0Gz@=EpvL!yPy!4tVrXvK<*q;>0T zO|G$Zyk~q?CqUBy9KbD6Aoxan`v?$(M#(X098S$LLC_*2MWNF1jF$)Y50WR&UQj_o z4!8-X{DdNGt_um3G4D?`8{c{v13!}K?y%kTKiI0fl8f7^7W&&pQ}Bb4a8H3-vu3bMj{NxF;eaK5 zVEs3r{#3R7E02e8jEK$w7#L|bTVM?+$>`0Mo<=YAX)gb5*Brh0esaV|)7#s=qN=EW zi$opK4s{~pAQ9?7IK9H-JbwfNgc3{g=U+f(x;(l z2=eVWNImfCJYUQa-PFhd5?n=Ig+C4jPbUGl`|_b!IB&bIvFW>!JYGG^CD*{m;;r56 zgjX@QCvoU0I$OZP4arzYWkh@1QbXp*d=f-=(evpC0G~t3J9`}uAzYk&cfd>nFOVnZ&#rr$=vAm>4fpqD6+Fn{h=4Gcre190lj*AUM4~69S zHal;sJ&6F7TCfUnY#)Z--XK|6&tQzYJvrbjY0u-#)pgKRyqQVE=e@|_ENWalfreLe z;(;A}I1gtl`gttx`w#BKQQ*nl(&$9@BhtO$Kz~2xxEw<^#1fG^IDls*w&kP&P`32( zLA*`qftCLNur~E7-1|GyK=?~Dgjn7Osle0k&S79uMsey%*~4NiuKzmarazM zQyqaj`*H%6!vvi(x9Bbv z!PXzmpY9$yu(FvfD9*WE2}B-V9rbS`kHTzzux~suGePNZS^>c5uW7tTf18q{r9Y`d zX8ScWY%s4s3btgf0MiTLf9-TaML)E{&$v3yiW45Qrw+e@`MR#RS1E4)EW)xTKsFK^ zeVk1j;|K`l17Mwws-5Q z4wV6p5{C{Do;di)73WChjzou5Zs`4w`Sr)<*jxSuF*^yZ9QhA;OxpZ)sJm?@Nu|`8 z{cA86hZ^)2f#CV-x;-E`6#BGX*FN=zvi>v=+@&T#J=}PZ1Ar5@b$U>OA3yX!0f7Ex zN)W3UbinLaF!4t`_U;!`Ih^+9B$?w!pIdZXMy4Mz!*C~e<0uZI?$W6)-^dhaELD9~Sk&oD0%-`4yFD#;@Mltp$aDxSrQ*{Sts`*4r#**>oYl z`pg^?@v|K_TEv4#bS!EqIOJ+IpS`7o;1O=c#8aSWu5NOgckK=NKEtGaYWQ z#)pHR)$E!-X4)EA;%VX6{V{y-<7e5I-}0DA#bfz)lz9f=a(Mek3pQ`5wSdfB7p%${ zZ56H?-QhE|NyVB6X52R)iQr+-Rxtsdt9m7AD;e+0<|#n*o-d8%q)-RG(;F|8yflLs z?pq4C;9n5u(-=5HW}HS&c&vTJebJfb6fS;;GjWO5Lbp?9ayNj&q)q|J?2udSCVK_4 zR5iRJma3t$OUWILN>d%F*lFYS3&UNOxmjK(wh*)J z66K^^QyUA_UMcR$z*z;NK}XU-xt2WrNyCvKAsudfe~tXUiD?yZhYZRIgY4#Ph^uxU zTbPaG$eh*vtlP`0t5xJ{({qaH0hdlri#~%p$|+xPZ`!s}AHj9X#xSrf48oSkh_UDQ z{;`t_05h|`D^3vUbldM&BWNy}y_;qu^LgD3LnAO7=bwg$g->9eA|P}=5F>QmgJN54 zobXWPLR_FxF%#a#?FuPEcrA@I@p;*&dY^cPb0wo=r0=LKyWWkX)Jr(0n58$gv=haGJio63`ZhLd*e$l?d4CkKM%bk)kJgiH^^ zz_I=0%T$p+Kd}MV1(*$wPv`Rv**&n>BFd=)??S3TD^9AZLL#9ZhPvDg$xIXR~iWNuSb0Gf^#(V#EgC z&F866mY@nSQwHtx{;*;a6Zh~KGSMp6S#KX7)LVr2FJVzUE1D0yFg`EOCEeb=qq$Xa zU%=TO>yEDoD16v3oD;{T6EB#1q1XnbTkl>;R$ehKzx8-v^x$Yr5cvx&1R4GgAYXo| zt;?zF%zOXjy?-jdAP8hI3{%RPF~+puygq0Epqs-&v&XIrnzi-u z&Fb62^*X#+-**%hzziw4qS1ck?ATUH)DuGJn}h2{l-2mTH$!+lq%O5-Yg@OD8taZ% z*pR5lhu`oA&&|>gx0K@}pXWvC*>8uyxwdht`Y6>ovu0hIMHp#VsxnxyjEmd=k3B2! z_Gno3ML+Pd#Nx8&g^q4S&I^>><>mTBwdEsCf#*)4tr%qVNq@{};#r#MA-ur4CrY`r z)hZ{wnzYZc1M~h-14JWntKb*m8?-NX`}1e6elXgd5_~LS-zN>OaBx6$g;e2!gW5vz@)QPG!uZ_HwBEyy zHtJMVnz-bQ*xwFR*j7^XJ+--F#BIb@wG5&SVR9=EFkQKj)+SqpxT9YwKpQUiBTZQ> zg#^2d9tcl2BAhpNE`fy3M3GL#ib_)-J#siiV^cQz=iZkOaR{YUMQ}Sdz51A$Jjo+m zWz{<}Lm~1OoR)M0iTq9#!wAm=;ih?T!+mq3u#(3Uv(kcigND|T{YLRXLqk_4} zbSJ7sS0%@GDGIlu7q`2gRC85d$1Tm{VWt%%d2Q)2d2ad0=!zk?1)`L_O-h0%$&n}E zp<_GE)=2bT_wrMgmq%dM4Q@s$I#sQCt23%TM~kF|4+t^z-OsCULnHd36Jxpo9R@d^ z!)8ElM~*!;ZQX&Z$nuwfIg|@JhXm`qH`2T;*)Z%hcp)~svBOVj)Af73aR&pwh_?}O zCASol24DuJTaU8T{)5%-0q+3*0tBYeFPfaGL1?i_3A4U`*(u{*EsFh&DL&$n?NieT zt{lXF=Q`pt(HpwE^wyr(uPJtJBYtab0jiv+iQJ?)q~?)r+aCo|fuLm46h!Vr&EQPs8l#U4xiVzzlpf=HYI8WAV2yT}p8eQwHqC5$YVPJf(qTn|6VyBoqGj}=1L z{eI^c3;LM11MYsGND~o*FL!Y{vv{4;2h5>1N@j9Z8*$9xaB~U{ZI=&Sx&pPN6mwx` z=BtbH%h_j1$^NE|( zu*nG~$Zq@#;yf^>4PMmHGVQCrq2CY)94;ESa*~&qV*B_j!noWy5(PU{-t^7Q{a%#} zB{xq=a!BpTmnrdk=FM_#`EDH_qEmU}?>zp@(yT1r)^+Ndc`yui(B}4A&sz$r%{%EYsR?Po<7QT3-x4*dTo}Gyc;&GvQ*CfA$c;I?hMCFE5vzJdkOlZOt2(vn3 z?0-hcT6U~z!553`nWFnpBeUrm^}O-`H@Fw02sD2kU5-;Da1mUH6Uz4~K5ZRy?}6oQ zNQWw#3C|4#a5b4}gKQtKO5Yc*?7BVXk9Y$PmZ8T2|I-4#A3j;obShkh6nvloGaYPF z)!qSIO!qfI^0#551B+~j#He>HNH(~0*yON4I;s54p39uHx7t@jNbER}z)C51i^WSA zq)toak3R0hY+ti8>+SL(coOsgTa*-0Oui8VI#oT)7fNawytkD8BvgK5aq6d%>@{wT zCV7q)%5&MMoLNv-D7Eo^Sn8@$#qcSGfq!N5gH(2omvcsEGd#ZzGIp2^zKI`HsA;~a zSO?^8HJUh-s}MVbCRc#@WA*!zKc(Cs>x%rGzheI_bR#WGA){98sZIrn%l+=G($W`r zD|wdR_AUST)@+L5OL{#g+T959bJbiM-o)gga!oTF~MDe_$zhJBwrGWGn z#K@iT*`PS!LBB5o{!1@IkCzEAmTm_mZlrwN<2W_%S(FnF2?Hkj=LU{w14=q*NXEf* zWZsbscJWaaZ2b8@!d53B;q=f+-nG0!hIe-h;vl{1$1pEOnV)b!zF)(+ue%Cy9uYB* zEx!8?6CLW?W#)d7)F#T%B;{3v^46jQ+bn)(&i}jh^0KPd(Qw=AJn+6syL;oYOH}CJJ~Gv-mM%nnHioJ$IbUox z|89q8##gAz#(^7N9DKiPMX#3GCNS*|pmr0(NLLui*f`#g{=UdADG5M-;Zvr z00gXuaV5m0FX(DPwjX`~v!Ans$^$jJK6SlrE9ivFX)~Wl&~QL^uM$Xh7Pg4pXq_agmwb+xBzpQDO;mt3l@r%=M6J<`xBalLLQN{*2Pyb`>h%KO>p)<}lIxWes4SB!SGYe;!=;9V zjqG-3|C({en~YjS=Ej%HD10<8czM8>C`wl0N!&LIFY~y^@fB<7@Euy?X&0_YO;^+!R#UER;7%()>zcZHY^8E9 zLq8amt}D8H-A~=Hyoa{d3KBXMN~1|-?`igJ-CWmmvbi?A8H$974a?S$ut&0ki92G_){l`!PhWKLbG(5T*+0HBV?(h1fW*n;+>^<=xIq2-v_Dg@Kd=3@S1au*3t|b31XFjp)O-MjB|i}ZVj}YFOkd6Wt~ zfqu{-p6h#SqKEant;dVQTLtU0MNRcvWbc1C(N5Ec-b@#sTbfY@UaE^vxKn2` z8DbqKy5ryF-TmN|uu!C9la%T4`=o-svn$DxZR3orI9$ur#5VFPS6Q_|j+L+cG%7Z@ z6OSirGVB{95v|1L0poTYRyn`_I5Gi>pS8SGMMOnA;#Eoy6uTyH;MnwyId*&R5*91> zTVJ(Lze?DCL9ohtqCgIuAC^COpftY9RhUPlZR1W0%HE>(!ISKX%BnT?_H1DDw_ZnU zojCmQ>-|xw)M#dD$K= zv%Kf!typU#uFNt#mK6KFsC{XBt)}ZNfw=KHyy3bep(6B0VCKbG@jCe)L4Acs@@uiN zM92~B=$7B0G1DqoF8OJqUlR=O(oU1n<{5;W;o9x>w8dgq-at*PyX_D{xQ>Kh9i3YfoB{VBy~fM)cAX!%=Sc~^waKs6oRW*VX769RK)}hlz15}&p%5~ zlDgK`<@dShEMYQaN%3JN=m(L8c4;^Ec&RZkq!BU)nl!Tb+jUqjcXFLf~WXHZ${E? zgPBgeCxzX|J;8qhCMU6v-MdIhFXX8;291UF-{uc@MpsE@ZI`Ay%ZqQ@vqpM6MDt_v z&%mQqD%VNgGhToqO3?1|7qSwErw7CiWBe2 zB`u3yGQJ$~=-uOT-730H#N^F|H(%-1KvBZxMB6&T`)2)6Wg5zvC|pguYrYKpz*d zAQ;T)gUhvsw-5djQOL-PO9zs{>JMY+Yph6zfWw}ZS^YkK=}N1UuVllCWk$@? zZv}-<>Q@jp$!*Bf{`M(vUUMMo{)vdmAKUr}zpCUpVdsi0VVd7KDZ5u(mqvnIpcP)N z;E4^p!#NZQku@M31aPx%rricju@GN&1Jd?RJEiZV_k|<&lRWsIVKUSxp!i=WQ%M;; zvxym^mYf|5u$FJ*%|rIWv0DZVdg{sVF$9A&dD?JlH0rv#4sV`^OO^J>FgvFC^uBsI z7EBWq){q%UNx*_lpvDAofhuI)-z5|U)_z@j5Ycf)pUR^=ASn{#NKzwDmbL3e8_TFe`mQ)&R=`7wMM9c0iK7viF;h&D; z&ezaYJ*v_!B2Solq`efk-*`q5OYP(~!=pGGr*cQ~*yd%snf2F#R-&;p+t)PswogZ7cVT`9SD_!nYNkh&eu2v=JMn0|Nmd%OJo3|SM%Ea{psj@lAgJ)fJg zgQg#2Wr2mZz>mBHA$&$@0oE4bPO(vcZNm4ay8>eHSYyBA#{qGk4ixZ$XOes)=R{po zI@)VRIvyr@)5>-nFGIC2bRUB-CPWjfS3in8XH~3*q_m5_UaE!hu5rmW?>Mf`tK8mO zBy^5t6&=^~TtO8ap|&^>mH782w-EL{*J}OSQbnpupHz)94 zVjSaO&@4)i|3p8hnzpvrq0HpqpOF(-Y5=EKc$+26X2+IYety}}z|Ke)`K4q4Ceijx=<88h=Lp=Un_xwou0_^p)jDna;oFP(s^%Yxt1yAbFz_8nnxkDOE zUOI%AcGhHhU_|IpO92%BJ?!ZvE;=YifCUWInLx6ZBg^`}*4lZ#k%Z4OtZZi$9`%20 zm31$V)2v%r|5BOFZ;0t^omDv5MZ`zzFt(^eV`7F?SBUIDA~7t$@fzDFp;Xksyz#oo zLxu_bkiMLiu8k?K^3plV#5}a(5?A+T7dY!fQ{(z2=mHko_*_?`?}11l?(bFHkksYZ z2R}4EE#Pu^+NdgEx!68p`UkW@1k7Q1x{4NPTlQXemAK~@(E~VoOlB!T+U@h(fkr&V zTf+*e1p6$>lHn5XQlaZN&7Bn@M~#Zs)Tw|vJ5mIbl9cR3H3BAb`>p-XcJ!2d)pv-- ze{3;O+6^-#--{AQNmHqQP;G*9kwl~UF@Vd;83)e4W5+L9CcZR{W3%covc4)l#*8mGp(w@DBCYZ8jRYV-<>NQ#-u+3klsYqC^` zI!4o8G@b3DAA`$qhTG&dE~R;Gs`v0Q4(y$#n;N?VDK8q5K?D*2tpJaoN4_{fU^X=W z2>>2qmFQaIhWJbyWjyuyjZ6kYbF9QVKl+bG?uU0x*7r&&oIairXA@Fh&-2U5PAKj(iBg9`reH3nk`GGYr=g2v3Zxx+PE zvx>O%Tv*hfXLs?U2yx^}>_5`Hvnj!6dfSy{VmNeXyc@%_=|RVYKhf1%!*I06p0Pzf znD<@Db9wsnBcvTWmWbgRna;8D>D8%Wb4sqj92FKqoUIWfYd``E;0jaItfa3LAWa47 zmN+1lRL)F#Cr*_th@J|wc@mpO%v3Kl7R(4=N&2GEM!MD>S)0tD%+p(8W>J-!?DAeX^=d?yTC z^;tZ6#cOCh#>>D(`2ey%;~ozBRA;j$g+!?N;#x)4ncOy2Q?!tXFe=5~>O~U`Q-pc6 zb%^}Dc@M*|5029b(Na)*l}Otg-#NZvEpmjHk2I@t&lkp;d{C!~$KbDv8tWpP-My7) z&$i-E!=c9yx9nj+i7N@yZ^sju?J6)WsmD}hkA<5I70wVPUUB&ArQv2&je%;v>tH)D z%yA#SQZY+lQt@wD#&?R8z(!QW9}vhS=Xu_$aeBBaQNU5D3oS);#{|=344Ef>mgkYAG~ds{~pPvb)joJ;~G7Ys-27=^T;q z1x~h)kiAOwHRuthm-HY<2TG=N;M3T}RM5(IMw^Sg5Zth|#r{}eW1v%Ki5g}dwr6s{ zdtax!+P!mJ{3y7OBR9ovZMYOZCL}k`afGY?F`+ZNihf#Mb<^8z-~QwvOWYT+jD)1( z`NX5T@}OE!=L?Pn9`1CYr~?y#FV^^9Q^ZIgx6Kl-pw+FwKYQ$I6MPkb=49j?|$^mzB$Yk3OiL3e$#$|l_pyx%re`@ zoT{4dob?@n&OF@p?%!~?gTBUkBs^8!e9ZtaenhBTt-02G;5brfRDL3Pk7Edw0#S=x z-8uL|?I^B|Ud+1+dGjCTw0Cd64)qu5*pauML4G{s?{k1;qCC@SPdzj&ZC_A4;5f#z zt*EsFursw{@u=p$p8t(BXOqq^OYyYIH1b^n#;-dTB~o*zUmkfW{k?2#P$jc&maSSLg>dj-BC=BpP`U>vbvbb`Cu;59KhvzL~H@t#iR%auHY`+b? zffC0<$BmWtIUTb4iTO9P$NA{19)8OQ?%!#Hq~g5`l5%;5Z8Ii-O-?J_T+i288<`*c zy+cQWB*t`#xqV+zqW^xPIC3|W-@vZMkz>qgQ?aoqGGG&sE)c8P_-ivw>9nmB) zY1P^8J<2xFG~pG2d0LcOg-zr_J|g3QZ&o?bhjHA_l?%OcyIC_uktTvjjjwpN=kMn} zIkvcQX`&@4@!A@&58|39nO2HIsprz^E};A2W`W-XFGtTW`R97td?l?XiAR+4cuTKS zKCBz1BpD%{($J0hYSt3Q3Y2MprX~2;2hME6Opy?HWMnY@-ocQ`%sg!(Y#E3D^to%iMYe#aw zT2T>w|J+5>$0Wv{t|QJZfLzbv>s%4+Cy)^U6e+90Q3@i`5l@pjRv^cVqpxsEaoj%_ z?NicGg|6hOt`xFm1|I@*E_s< zN#LL2n3vIIX%t!GU|0jcEBz$XAsq;&Pp?p#MpKG2e~(6h!Ei`pGDtgb&Re5bfD@G3 ztw`2x;XPly*-NK>#hc-4$)xx2nqS@e_DlC%l<^@yT~XnzKL4W=OY-VYcEq95m+PqN z&k|9)0%F$n1%0$RN82s)N*E5 ziLMUrq8yl6zgtiS^B%uhqZd)^vXBNW7y(}QeSJ4AsYfWqicAKTF>pM(Mh*GC$D%fb zA)vXOWxPU0@tb8Kqde%L3M*>U1``4tpXe3PG=x_s>#HA1HdK8rmDEqJ`E`6FyFn)! zf7=8m%3G%&KVns3?Gd~#u5^^TPDIWi&6CErrKC-jtRE^RV0&HSOL+?b*_%GqPsOx< zVsDH9GqAvJI}QR17T;Y^6fNFI9CfiMwF`SDq#gyM@Do+lRQ{Y%rZ{uvm6wr`V6@g^ zmKoZ$7p-3DvuaXnkV!@kSe^V%@(Vq++2 zyJXS=ph3Uu(Kvp^*ZS^+pZxB+`jQu^{OdCH>Vt}k{59t++N~&p3kA*%O34MHWH&$x z(T|%l;}5ADP3E!q9xQ)x)Jvbx)8znb01&|)B~bZC5{2$uIPNES5g#BxkI%1B{teYs z{?$|>^a6B-kpLsLuSZ~E+-X~OC8kjEYxCc0xPH(!H~YuI08h8Bar^t`VJ;5(9kn9L z;jBNOGk&i1=a;~V#2~^RbyO*fdD_g>JCu>ah|CbBr)>E9gUgVPJVbluey#Oe;g$pa z1XTQ~`YIR&XAFWYiaTgo8JxXjec%x?GyiP9;%Cga!9;`hBz7P;@i*hl^Sm#<<3!X0 zRZH((jtu{;kGGqwJnN{jZeiVVy#KF;!sS%L2a$a#ssC<%HfDl|fbDm4+>xk2b_LX8 z(dwvYE4u$+pn~0Y3zYyiX6B!)u7&nOCz)Z4OGT+s zBTDMpKb^WX?m{WR-u29Sdle9lNU zHF|Sec&sP*N==(Pw<5CXxgV_Y9a$pKk8#jjm7zS(T?)nsND~j!MtHd$Kh&lU0o+~p%!%=L_Wsx(tNN-0U;IoDT$Ldun*m8O#?Ae z*iDpL9IcUqo?5ujMK?BU0CWSa94;z5Z5=zB^VXA~qsQ$q#P zW#tbIF|VqOjr#ea&jDx;<|%gL1?Z7nombNEQi(?8Pxr3&(;Zh4f#OWxJm43}FU9t? ze)R9gsfaODjFRf8Vd7iD!C}h!;ls=_i{8BS-qqTZpNN)UvtA7;G-!GP(lh{q884SY zhSEB>HrUbwWR`+bwUubbKzC|vg+du5>fpEehi4=hvRlT@d70vtW!vs&s?##PZJn0K zEz4!vZ(rW@$xx;mxt^}IuIH)#vp~KQsgC?^>(U3eQs&Suy?fq?Wv*ZR5e4T8CF)av zEFk4!fHx6=qu2w+==LTBtg?>}EH27V?81%+dd!s=UJ>>Kr99 zx~IZ!Q~q@OYmb*xHyH7XW7B$qa%D_NH7$-`&vf%&wHReN@a{LtKN#Qx<=?U4B8sXTj5 zgrPoRZkQV8hRi+}lNzJJ^{X}4_vMf5#g_{t{q&lb?%{F!F7Wn}Rc9`m5Y_3El)^PE zaSClW^L7-g@~9~cWTYsV^jw-^h#U;a`7a!~SMwNw&NgcRbcRrl=g+wOcd?Y0zd#&0 zSCq3;PsR1P&Yx&n^EV2kCww$ylIu9*!t46=bXjNL0N|Y+D1XlIf+zhuQEtQtPOOm1 zzbuH&ak#wwlNzi;cWs%4LxUc#eKl#DWG~&5N_h?cCGJ0A+6GeQK%CN4{^tK4r0ELx zIWx{&cyQ)uo_o_TkIFt(w-x3CG3f>>etJQ&*CU<=7;3|jNxBmXs%f9d38uZK-9ZrPdiLiJzUO^$L*bg|S1x@p^mD#XnAiKxJ!Yng zxaWw-)gm|;B89ouLC&Bk*7kGJ1Nla1k>j*-PWfLK|95~j0D6FO&Qz&*!YOpu&!FLW z8Wq_=ymWeWKD|Ks*UAgCpQuFB6HnTi0 zS;r}i-yu8U`o5c2OyzG}{#-EONYG)0KF;(xBQ>v|Y2C5MqFD?PHQEtrg>U6Ek8SmR zd4^E@od9$)erwK*lfjHQW&Q_3vYy_DjxlR)JC|ea`@!;$M`fR)A(*V$%%vb+%YowW z1d=XxtdR@vu(suyqz!>T=6dRYH1jmeg%b17MGEE9)w-Y=PEj?9yDfTS5*iP6W0dQ^ z310Jm_5Ji0H*cx^H=cabd@G}!%$jTB`{gI*L7a;<3rL1_at!FElt4s!BZH|t zQAC|ykNo}PK&Sek(*nJb#@6jy1a=yq7^NL$zVH80UN^Cxv_|WhGo@N2Dyy=f45W-V z*`x`WSK3yVv`%qE)*E@#>b7}3IwqrS%0E@#sqPae0ynP&Zb31WYaUmS9yOcIN`A5d zIvxIPh46NC8Pms(+%PhqadXhF6I~&`|0E!r(1lIY@JQEJAz8f%DS89ER03x9+=Z$+ zQ$+Lpo7eX|8Zl}1!FVj$ zD>x+Xn<{O|@M)}T%CJsYebx=inaXU|M9tKk-Z>9;hJ3(^8r%#YMvbA>L1R)5{!;D~ zPyaJM^yfEis{b!uUlRIo@v9XT(GSMw?XKW|hPw4Ec6$Oua<-GgYel5j;<*r+qa5dg zV~*U*JlNRb8MFpKhw$WX=G;$xw5AIu0-2 zscs6B+N49=RIP^Yl7ebmx81RxtjY%f01yC4L_t*6!Hmxg`W`(1;BmV$EfRzZKT=Qv zH@^sqhCqv~0oyfr2TK-I#@!Gi&lf$)L(&)m+p29?`n_5x^%62(hUzo0$&tb@(FmDIxGhhx1k`7c|p0B?YH1#^1wVD)CGb%;6}0d z+Y}lPbkkAJMH)UWseeQJcWvHS^I}KJzpSuk4=3VY#Xg_RzK&#H8av0GurSbRisvO8 z0+L4&1P5vrJ>}Jkxn{PLv1`2YGUnNgTt$Gce>bmSXXU7mL&?0Ji0K>UUss3vEmZ#9 zeBdurdyUtxOw$oDUmxUkxV>a_OS(pjUe61&mt%{Dmrl@)y(KULK&7y~m9~2VGuw}t zcl+~<@^_Idn^C?#`=NEWT=0%)J;L|38O&OCMMop4_c`nRD-%Q4L7AQ*5Kk_jq>MM9 zc1z$oHwso8!=yuOGqB5YvZf4179*3H?WgFuNYQ9XR&PYAt`Y{vj<12<2^4m*>fp*2 zm{<#A)l5xF+i8jw<6z$jl(|vBz9n5O1%qgZk2rhabqm@}^Bg zK|LNf#hPi_w9!N`G1!qH?j1(`SX*-pd0T7$3meDOJeNuNn}6a{T2#Bc($3WaIfh+q zWia=nyxnR_cn(FRTXn(;^O_Y68N<_iYJE*nb+Z03DUrJqEqW(#BL zIju7nWsY(q!Fs=I-tSiqQ*t9{D*be#7TQmDTObiNEUNu>9-r0a^;@-;{}Pe+y3Ldo zj7(mlj>|!tV2pt@6|GUYM#1X-GzPRLEpznREKr_xzV^Sesc7x*u_WB> zZ@%Y0COLYZ`084S97QVWn!p2JN<=6wW4*@kFZ+ zm}p)JWYohzTsJ2>STZ8t^vPo0TevYJdd%|OqHbjMMX622_l?vBsB#CnTjYgnd z7mazUF{Uy$irY7Jo5<|9EaQI>NdV z$iTWx^G$v7)|_b?qJn^-=Ci2TE#&5=xm9!QxPNo1_VG*){Kex?@5ZWUXy3V7i8>4- z#ktv?aH$OZA_57|?sh7bdnuV66{*qYlfj|&Stc~yCgv~#(d6a^y27d;lI)h49B|El z^Q+%N<)2J-t7=yZ^ZMn;hdG`jv)k6ua4I?2tZtj$m1CnIlC@L>iFy_%7yf1n;<%jo zm%zO4r&R9D{EfVt=fJGD>mJPh8|$M)Lk~0iZ7BtJgND4Ik)8j!V3fUir7l^siL>tw zRK^=&&Vexq(wv1Gg>5_pemd1?7*LVxfP5Hb-AEqyn}|x@l=ag|n7SSf4X>dNW-UL( z8Gf>A0}_?%+0G^|MO4#3O|Y)twwrx^O#EP|v7zn5u?}{Hc_d!wMv-gAexOvT;2i}g z62b71!!Tv)1WcPg5o5-VM6AF#?3$G@%Qcs$;trI#!#t+>%7IYv7nKxY+=S7XI&C5* zP8x^OVWn_l5%}aEln=_159|BQ`-}s5$ivNxVfd(W$}m3Q(JX_h-(23v9h9g197hlk zT5b{?Y{wxu9H<|TbLz=NQvM&FTSC`gxEL3%IBsmrbv{j)7C}&kZfvPEMQmaroc4HNL{Dx?~xm89M4vb4q9Ae0msk|112K@U*_V zT0q>it4$nGqY+>fs!^mquQkOP_r%F#aq{sCan9+BaL#E*dh+2gJkC1xC>-{Yy)bs%C>Z`w9MefbJ(h2d&5YkrBrl37 zlgHtNMF-*R(~rT~l%e9J!?5f8S%}gw2xM)luX!sgN@ce&mc%S9 z*{0<*0!!m*-pG}_P|PPUyCkZ|wf;nP4U#prT`J$3LK-qc1H06v(E6i59yV*}m9#~e z-L)2c%H60;synS9qTuEi!_Bb={?N3U*}qw5H%0)^WeA#TH%B^;DzXp@7gEVCVIhf4Gv($`3b57sKnnMLP`dU6)_4fzgVS7ddMygt2p ze^<))wu}Idi0rIghigyUhZ9d@D-_-p(Op`92xgs}^_$rvI@?6xaJ}EnD~94gh+N!! zAL-!XKMqwrluv7kh(^H_OmV|lDD785%WGkIHf=is92mDB1bf2Ymc&C7wk0+0XkKSp zai&c2MFmO}RQ_hYeZqt>7&m4lYnif)nnGbg9(I~F1<|~GfQlL{ACN;E&QVM=|x;4`SqJUl?na0ToYUj5eF_*bpOz_SiA84+5`sT8sM8O`|J+nTdTIQ002~ zUUez&s#MyWMA;-$*};%5B4Eg_sKyJRaa2?#C%0wHE3a*mJsu1QXh&5-u)Z@{jSZbn zB+#&RE6sS`9-URlQa&%@nQU#(5hq+<)XZ{C-fIbX{#we^x!hEi9@qPmH5t8r&1{2M zxiLfvN})Jd1_?R#!#-2_*TKwuc_Ml&od7Nse5@9=ZWxoggj3~f|1GU)9)=aLY2bC) z0tF(<8>vOYwq4CEQ&#h0)+}RUn0=B@GD;Lm0g8%TxhN{kXEqh8<{557#l`f*^4e^y zW6B#+O?4e;D)aop0z|3AjQ~70-*z^WZ4es;=Q9qEJ#) z2ysojjpUnhOxKDsoBoghVKB#l*+r|3c}>UcUK<>nGO`z?D8}RTCjw=faqXoNco*U1W+AX2=IM6Rw_b4*-oOfx6beHPfby7EC*r&(K;J&-fx)?P z0Bj*RQEG8yc1^x!vLS#fsBuJwHnEam#%~mGGl7b?dEXZ*UKkbBp1`_k$AL%kjp7Z+ zp+Z+M&kqFY#z4r#gEDzdJdCjrP*dB6pWzYkH+(?@q@5gqFssqSjvS~+x7%D(<-Oe2O9OfexKary!+csyx68xPb@hY8fO9$Jn^4Qd!W&P z?rphMIyeO)*%wOSLS%M2Qp{!9?d+`qupNs*wU=`BRuTD-H;Nx7xLNm4*4Dv%>NhOE zf6`r@8MlO&flc!>6$AiLxr*bnoAR1Ejb+&f8UjAkjBye4jmJ9ZH`}AkMs7o3T`g#{ZBL-cZSs12 z=xNdKyD9k^mM%?a;_}}U7nb@^*ZGjKEE5Qlq5>5#{RHxTna5?(kFDN6hL*p~dQ z0nieSL9}1AHQk`KZ8Yoqwg)`8g|*0N65|)BNMoSpl-qd01yC4L_t)mg+ClZ4q-Yp& z_KANqZl+)6`}HPnOzHKnPg6b>U09qt3@BI#1I_dxzh%9)`^Jc%>>L6@IM5(4sVrk;@S0@TO>LV#nSIk|pZzp_ zX*Pf$Gz7%RMPe{)C{7S%_eauH=yRRd@FCMQ!4cvUv<+BlaGa3|F6Q!;AwX=MfDW@V5|d#ThG-qx*VdcnGX zbv0+vU2~yl;GNB6b2;znxX#B%oN!&SVNA`no;T|YByEnh=1rSQ1n-cuKPIVDa`Q`| zB6;8ija=O{JD5(?L8o~xxSu6CQv|JkUEM~)7|zl4q6r1Hflab#c$uM`syV)Db5(-s zhOtqs0Vzc#?!c!~^r-LyWyshlMo`&mDosNsKh6C#%fTpr6Z06t`b0d;{mx`)aR5^; zDBogValeU7xlHvz`C#fRmZy?Nz{hRDlf$_vX!a|NhtG7hs28}A$j>L~7Q9UVJRWab z7NAk2n@O55;{~LJKW&0;bKIRnt^F z_^nej9SzC!eE5-%H20iWO!Kada|MC<*L>?5DZSuEkOYk;C&Ng~zeHh*P;qYyW?MU$1$y>U1hl{<@2w#|>Qn#sE+p2+1&K z1N&w^exuE3oIN3FO`rzL_Q|*e5Ot990UC$s&3K$Wi3f<&_Rh;T2^y7_ITo0G4emE7 zH=hdDr-JmU{2CP^uP<|r83jp2Z(Qzy``nGdD9>%m-;@!D{EYGiG+S#=Y=?ZoHekvJ z`_23r2gj^0lq*AhEgY^*-AqIoz%uRICPNd1GMKvT4@`TEO+QlXn@`fs#tzd>{|ycX z6U{oBa%k<2lQU@qU%~#?~YWqpuW?2&r zvYDgJd(<=3qZ%o?{u`>QIRf$YHW$i)*j&>hvXdz1^ob|M=dD`u^#(uDu+mF5yp^ii zjE1TW@RCNabxqjdsecYqT`N6sr93nHr)PsFBzr<=UCVR66^K*EhqJABk%IQC=pxOn z3n5(G{!0ypJT}MJ0He)NOXY8roJS>R$S6Id{J>-I8Z%1Yr^5G0sV4Fw5i}})&b}3i zKB`zpOEpj zKLC>U{7VS&DE3hyWHSbU&rFsH$!(o96pwiN9MS%~XAV5D%=AALM3Z4@U=Wy4@wI%{ zagI?+=8ICLP%_;cJ#j>sG_kx$r7!XUIFsgb0Fre5`^jWj(OgK2a?ChTIqH(pM_x0s zu3^*aMAe3;Q*~8Zn+N2m)w&|26Dx#@()cH{u9P3BGTQf%s;{IX$f&1#B$|k9QI6i& zQDry1#bd)Ypap;-x@}hg+m%v~CY-er6K#^IP>o^>$f05*!D&>Eno8cu&%^N1BQS5? zOdPQ9t~l&~J#fVSyW`0HcEb@oK4PC;aKv8oaKwV0aM&KRFmH0^uh5PzE5gCMOvmAS z%qGpj;jDASg2wo86El6E*^@D>D4)``4@G%V>@s;Q4&R;i7Z}>9i3j67$cMCW*BO{I zVO0CdOc4>37Z*}a*4=Xs<(`Wp_SqRn?lT`p?#I6EzZ>@2eJ;jL8V9$q06H20=n*i6 zfH84X&se$XcyZ6lsR42u+{=9vbR`LUEc2$W44b|9ek(vTvaEczAo z?1DVE-6tH4#4i`EsMp$G<9q&wG+qDZbH64%7fJgbyoTD+MCFEK{Q4~yrZ%PObt?Hh zpsA?3D6uF+3Q8d=_b*#7Q460-8_4pewchA?D&rcdX)czS;AwH|q4{SLECB*;jmjUw z1CnkcpVyO_-8L$J2PzuH*zu!rg{zB4A^pq-}Rpt(~6I&ki^AUrH|4kSU|qA%`uoqCgqdT9H}+HW&T2sd!7v&Z2U5<1*lx4zrY{G~ zpN#`|+X+YPw;N79=3rDDyAX3`Pe-JHh5$VT0WJn3Mk(HDd{rF?7inL`9t44FbyiSjr6q=6>qCMT)b3 z2nenC)wS2IBGd=PDjI)64bA+PZfW)iuzZb>>D-wo73Yf11kB{0F#X^=9Rz z>L+N?=j0XPEO9{Z1-ks*dh^b|a-$F-I@_DbxjmmV5PCK^0-Mv5Rq?1BWUc6w!7U@> zyt1^fv`%ou^fDIOfirYS--5K@vN1*giPw-(W`?#Yf8`)brMJ^g({aq9`(gf^=@>q& zH28-)Q5pr&NCfT4MbvF1M^QntA@j&bTM`jOn#x7p#`00ObxhA6012LAZ(Cn8-7)gD zsS|Zu^Dt%F)@?pEyrXW5@@@KK{`pWzQ4yw29D}15?u~=?+Z`q59}uw)9L`0QYI;3T zpoRPF6AZx|qs0~g4c#K8lil_3{am{H}3L|vHj#*s3V$f2??E-Jw8JI};kd(4CRFNa_n zh?6mx3L>(<6a|E*Oyl6-sw9GA4-qg^ptgqXU zP9)!?W~FuwEz$3ZzWJa}eKnHRo8U3_y%fdM+vE^{R=iAy$SK_4_DagO0wFu*wLdWr zCWYkXQj+#PG|v9F_cv?u3D!b;%QruttnHBJieVg|-4yX1QXYWTi1W&72@(%AsPGL5 z4@d-wdPf0NY;YWm9Xk@c&7FY}!%88-8Ln=;Y-vbgD3b9A%qJdGrc6MD#u1d$h?NkV zCgrcuh$&1gXt8ZS&`dSNs9n&|n^^LxjsLhm<5FP$iWM>6*JtodaolLfqFsXV00h>r zIce>3Z2}QcJRg(tN7NS+Qy$td;0P3zPufE%cQ1jIwiJfr6mK`oQ{v8Hx{z9hW&5B; zD_NeoCw^!A{Nc< z%hmKhc6Wuk;jCQCY1?BWeAA1DX37FhG@YCZ~f1wp>q4@lrs=?AWV z;Wfqmg2Dn!o;Vic#*BpGadY*d9v9^2VHYlml#~{u884s8-xon(Kr!Zx$^Ay8w86J= zm%rw~5O*2OIcO#x(z=2f9M+1jm-1MX+6B#}&+?Hm^T67kEWnf1u1LEC8Vg8s8IHqI zyS&DPA^$51Qr6&=j8;P)iW~r7VLa}-u5%=SSrE|7MypL6HA#z_H*N9VC9N{dK|Icp zo-Sc6Zx{t+k_Lh_XTIr%nn3w`T=UmP33hr1n$ZI^1UitIMFQoY!?S;O(?GBNbW*1q zs=4ozz@N6nXkJge zmk^Lq^no%{C>>UYabrgz%5`z#9GsQ5vClhgN2a#@HOC-;<3^1@`S4+I&6@!RGpY}R zpHD17GMECym;2zR1H8GjEwoE8Y@tQd{jcAWg4D8YMceGW_SzFJn>?0c6t&6f@G$N9 z!OhoUp`o4W`VmaA20#v61Y0V{&4#G`1Q;GBj@FV7CA*XqM`wlQN1@l!oc=Ec=(R6D zouu2l5^3`t|5O9A`7ol;?HCvULUb@Nn>KTAnAwU>a<>LrZy>#+wf_?7uYmV6?WG^{ zQ;8q=p7&My*FFpJ?_k-Rw~@vzl-{I$4x+xw-h594KjOglQ}X(Q*4n%g!KEC0$bIuv zbb@*GWCpEsSKa~Sg_-!ST1pi)9{@tP@-!o+#<$o0-pW_I6oA|h1 zYx%S`A35>UpM>a(eXXx1@6WT$y;|^F4gIr}$n#jzx%+>{he198*cO|8Dk-D*b+FH&kFifh2G>Q5uI{|NsIl@|QHt7U&E`t{?d7-e;#({6 z(wpy+R*;tCr8h{gFT+c(z0))5Rb96EI;m~jufBtSzxH-3+9sbVZ^+1P=uP(HEki5u z-0LgwzZI(yC{9A?w;&BcNpTUJXcU?TIUHhVuw-oT)7%HK+!hP-%`uE|Kt?{4beI2w$ zI#GvI-Bx(URW&~yxpMXI-*aF4+sgloKmXoOZ~M>tOX9Du{(1cEb$|ToyPKEZyK3Xp zk9@fPFZZrn`}6}FYFf~Sr4O#%`1Cy=nlfvbKDctj()*Te`un|atbgYI|808uz85w= zGv)6a>tcV*OV<3tQ~slX`+bh1UsfJ&NvGw(%38ImE2p(#>ssaeDyp?sT`51<%Cx?~ zWxM!hm!!cT0;f%mi49TF`y|~&U<@iVZOCyThJ-U(R^m0mOQO*ekftknYfU}YZrY04 zy81?{twU{19qMXo(LhR&%;&carD|%>NVUNyxlLMk$?$7S?RnHR@~^E$$`EBpHBqvf zX|1RRDaw^3H88!Nq89NaeOq)qF zJs_rYJfat%`8){s&B@qKY$By0tJ8V%*7nradrLQLi16k-gr_sVbx!9;=Dhhd-gpnR zDV*bnx_{(jGv?%sRsGYPZ6y-|k^F-FMPw(4Z&k7C@f7E{ZJD&D$ z;IQ`@cI!r+PJT6=uDjpaq<{YY&+yj|ej0yq$@k!PLJ{V-rEPV#@gko<1`iI?S`lS(T9A>^$mFI{h_Gyg zG8);1^zXeh5@|qr6cy{ypHNNuzfGu z&}m`D<=={lat>#Y$CJ!CVU!^-mzTzzblTf%^x4-Gwk_3=$X}4s{g>#N|W*i>IAe+n$3IQuVvFh(yo;^yQ9S~`< z+bH|f#E>^c)1FZbehN*1HyEpP=na)v%K^L@icbGmok}rPzG=xRQQI3%H7DP7&}Ze@*6T~ zL0Qu`@}>iT{Y3~7Z{p}v>6_02o7i9o9-G*w2gMKa)plc`l_5YtL1T<;f<3_&UX-ul ziHgvB%sl;{AE_Tnk>;E7J)x8_+{mD1-CW-{;+pHPALJAT8Ls-FA=^X-1Y(iGIih4& zffQ#b(kj_YrAm`kHG4)Hip*;NRJ)P!7t=}q-a@5+z3Qj8{cGhf;wv{l z8Lz_9c)CXo_Ec1wWE7U?xbR8l7_850hL=p^W!=0VC`8RRjDn;>S53-aQHd&}V2z?R#B?yf z6*bifn8GkId>A(~?`cdn+k)W{2uVF^+HY>t>>IgE9LVOu7y<#&2-s!_G?yU{b6GS} zYYa>i8U|Abns}v2p6kN=(;&@d0T543@|%f9fx+B(TpEdW4Ze&(K3WI{4FK1bH()l+ z6ce|g*Is+V)eb$Q&?8zTcWXDz^W(8!-KsQn3Jb@AGHU<~+zA&Y&Y^%z;ypp18amv3 z-o)wp29>I>o9~Kqz>Gzg$68g0$9*NTN(0ZdO3%^(6(@q3KWG1bI@LHrWQliMHX;Hi zRsh9;1l`;?fTc*nEe5c$&0{`@CGZH%P*EVFOLFU217O z*1VCl*;mvsGysee3dk{vi?8D*6KGR{YF_zUrgTd4%SKXF0Q%FK(ssi$0 z;&#eDP`)h7`n*Ac-|at*GO`K4$k-b~XB%apyn*3Cc|p01?Azq!u}L$1FcjnkX;cYh z&Dz0@(JH3+nm9Ep`!umk6F-Bc=6-X!IX1ZZ#MO6*Y16qd_?mue!w2GN$figv?|s~H z2UFN+o3LTSsHhfaPe6LS2AE3w@J#>Fyybq(dy@wXN3avF0qnqvAI+ ze{Gb1Fg$cxt;Yp~gNG9>=uUm!F}jL3&p)-VzpMX0d*1;d$5E{PRnP41CFLwx!P%Cx z1%r8*_{bP1Fwr6#EMshtgDl%v;3y|qIfu))ai;tK zYIgUuce;FgC)wOw*L13`uCD3ss_vehb2jM$TxY}d57*b@&b7b0>e*HIe7UK3Zr>le zFLPXRTkpQDt#Qc@*6dhv<9$^@>(9c#e8a)%ta~*dDT!WwXYgg@guT7U zt^<{RQK>pAbE^<3C@OyTClFjQ9(t<$eUubelqV(cMbUxO%4#j47+9mq?R-oaP=A}D@DiJX+$Yl`Wn}C&39D%Rzdkx z&~}hVZ8wxhL6-iOF?ACflek8!-3|QOKovg?fZekCoS^RjSQ-HA<9#l(rx7W3C=3T5 zeAYGWHN#QWNFALe5KmMc&Eb3y)2j$QI;*SyOcr5Q7p?jkT^wuRozZz0VC&sqNpTj_ z4+8zGM}qyxbX5L9rj=*@EpXiS#87unP7o#*hw019f7E20t+w+l@ovsvUqcvvA;<<_ zUnlmL>wbUray`qh^wz#h!aTijVSL-dg>~!S`-btW-|@ydYc4$d&@Gp|>4*&%opbVr zi_Sh{?FDDOe%*yC1)MI`P~cVGHw4=?|ki)e_b+v z>_ZDru6Fb1`wrkti=nvO27S5kp_|t3y#JQJjjpM=NrK$<5SxUvUlx%+IAAR&%czqL z1p}3#*^PKKnV?F>sud)Dr*sKcfd)uOKUBXQjEDBL@^Cp$S=bWCGOn~W>=<{XixP+-YvhCX z^4=tc#zbw2_-9+x*ETVcK^;)&(0)rA3MyA&o#Ocr%1|O1#!!%F4Epbr+hywc0C}-R za7KWlOdJFv(q@d=c-UccbI;6OB+$#bg3qu6#OAYgfSZd!T>9LiUjsN&90Z-=EUG*Y z6T6hR#I>gkXZuF#iX@Pv{*Jvy#7u$kFG}nF7UpsYboEbbRkTT^;k}Y72Xf@#HFqT;+S-m7XzIa#C&wPrNH(ysLP8bCnVQYB>LD-*{Ii z5;a#(cB4K!t9JayHqWa2(3XqOxp3np=bXFo;`2`3c)@uGue|Wg$(t@cx5kklPGiz1 z=KFxOa`Csb+n4=x<&OJq{Zr5u`;HTPoptV7h}=XQZ#hZ4Jaz@xPaJ4U6QJQDSo*pe ztsOlYv-h2i!wx?LM;vh|jyn8c95eSI9COHlIOgE}am+#c;iv;=W5&dBFj~g3pf986~)V1TK`@YuRq z96D<{<+X1CRCX#u%JRs$2jhsMd=ICL4>)LljGH(CM!%{k22Ob>f{KapR`JZnantrw z>PUW0Q(0HhEtx;x^L&{?F;6Kb4cLob1mx;qzyyZ5pkt#8f*<;d`P>s`=&Kp2p|b?` z|F0<%Ma+pD6CJ+$-x*KimP$XbtA82+8ZUsv1v1Y!@iV8No2)6M74BmvZE9d>85~y8 zAGl*j46><$@*jq6{}Q9ZNzCi+%AX@Dwle$lboGf62LrY`FNG*Pc?7J|7d@W${Gyr=4%E;)Hai81q>J`3#@kCm^_7 z#C%YUTr4nehsb{b@XD{DWleuK$#b=+j@!jVfyW#u=`#imHew`XA82sfLmfz5{ zW68~r)tgm64kWx@48A}?eg|-s)8Uk{)A(2+`OZ3{IN&k#G?<#Nk`7b$wwZ7lMX)!C(S(&CmymtW>1+&@l#om zl+x`0jF3D;ea9Pd+8yge5j-lqapT8P`Hjce2?Q#^F%u?W4E?baH9j8W7$3**IHrvw z&iKASgkPXLE((t){RymR0t$2|U=-`E8dV3xG*V5O1Zrxkjx$DPGbv8!(S=m}0Sy4hV}K5bBGa9b*GiSq?*gJPu+Z2pUO zbHfkU|Mu#~wl7MyBAS{}xcYc!i{vq`@Hb!b#yRURIA{K}?6eC^P5dJ<=RR#j6|h|C zAfGU*@Sq~Edb;`3g$_FZT1_uP9<9p#b{J+d-Dw`57s zxa8I~+m_t)`<$Qu8U$aY5%4X-T@G%eLu3fh8zB8k784~V*u@MJC?pV^7#Oag_d9Su z9Lm+{)TvW2ipsIJrW)-~U4^;`5~!_8AnyASElWjM4va?WT3b7?4q#V(U|ftrqP7Od zQu(O@JYE$c72*l=j=)J&h9@0!I8LVVaWa)<0p>E^A2^Ko9pNO>Q#g_IPoQyg3~3*z ztM5q@;AjXq8q;V88aL!qd5r+OUn)z5^7fa)o-wFN#8F3C)m0~;va6;1YHO-?$+V^h zwUogG8Yu@JbN~+EE3=lf zycu#4rHZxxrH8+S%9~?tcq$g;&G6JTQcJr8l1al!{WWkhzCEp*fMdrExb+{>2Xgvel7qt&;l!;U|B(V6 zPQG+VIoL;>b>Bglzc$FaZ?FIDwU2FB_Kh^L`o$cPWgAz{FI{xjK^rbQ_l?uXyZ2ed zK5l%uN`zqKf4E9NA0qP^Kaenvb*Pf3H~dZtRmSL#B~SZ`;3Np%A|n3Fk2)MP zX72-2RR!mHfCBl05r7U6&kU%K^2_ltbI3DE+c_AB(O8%`aU#Z!8;htu@Y4E39BWS1 zB>aX0a3B-s6N=7h7GSbA#(up&Q-v@nr^;VJ+cGFZ!&I2_T=WcvqdJN^M>!8iRU>tj zByi-GDHFwb^JoZ1goQT990k8OD=Rjb1wX1+m@Cuulwz_OE=w- zOc9|w#+bFcdwSu*_{MjiIdkKM=bn+TuDc`-^D!~*8W^LB|56co4JWC6S+b768I$+D z*YS}fMxMus;2_THuQNv8>xub{IP-a~^R6_$cloU9i2#*AYQGn)z3A-MuD{^C1N69H zpv~E_^yZfOB{x32Bj5U6gzgKZ^KHUks47Z30*vlOjzUpxd~QAap3k5}6RKm683>R- zm4J^%?pEOBhHUl_*1QsOLS589fRI)7Y4I17e;#3vs!%aR5hbZnFE0$9ty zfT;{mLAIH~A)m_h&{wGyjTit!FXxUm*y{MvVq_mqB!0)7jxp^FK{ksp-{WG-kj^38 z0snvd;>ZKaDP;4p?GEmD&bdDC0yy1qNVg))P{+D{*$w4Y4=l_H$#^k{7t@LxXnovc zo&84Wf@|0PI{D`vORw*C!sx0(_HNn!p7TbnzvQeV#?_|&12%CPJX}rLU+H=BZmynB zWI1l-qsQk!oR@=Bqh%Cm5J1dXobW%)o!_Kyyrk#bE5=lhdiT1E&pA_fKAyPn%vzOA zzZ{Zs?{R+m0%?3toc%ks#Wse+e$yL*0%{ovnN!kPj-8g~7Uc5*S!8cw zBKf5L>1UXQ=vcbUNmLV{zL?0T%wZYU>J@)af=9z=>g0(y;;@4;e$qr(#vMBZ$X|iy zBLAQ-1n{CC@yuH#+F&`C=>JP-ZRH6aX|jfb;02)pQq_~Er&dD0S+*p~D&2@GG2|1D z$6=+k!TLd9q5KS%&Ty1>6h37z994mjKh0->k!#o_;KVcD>8x%Xp*Zpw>cZI|M8LCu z^}EP!Pa=_8F>gdWvXshfy_ z7fYzM^CAc9PlF)$`Sq(e{7ApS&pP@xCJKn6KhN86;aMl-tv%m3^GShx0Y*N|&i*Qh zOabJTp!Ctr#i<-|K&CQq3dH=U@#OO!6Jn3wai7 zIY@d0(%@KObI=ZIjXF(KHbrkm&~Oo`tfkqVG{44{8KVnJ@u|9DD!_k3*bc z?^obB1C((9!rc+Dtps&!ez^$SWuR@L=%_?ppFoYSojV(_cI`Se*3*_(s!E)`IA(XO$_3Cxx5JIDCiOlqPhJjDsAi){lFhV9h3wP ze$UNyV&nus>d?cbogiBM=lc4^J(zINWFKa3SgyPDkSZ5*+`Vtg&3N2fat zdhYLd1kfYmkYsAlM+=O%)Q0w3*)aUV#@}7Ltgrw4Mp@!aB5U7u_PC7~opX`|@rymr zTm_iVi81ehnEeGvP)76=``-q%y&i94#mGEkd9yD!t*P!n&DOJHel8IO?YX;X1ug<3trl^m9Pz~w(P);mNpI*$0igMqKl~U z)~(Z;3*f{Ql)u@fP)p_aq#ujNFmu{e?gC7pf#QLR7ZCDX0`Sr>n8u*G?)Ys@XR&U3 z176y^9WQO#Mxg9ehAPVqn<&?fSWWq^p`2H)T#YB6cp9r;T8$hRG5Rc2GF1L5+(4|$ zOy74>fEBFqK0>*BW})OIR%1aUpgesL;ebb|(R%>eF9tzpsEexX?eOvF#Gz@#02u10 zT1MZibq7GaeL38?LMzMY6m;A^3o;qlAn26SZPXKyLj>~fZ96v~g=8{%t}7Q`md$zD zC!7m^&8e;1iP=)=2dO65Fv|+G8`2v-7qZW9wl4ewXNNCNUc2E(TYs0_*4OIEt-N^t zg3;^GKleBjlM6-6=P8S;A!Y#tlS#)TjJSdXsG=~|895ok9TAalIM2Jb#&_@AT$6t7 zx{DX=uirjUnHJD{zqdU4lbt&sxaE$(*e_UTzv-NdemcmK!@i9qj+_%Xu|dN@K99Al z*5DtHJcgx9AHf659>bDHmSf33{)HuvJ%J_vd>Tt0e+KtI`5abm+=}R|m?Uf`V3f@% zIuTHIh%SazsZyRa0E7ybh5(T}m)}!y2SIgJ0<&gJ=P>{c0K;iPfqbI`Y=~o%=)v(u z@u4ezw>CE8k>_8+k|$PR$>UE`c25;#_>ada(|=+KW&6NGk7DVAkKkd-`o-s7K&quR zdJg(56i6Nr1WqJV+m?0X_RF|u=FZn@QM#VNGoH0(tiV*ELRZKJDRii)LbNMM#&X_Q z`B#!=_q0?RuF-UlFp^Vg3HWjEWHB`%CDnB4fM z>+fB@{02?yQ|&Ku3w!g0XCG4Kn}uEspZARUym;~&5sU>e(zhvuZk<4l#z`XbM$eaP zVdR@eypOr@c+-Z9&O73zg|8dqk_AH&y~E>wiw+RO z0fOSCVS>gqO;~ZHa5$lYkfSl(*4%>Dre^4S0nH3IQ#nP@+J@$~Hl%rEpq424MrR;7 zB~4K3^wdc)Okb;}QUf&1NaylWb9LcVQ;R3WYH=|8~@@=J`Nv9AkUW&w`XQ4z2V7b$u8#N{Epr4zDO=JBxK(vhR7H;aN8+EJ^DB?OQ3K{gH#)$&?PxGFMmsaJEDG zP)FF&bA;!~(+#ARG3doG>*Lhs+Ck$As)P*txX>Wbcmc>3)1ok(Qi$#v>I?;(2xZreGpI(| zl`12h%XN;ck)hwH&xaNrA>`G} z`eeV`_H )&V7Xv2G}k7nQ00KLM37G(W;~q)KooJPUwvusocE2(gmkqDx)_Vj;RV zq|;<7^brX)N~*{$A6V!vKt+fR5s~P5s!WH7emPkRS!>#E$`ePzLnna)rfwN8B1f=e z_qes?XsHIdEYu$eH{vi2Fc-Rb)jMaM{Uw_I!1hJSwv=c81cy6FvwekaZHRod_1~Vv z?4frY?RvKH5}f;?NY(fCH|Kq=RX>Znv#jxb000mGNklr9++? zT2olNy&h|}?jUT#nk|IQTd^jBE%XVSI>BbfqkQ6RQP{@w_?_6^)Pjue6q0Utbt;D( zBM>n(01P6%s=OD$p}@1^DLCG`q6N*+TNjyzd4}miP+6*$CmDK<)D;+u^1wK(^<1!J zYNr%Z6f%OId4wtcDUQV9Xs;5?6;kB&ycdyfoe-z z|4bN0=Y%$igZxIU{CNA4@3r=&ZU@M!cb+#TOxriY$5+_Ck3nQVCUf-)j9e9zfCwKE z>R`+n5c5SE17EF;xr^3UwH>qeU1yKGy)Qcg%a`Yy9=PRSCZ4|DS^HJ$jz`I1Gl7qi z_aS$ks?nLX1X10k{Gv}!v!kp|H)eH6=^QU*YI=7xN;ofW-WI)caPL2##J!I_fqNhQ zCt*4M_V8o4m*?pB@*I74_(viCk>$kw7w-KB%RTlamOk?W4S{-URPJPyD(K`ZKMEWx zaKgPof`;4C^hp;Z%0GCcE2i}B6^i%0glOfV?l^T#yQ*rG;cmrf<_e)=PT%hcx$DCD zuVOSSNl5v!G*Hy9B;D?5IcKcdG-3dBFQJO&aJ71ZKx%p->YRnX{crpJ@?Ur449D4i zo^L+kjQ7@A@8V7Xj{@T3y=ScruXoOEv0*-1{i`9+9g!hQ&vHU~RIIse$I_cy`cj9x z^wQXdOU^kU7Iz=?Jo8-nX`Jsz;6!M{E6o|Xa zQ8r9tQ6`Zt?_D}(QO1U-Uh9^1jO~uqn9Yl`*4sU2a49N_fChm2gQGNbr7eZ8vLCt{ z$|yUbxnVc-Hh{}Yj06WPfrYnwFy3+O_&q8uUHRKw4jcdjuBxn{#}Rgb_;Mw3!39&^ zlAOHe)?_wg+#Rgv#vs$$tmpq__`?un_jYbQVDH@V;E!}$xW_ssVrm&yWB`IWz2L>h@?LV<_VzFX<>gD^hz3|rX+%C{Xn3SRumqK(`-OyY({qk(_G0oZkY?sFtui2C_J#7LqLtL#1Ck*6xz%2KTiHO`C}<{gSq zkRLGsD)abyrlRNnGK|ofG|@e!mC~4q0*nc`7hY|51_aiB>WRv*EpO7?(GRH~KzqU>nXMensC5 zCcP2Y9|`aw5WS9l`E1`aUysB8Oso;7>W7zle^D@Lx8Hy3i&?mDI)^V?xMxXg@b3i( zsn9tT0*;=OqQapMLP zv#IU4E{X_1IVn>2{1ZpzBxN#-Wa&W-fP!o}-IQ>Y9>E!wqeJ^89z|8vQe7>WS4d3a znnHm0Am(#ziVTAlce(=*a@|{T!WAH>I}-}70<`)-!a>lIZmX{6)9eHK2+E)&P$M<^ zM!ufK_bBg>tADEldr-A?Dl8CQQ^lB%#bWOI88fFJ=GNuYZMpE%fc+b>2R{juNM9TW z{#H&_4>c~iwZ{(=xn$B@vhod6B=9cg{P81ka)`E~n>t34hg$;dP<)nC*tOmlGv0TQ zi(R_5Ds|B6OXgQOK+wb0`oJyQ<0=0q*2<@;Y#xJi12aNd3+pP>#IV+?iXXyA85lo? zcvTguYHLwdSBq-;1*k!F4WXJ)RgJxJxKHz-un=}rNZ5LsjNraQ<`S#l}6gAZpB zDQ_xqk7M1ZZwRW?g>l^#i1N9@hXV7|*F2Bm7;y|i(_l$Um*_p!OSY9KK)Gbw=o1~~ zS1Dfb1&ZGpJSzLDM1nG_CQxQ7J3>uu4QlFYDcd?EYU}9NBEfV|{|A7fLBMBW2`B=& z@L6~DrpIz#!>TITzG4!Sm{N54M9f(MEt_IO1-+0MKc@q_Ym`Bya+K9_5WZDh{^p=2 zSm5P@Y&OId>~xP1a)Tn?m%^-s?SBad_Yu7?*D%o8F+*NhzGQqD%7?}HU*Nnk79wQ7 zGv-Kqk4iwFh(y;Ro+nqvJab*l)xLJ!yUreadoM2uZd!grs$pvLUoG4x_`Ltd>8JwV z3y2tk0~MGQz@zdXKY23d9(g2=Klvn_a?(jS?S$iT`f>Ac+OfwHj>M@)AC8lcn2UX< zO@(lEOO=Yw;K~GDsAX3|6lVjd>e#*AZe=^uinJ>gw@2{95QZRJ#YYNV!vsw$fwb=! zgaGmM=XRd6G&n5dlpyP7sQY@!Jl!sa%R*7QHlUd1AkgWjD|!>gj>54AAAnPjJRGMU zbp%d3<|v#t?`WK+vONBHoO zjH`7n__#p+$w8spZe+1?^>v^ml48@J8(Y2ksSfF#B1`7aPqez=6LL;97&6tj3sF)5t;&9BKH4FMhyjjzyVHTDB?5UG6o5sPM$&)a9(gcj-DprK_ z95@tq0kxJo>~>L;ln{ksLxAJF>UO6QWfkdyieHtxDu1Cu2jeAB<-R+FkTM*_)wqf3 z()t|A>3134jrpz;C_^Tb%`WNH#1oh?ejMgZo`^Y9D6=V)-;^nsHI+bls$6GM-ZN+J zgM$w_1oKWj5pxba5MIoOJ`Sfh0Wa|==9-;5Q(e9f)zUJ`!iCL5bwnPlbCie9YpX=$ z1|#wT9$npOog3cYJ@=s%`dx>iTKw%^Jv)!XOO&xSyJwE1421+TO)bA^{^ zix>BJ9A&M0Q!XDQvMLkDk`>}%gv?M%z!~b?{dxPO5${VI(wk3Nzi?qSpC0F# zZt7jMC~R0Vb4A|TtEqB-POJeK0W4K?tP4(99i8X!@ma5*RYR98D|tc5nRkUG$N!1nAkf?pHf7xy#Q9Dl;=?)H7}p90a3Qw@}E#mlM8 zc{~B_I~6D1H9^kv_Ib@RdK-bEm%z$fF54akxu398Zf9-ucrUsz2w-^x;MnhrNfq}j zC!8R?YiEt~q!E$noH0HM$hY7m4$i`aA#fbo4(mMNUp*10Pz-XpN%UW%gP|prA$yE< zUuYy}?~_2C5%V9Ohp(HO^gC9bzd%3gBHgo@v`r7*yqeGZS{i;g6Hixc^!nktpkAeC z`!%%?<8flefWfi*DHOOVMxumb7%tLPs;>-{xn|K0=(0XN=LN^OZvPuaEP|F5r9&vx z19Z_OpqLuvIkaC&rn@>Vu{57vM<=AB&@mO=(qVo5$ zEnI}`$|{ALW^S`F&u;;w$AtpAzsPCy65(UM*vl!~kDRi2+5N}eu{K zK7%l~``jwgh4CUdlAuOF<=Jp1!>+x`h1*y5MEveEkF3roVn-8qmf{7JNn1M0IfE&h zFaj?Z33w1Wmhgq@nE%NQ7o9VmPf(}-?)Z20_usO?WbAd;x*rj#!}wslXGh|l5!9&G zRTaB_=d6Cmm3+j!21=zhti&U&XdzTB3YuI(Y4?d2(8%AgaTB&~+JqpV1DqukunYHg zZ0aUvF{-;6C7>)(0?UD_=}{bNI-vWD#996B`I@LumZMRHvZO%;Eyu5p;Z8(E9BP6f zKcVn}$d0-5rY{fkL9QOor8?&HDHalI3+aE&<#XqsbmGImIOQ+P>X}9<@J^RT=1Cf; z&q18xGG)&@H4JG04;Ld&AnB@@c|j{2F#uEw!vxRY{K=MlKL34)Y+#j9r*s@~fo^qk zurO^%T(tVn=m+3DihtJj;X4y<0N{90G=YLY}5dtQqbt` zFp{!YOF+28#0UA`o|x;lod2dndtugX|HrLcGin65@J9BTeoxTC3b^8nw5R? zBb~{Bs&-J&VbpNEq68&X9mQD-jR3`EsJ*_Bc{>{#u=J5fu=Js&XxzCIPG9U(L?OpT zaP+MhSe~P_gT@IwT!CXg=M5-m*qK7vC=9gMS6+81tW^;O5#Ldu`U>#ox$w zYkQ-5l`v(YHPfpJvu^U1S^7EPC7M(Ku-ldV000mGNklGW_8kn-wB!h@6m71U@L_cj}|Ut9OCvtE@MdCb-oT`TKd{`lj#=e{Law{{K3V6H%v z6v(*VfudqpC2u{0kVh5~@C-%cj%k_)8U_lM=^?=qPvazjQl0JWu>STiSS6u%=g2+_ z60yB3qouIQNM%(HnpOanYY}MN=({`XR!@#^AgSjFY$5M^(2;%Q zgAi69;_&bhipXJs zdIPM$57RYmaT+cgz7FnmvJrE7J$WeoFn5qt( zbx)S+VlONONBrTEz|+$=ueL$>LpZlC$mX5n%4A6D<|JT)Ji=^$chf4$4Xtszp{aK4 zrkf4<*+6Oy)m zR?}va%YB;K<37i0H(bu%Qc7f4gTnsKF-6_sSj<6{PngT0rM@29Hmt{{wQI3~uzvk| zY}mK~8#ZmkMxM!U+_D9Y%`MRFdX}P^!Y&|9O;h@XM2#shg?WYgxC6kj3j=IqJxi(h zpIZJHQcX?Z)&mg>^eh#BXbdcutCq^#6#?`k#qUuhW#AZgz7O>+!+{?|K;J2T(J*jA zdZeoybq89na-i}uY=rs|Q^i|*oCI_sN{?7tVi&ZfGuX0y8#Zpa5P=0Ld`t|Fu ze%(53q+Bnq%Ov+z))GEcITAZD-ZqwGpq`BL*I!$heQR$XE zU{*)0990O%4elg%TeA4(#@2w~**a+E-fM!=Dt@jt%btlIN? z{Cx1DMd9|uhBZ6_`Uo4UyJhscaITOhC7)3$0^VK02;dmd{_m)q5dpVbg8){6@vbR+OFk3MxYa2%@s3@0hOgj);($6c_+mKypyc~;ittUTaMFxn9tFQm?!CX->Zw;D|p25?~+en(Fh{zqd7$YWr?JoqCKNJvxu+8gt;O z&W3w>^{>otaiLkwfFpG2m?0Us(GYV~mk3OxjL_1Sh8@1a!dgdMr6s^;9TUM}G2>mn zu`2Vf9*uw{OM-@->E|r)QL3M<-SHxkb|Gat)nG#HqM+dts3J04Z5d}F1hfMS5g!EY zW4qPC>|JSP5#mSbC7(Pv8zP@!Pi8$}BNbZ}zcpNU6Lh7ot9MoMA(ePQC7+MQk)v|Y zsUJ@u8;?hjqpxXsrUwK|0P!R6xJkyH06&pHO-&8rK6efxX1cj4Bg!X12%WJ?#*?Ni zs`|TumTLzK4n(NTlwKrPwpqiabY@b{4*G1`!L?I0%pV(Oa{ELn*IctJhY2k$qjC;* z1t7bfjdDV563C~k;r-vMPPu>M8SEn%z!m1~^VYd%*`yv9CIcYd=OWtQ@_8DcWTMZ7 z28XEfTstlmZy!s15QC#QQo4Hy9Qf|9ojrcR`}RSy%NL*Av}d=yqTz949!dAN~pUSpP1v?6!nH)Bq^PclY$`e_nvu%+dduK(A|;=L#dGKbdr{YlM;1 zmxTn_*JIQShlwvA-CUD?N009WtXPz*$+tXUL;D#vu9eV(BjsXlq>M_@lCOC>*_Psw zueSndmfDx)5asAi%a%Y9OJViP<3iQOG)DtK6~E>77pVM&3cGgnXiT0m6;q~8$K+`< zFljnr+Vq02;RyC&T!DEr2uz!-K)lHcEI)b56ilOk(Cj%lV8%Wq@1dOM&f$nXwn1g0 z%3ghFq5^r{cus>ocI9>VNgvqo zaFCx~lGxofBA%5IVReOOipRwPlI5?x!3)YeAbC z6}lX^;&l6;BeX&k`%lI>!O`=I zc*o#`qmIT2M;?U}jyMu09CkS79eOAZnmH47)iqu6tU9Ftj``%73j$UAMhB5HUi8%< z(5QkYQO3mYLJo@10g7!^uS0rSgn6vFETKbi3~X?4hm>MOi2YxG=Bw;Q^aq=@CN$*`XP133fx4^e3rTQFh(6a`Un zFX@c$#|}%>)V;MTll#D!)83^Wjk51p)7HDc+UDoeKj+~5J)G6A1a?FiP|T53fH0RH zQ0WR5YT+Q;)Ur9<(i2VJ1-0VrA=phiyO#=iR@<7+KnKt6xg#n4kpPt`EN6Z7OVFS4 zgT>|Jjyu7T#&{oH|E}{+?}=XhBR^|)LGGInxsNR8JK~a3lu+_@hGSTju>uNV$1o3n z0s8F#%|moW3X`Eo36}dEv>5TVOaw$KcckltQ!F8%5g^>Q-*>dNRMRg)qKc;hF+^=554-VPOrxxz&lXfRk@BY@zyu12jnYgww!{&; z;54Z#S_O=N(w@Tl@HMG01#5gu8YMBbfP5vLgfBS?^%(EVx4Yt8j_7xoxfxWj{rK7?Nqea=!**%Oya!sR)IMKK#Z_QT#noSp?13Ashvu}wK@s@ zSzetfkBo#XCxJO@ZjBqyOsyI}X{<5we&4Hp=lB!PAJtV2tN-}LZMj_bht9ft;9QT& zva9l)(;NqTs6Q{{nPlg@;f!-WsOM1Kh!u-EP1qf|5EHKvBL@L2f!?olCWmY;4^>{h zV~=2TCjs4l+|kquh5F_;G_|Fo9Tt7rGT;u9Dh`G2%2y&?AXS`%j`L#fVR1wgN0XhvSIEI~eCj~=AL|@}p3{**jK7>d`aP*bnZa|Gi zrzCd)i$S53!m=pQGY0ymzh702!{#1|gAY6eW57nlpbsM|iN5HLfD*7pc9zdcwVr)-%qc81VJjym>Khnk-;5-b~ge z4w%p&3+lMa1v-bRbO%YSZ^c6=Zey;g`K5Anj=l2eNVrlGs2La52xLm4Zj6alRZlY_ zA2qS~*;CHEuy*%4#BFN!UxI|+Wk-L2u=_WAcQ4d!ilgGUdw%t={Otfle`?e1piKfM z2RGV5qR;=L_bXETt*I<5C$&DHkDzoT0rp9xvw5`9plZovX#iwV-AWC z7d5tYrJ|EoH>3uaEK(yzPV+tUu}$whw}(kNX&dHDe~fzU|D2%3m1-44%0}akvw8`@ za0ygNse)1;R9FyBeF~*8sIW*++t>j#jlgW)SrPB;P8cr(rLA=cmHnY}568?|v$*1~ z0f^IqCKc~+#X~Cp)|O@neKi8Oz@V@wDe?fl)Mr$K3Q?4|-T7lNwnN(jm01~Ro7L#C zLgPx)>BAIc<5)&P%SFCcQUD@jd?v^pR)}`IOEPI2y>J7m{I9X{^;1rKXf?yGBUb-o zQxBVVoWOf41bY0yPJlgfQN+;)fK<-I#6Z?K{Og{j&$pG=x5orf&crJ*?9%z^5UKWJ zo>WhmEaG|liIGp|)Bb`kjesS|K8bd5I* zsbpn)AU%I_^ZzZJ>x*#{;4q;-Eaf<%rPCuuK!0S`8P{;K_PNihJ1Lf1ks%GLfJZF3 zAVek?M210ATN>M&+Gq^4K#hT1K7itPrZvbo)r5>DjW^hsyX>KbCs#AvwY%k-Y_>Xf zJJt1{0qJ+4*l)iBu;0G> zW8c|xFn-*40HjJ4!+zVYCn*+oP4HqKetOF%000mGNklE6IqpP8j`*zIgtEeQ21xO^i8JAYC3k^pE1e zu>+2=kYi2dJa8)gDCv-Pp?NTgj&d9^0Qw=r{-ByfVg@@m;`KuU!c|%#9_zM*D*A#!5Fo{! z0yPG9Hn*aYy9DW64%Sv~`_jP~h)fb=-aa!i;Xl}Sq&vo1Pno*I`2Kfbuo7I`BDxe^ zl{9^JCenf`p;?C=f&Gs<7OMPbA8;_{?0+Ek<+=R6bN0i2RQ&tRnvH$;nThc{pN|wc zY3ax*fwqvmFtw8_)}i%8P&5E)$BxG=()Q!=Xr?XE*%N-2&uH70O=#Y}4f<0(J}^%I zYz^lKP7nac5Tyl5EovO2P@|zMUTbS=FmviO?8{w^{btXm?DoZeZ0EjeK<$43<$4fi z(Xg6z@LcSB&)D@?l^a*hwX$-3at&iSTu!brR_ z65y(KUs4hRym%F=#!rIrJdb1HC}Z#`xYUC2C!RlA^UGoDZ69xwZ2Ink5n%ttC#U1F z9HqywqvBTsphu+P$;^nuj-72T`kx2gRoND#5)d<$`5vKPPL)$DcLBoT8%X`)Z*ND| z&RG?UK{U_{vyR$9<*hpdEj)gyZ%&~>4FMVkwsMAm_JKLZmk+GHXu;7vvE=VR?Fljd z%?@DyVgu-)08Vub2nX3DDyT`b_CxL1@kmtHB2iV1M4|*$NF)gH1QG<#^Hm>0yHZD> ziXu&Qj&#+B3QPg4Pz=DZDmP0S(`L>_qPnIFIvBPzwY;;rJXijku;In$VKbTN9RQRm zemL-oZuIH+d^Hv_qhtFEd<)wdkHt|%StU51B~*416y>S1t*T+$Yf(LVEGBRVWYX;Y zP&H~aiL0Cl3~3!sg9S(cZjPVH>0HU&p>tlo?p2(8|ARHm;S)9V_n4!oksF$wwC>uJ z1LTDX{?2vibnQ_=bmHX|6&2%6V}m033MZqQiId?cY5);mAV)jreT-*?Zys~n2gae( zweHXV+Ys8!pI|L_0_@`)G+`%{Zldhk-0HJ?!S+8HY-L-w;mQ3wZqqWO+=Y3s3gV_P zO$fcY-SV?kK$&c1SEIXC@(NCFV~ zOZBIqNBhSA$m&bxkL?OAiED7s^jGKN<3|6MV{0jKI`%OEd@85W6DCt3MPm+*NCO5f zNSY)8C&hq9G)&N)1IsZkT=h?!qPDC+i|h%|XVbBkV3BZeC#}%^2 z)v^A(ueZ9i&sr+sP=V*bVHpGrtAfb$XiFi-t>P@V@=K86nSLgnhC-UYLMlbsx3*yW zwyjvR@_D@Y)Z^GhzpZ`;^yNQL*?@IHcK{;ak?9hE_BWy{O5v?H{`;CQo)1Hah<9@n zC*$~lPsaOi@Vh}7RPwfk3+s%DzttG?8WI{!*rQJ8s0^QJ$gwj_j5v;gCQgqJbX6Z^ z-&OKR+Mp$1xUKEbE?*Hr4Ob-bx-k%Az)8+$9Y@grZyVT4C!cche(3aH(zY!6%=WDK zf3P!EU=CU6$x3G0)ze%48Ah z1aoWnxxl~-5`hv1QTJ*d;fFQ zf}^^^(tjdTH+7bqI(zm*#xr*T(u}eygIpHrwpIk(sjxtF9zRgu73n%M1+qqRyBy=3 zU||he_Bo`*>$R>Ojg5G5?HW9{@&!D{E&69)coxq-zXH#4#s2KGPh-WiPvO}W3QywM zXP(50r%UhzR#3S=^Yr6TSn<@qu;R&o;#tD;gjG*JflV(yhlY*ok!x=1sQfuL^d-RP zXMoCmsZkm(f}^i6P&t*C3rRndYD0#G2^c16I;BoeT`4H8mf3U1u6%MjWm`&Bj6p*Z zFxjfqna;Sk@G;FGK_To>rv`u;bxw`3;o(Ge#u^(lPjywD=_+v~ZIBY6B4>x22H{^B zDW5BZy2;bwC#nG=0Pwh3cnBcx4P)L%CT^}d8VeTiTnz1(8UZ1X1Jnp`&h_{cK}Q3? z?j;5Q1oa%vy&3>9CqD58ZJDdm$?!P9a-wS&-bkQN5~zw(arFtadn8WcP(!ShhFD`; z8m&Bc4s_+&eUV;rcvL2Gs5SnF)_49@z%|#f%K+mVGVa*7AJ<%4SN$vYx0gsT02xsw zXSvGSzIHX5c5EYG0d&DNU?Aa=(~6yr-6Ck%GHk`b5h5i>q@L<(qEg(nb#w7Q>aD}Z z^=q+l-5SDbY+Adj9X6ByE$dce>-qw0V|d$!HQ2FXEq1WXPWttP28J6qtV7$jEy%0l zr|}~^3M{PrLqv+745?g%yg{MVpmLw-ec_*^dX%RcgsnV!s3%Y9=0=pBiLfrMUx7iU zM|W;NxoKiECr7ag-rjTJ*>lB{cNj5;k!*~x$DNK5-Qi(39Tr~rNZTm)NA&B7T~#<@ z0CXjlL8i^AJuC*1i5zH@eHk%M6p7JeQ9XV#jQ(SfC`*J$vp6W;0popQVrt5nW9MJe z;p|h71H8~JwxRn!XYF4|cejrP(KvvOw*PxQDTrzv;+vy^+YrR;j^66Yxt!0o*Y<`7 zphpzTSt*syBA*XA->}>Fh&_^7E`bDBu10%ddEx-W`W*CFpozNy3Rx;*H}Ki6irV39 zuXf@ax?^WldLK9IgkzuaJ+qh^r6E#s>k zn>S#~mQC2q{LNIFTefe7D&>IX^aZ+dU=oKLYo~l%5f}r1z!u?o4Z zUjXi0MwDUPr$YA`_vpu1&ZkWMd=5T+Pa(*| zm4k3F(mVm#17%bMdUP&o69bw{1Z{6ueCsc6w$<;1%1h<8W$Px&aAQH1l&Q*g8;y~z z>sDbScQe*=mqLvbOMXS4e+eRKI@ab`sRF?9Kc4U!%)2y(3SAp6dhG-%{&yOf(@3(8 zu*aRYHlw>^DvROa#&Ln!0Bi5zqt)tnt98Tx*i9sZPLO!j=n3r5wcz0DOwDkzjqw;j zUkDUw7r*eDB#?8&c^`{~xN!1m@7b^XNMOzHla0C7s(VB0{?9u5TWYzEKd*_xD-AL=0@AuOqlx_|&tV}G*`GeJ<-#*3N5zFBi-E&dEOakyZgE>$vu@h{`({Rs z9s37?JWmCe6DmAad?6KPlPde?pTWjwpTq_l1M6rEta)JtR=@Zx*1YsQ)~tFFYq;v( zLFE>(-)PS%hmdu#e%=etK>*nWj^Xk&RiNeNy9ztjqX0`|U4c6#LQo~{vk%8Ojum_= zbd5*pWnqnb#8-uH=nKa_+JQB){a_uSFO)mE#jcAn^4#7AW;gMV>nyLO%2p#0ts_94k|x z*X~4Wn0NH3YFCk813Y%=ycneVY%y{!n^WjF?Un7hK*(p6)gvkGB*Vi-o{xV`)@$wX z`QP%Y+ci*m;z)Qf5^(UV#!Ui*TltQ|3cFn#m+-3UVB%GP4n`C&RfK|5oiQH@ja)Wy zOWkP`Pkqm*VlKAc{qeTy^tgv;GJo4S`xED!ZhPu@<#-QagIo!9kNRwMMj#DMTN+Zm zF%q|wk?X_vxO*eSe4SU)w6HT&I3q>?1=2?+B8ZLw==ZTmpV0Ifq%t|^uLRm?41{6S zRr^%749R0(IysQUTW;skS}9#$1l0Ec#_<@Swk_-SJNl>>J>UB)C$xGh6R5&5R6dRh zHLqvU4Lgyp-+>GvMQCf>iPpw?v^F&m8j($>VL4lN2JKS{QN5Ip6Pg&{x{3*!?wDo? zV48xab*9ld&T$Jw3Y+qvQiznlDtLlNh?IIUU5KYp*v=<%1h(jsRArgoxg82A^(osN@tm&w$)`R8 zp%3MWKn}?HFU^9i*@SZO|Ju!3q#ybCJ#E`55^O zD?qQ0B$@^C;OT#Us-Zi4(_O|$`T!-6{G9lLnGkf@@pSN84kqKr5vv{zFGf3ogAcnd zo$wA4;6jM`1U&byN%P-(Kxuny<>Je->u|{9Vb1=LX8QLyfc`$eYTkBs1a8h3-T|I~0_ws2qzR#8rseqhS)%xX@R+g*_Y+r)%fhxpCs; z^^@i=9MpFGwoA^MYK3zr%GltiVHkYF*>JH7g9gVz*{Qlc6>YY#jX=`&#sGDL&UKVVkC^?+%u)HP z5fG@H`&IJa7dh_xFsdAeKmH{(#RI{>P540jz=5fv1dg-}^}>=!=y(UmqB2jkFj$zEtlyaQ+& zfuj!KptJ-hfQx*AW;rl{PSY$?I+O>{IUX4RDikVs<$xzJu$DRJ(Q%WxhZv9?tM6R@=xX^5oJ%8>lVOI3q>J zbb*0jvh(Ckj^Sd+$xc>v?}?8*vfB&e zOmBClJv0)ihym`^QwN)#KWDkcpy2_K`1dLzf<$x-&}Fx+m@M=~yVguD`o}_gCd$6rXWAli zI6QOa`Hw7!MTM@pChFsdEnL`c1WbO_Q7a6vj7q0bIN{Pqq>Lg36> zgmGQn5*=MZR0%sYMBoUj_`{-aIes-AP`L>#NM3=3x}v3Nst}+JR{#--UvQ*Bx+sBR zOB6J&evjY*s?4d>5dk+!yfWYpk7L~WFBs3Wd25?FY<=hcpyC-4pdy0$9pUdd<2HV{mr|3KW1-iIJ0avb@YD#%)9C6f^G>M$ z_EDh(s$$fu8en)RT}b87mdTf0` zGrrs{7!sGt>8Q7eR0{R|p{zX0%use32IVG9lPJxW$<`rK_*y0c5+yY$czQ2_ zEBYvk!ucXHEAHHRJU%2t=+pJYg=f|xk$5``yapnp`c!C`khEXv>VH^wA|0Ao?<2C^ zt?I3rlv!^mBl&%jfO9f;QrjdW@?XT)6+69-jt>Qv$_Q>($1hSi)nOkl+X)Jt2MZC`QkR zVZ5rE3vlv6X>^c*@~=CW!d1Vn@I@0SD_gUD+zZ+TS|>*baN%y{d)3W5s=Q;05vy~gRhqeqdB@)!=L*mz6WXq{br-16nU z&cbFmnEE%UL*{+zhS9NFv(O066Jb~ErqPzG(r!Bg3;Ky_#ApO~GzP?Q3$GhG0(i`t z!jX0si+##ieBSrnhjTUIdE$FhLeJdeZ1_c3{E$QKL4qn-dMq{6PS}2*lITM{zKdlY zOs`)I>;tJTQB>AxZaHW44O~uGT_TMnMiMZ_Am$s02!`XboD_9+)ZCUvu8WP&J6W)O|2^;uL^84;|VMwuR3rmrg-~JcO%%P<~LOis`~|N!hCpO;dpvePMY%1K!J> z)=-FRej(I;&x*y`>Fv>;1Mj`y4f}Dv`!9Cc0|D`|mt86DbOoUuHm)eyHYe`MSeXAu zUn(_yK^np7NdiuezWkOkA*#<7F&_m?b+_0Y6jle24v=n<1Rhr$vDz{4t4G23ae%!W z-CiOBj3v&=06q-k{ug4d_H1~ugEtFs_XFkz=KY4%KTd_%%rN^2??dD`<8uCu43u7L z$Ro&dg4jgFu6bg{V4YgwIuQG_(j07^FURsirW5*q0q75xD;NQJ=m&#xx;xf~ij60) zbwPTzTewh-V6gAI2*8cwYv68QxX?o4OR12!1?qxGEwl4S%85SHAyi=Pii}&?VX76m zwl+j|fkV5`Abcw3iB!rn4m<+W4>|(lX$-JFL;$^1atgc3b*57uCkre@d{Y2x3<3gb zM`6rHzv3>_>Z+nU7fbz+=HpA&}`y;u9h!j7SzhPsf2nG4FX;EDLBG4;1 zM^Aws9b3YKh4Dl+caF9 zQMhgsoZL025%*8e$4_T`9jSUqs>pnJt3zAm%C-`8q^C?!+F(^rrxKA0W@M`eyP% z4?}XXt{5keSYL>HUYOyOvw_UmzTgiOM^~6>TH|@{zzmeeE4`eE6kY_RKb&nX7y-FH z8v*QJlj0`!+O1VhRUP;gaqRC-m^jx?SW83nUrcF;6cYzsq^Q_24WXdG9Ki7?G|KZW zO-MKFK%lz;#I+p&a8%HSTks~HfZ^6TtLW&oQi68kj%6J>$5|^tT&SE0O51XhkFlzp1we>%w8C6qBm+IzE1?0fbpnPSxGO&G+X&d078RA&KBU~j>n`BIg^uMj^Re; z^{InkU{}mo7w=2ay1JGO7ZsZXt}BbbFC_YmqsIVg8UX=UG<`1gZ-yB6J$QyaW|-S; zsXS7gF!h~)UX3(I9dG39T)t`yJMr$!(hykk_iI7}`yiXKfs-P;JVQiQMFn;^_RnY) z3o0;$Fx7_C_ASUXH!|v=I;|_v@vCG=4RGzDD5^JYM_1*_(+Z`?wvJ_E00oZ*K79-6 zw(fEo!xav=>Tlk(1|fF?w!HrvOSmgLK+b1qSJ)2=qER5&(8O z$nWCm*dLhb2tB%Wb*%@Bm=9BgdRjms_J}_0nL5X$vRQVKUYk-W-W>b+W@qJ|yZ^Rq zb6<(>b%N_F<9i5C4*2l55=UQt%j|I}DpTM4l<~Z)*b%8{q5Zo@wj2izA5xDvyJfP6 z>ybd5MnGf;wC@J=T3tISV)}$}gt_GMR1Q)-8ep^?rP6_ff$O+B>!yZZFIo{;=WVB4 zI{t5iZtw?_tdny(n?r_R^;ryt*ekTGXAELA0z`(gd!WlppO60kfR!|K5>8*=OahF%u} zt1o%;Sl@adWFI&V5H57ES6!CYSt^}Hs0P#UamGF_&%ovXlH`->Yqh4Yi2v4j3vc(R z_(vc6(cAW~N{oITXUNN)_}>s?&PVjK6K=9|0?`$IPjYg)eHIOtp0K*k6K)Us+zp7= zjfY=78YUKpaNDqJ>>g5O*VO~opt2lXbx7z7k=+Rq4*&oV07*naR3tlVOu{Bk7y5q< zIu;p%SionXE6r>!51q(|po*6@N%oH7F~FW_hCY*)R2FIo=(`);bK%DD=}e8&~6L$sP@p8yq_&7_dpxE9T8UO=v;86l60IG2Mo zI)IipYCy0|lu=~BF~bp=E=umsFQ8K`iUMQwIFCSqT=iJCJDQbDR|S^Oq|v%%0~%j^ z1_4+9P-VvUM)IPsMnJ?U+pJETfG)1?(o3ig$P-p@2K`~t_n2gW+jquQoCIt=6~K#Ke5xc;>&2$bOtrBnn1=A0^=tjW^a3pQZ0f$zm((Pw{h*W0y=y4T^a+%)b|0@2*~&K z&GP}*^ztm2i1}du-eb&%oH_NSKz&A*%BD+3M1%_9HDkrrcA2k4{0IWl-g;2u_l{s{E8-@tln$bc7pyBx^c_gq2IV!(=HiLkDC7_WX83kbgk7d;e zV4vkQ;8+`hj^q&1?+I)H8)O90#il@X43oPa%Bu0I%B6d-2*_8rb(6UcWoh69Wsv8J zU*GmmH||8!+85FA+~WwEXghI^Qv*OKA8m8N2yhUUA%OyjT-27bo0NsS3_4iu8f;+R zFY9H^-|6@2rsk#`CLXSUfqv?@*D87?SYZS;T6G62q^v7Ux?6I51X&tkPq5C#6o~5< zv5O4tbzLO?n?dwJB)0Fqrv^d%omS#k88J6{p8s8AybHwh zW>S%Rx;+-8Q^++nA+vKk{YK!_ulOgL*RDj{mUU>`x`D6}xrUvQ0pO92r^W+)*1Ri~h=voU3ER}0G$ zcMCdzJAdx4*;fB2Sh6J84WU!Q`h{;8FHSxIkwaOp-zj^KN7)|a*k3a|VjM#lRx_P>VrRV)2Mk@{BYt?AN}!+!#{rO z{zo+4!mX*FVWUeX$x0r!e?)a;BoxwDpwUAm;A8Y z(aWq~g{G(ff%@eSpy9Fm(fH2?(e~og&?5sar#k@}Rs(=a2_)GoX!Sr*ZK|ihxTaf> zGy|RKxV7NZ`jcbE}mYvFfKLz+r^kSQC#uf@b^T1NLlIXpIvJ>tIrt0W4X|~GYM$-=~vw^ ze*=Ju9Xz9-o4h@K@<(s4p8LV?)*XHMEn`pk^p7XZ{rD|YkNWJ7XUzTRP5U3wH2bi8 zZTzIP8TW>&(0gZI5|@jI>wPh|8sGfR_q;_CGvA`Z|1UA}DiN7MU`MlJIRS+nfVv&1 z|4%%#V;h3j)=1%ZmUhS;fNXOkEIUKzJa+cok_MYV%a3(&pbnN$CCdxqG`U7inUJRZ zdM7MPmIOIxwm2uN+p&is5OU(z57A_3v0K;YiE9`*+p|@$-=mr*t)7U!m+`!VTgSe; zl@qW2kBL@tmjm3e7zSBxQL7OUbTyvHsBdrr`RRTOZ21j&fly=17Vr_%XdpVMd%z|* z2>XD7DmFud!(%;$bqWF8>Q@lm0;fWBMF3aQYA8hI*l!F03M8q{@zxVCyUYp03iOJu z+p;{SaOA~tf#JwQ0rGH~Rs;}7$?Qgh3654pR94%;`OM|B0Ou<&n}tvCNY{{#aK4LH z{`3vypo(8XC@YYLu&oN(=G{5h+It4F60+*&%c;J?Q^^DEO#5umUs_htod6D& z>-(|K8sC4HG2UskDrZya*K$5FG;5_4NodX|VJ5C<1*K zAmCPj7-zm}z91 z?%rre)Ky4DJ9>&cHBGH)sKLhiIylyQY^F8jaZ`un4v_%f3MOoh1}-^cKV#I9ZO0_E z8v(*GZ`25&67>|q5QZQGlz{0V9vqvgKB1rtL0S%$Lg@_({fu?92n+G9VMl$4Zp*t; zeaZ*xZ0DzFq-b2{ELB83)eq%Aq7gvY_kD(Aq+zJ+g#f03ac#3#l!YOVXq{ate`TF5 zD0h4&<P zT2qpvY*VHJEQT4cuU)> zU64IL2ofL3BHzB=aJ2Kn1A>YyrwVa_M->qO538a;6q_L1= zhhVsb;mX3Lw88m&{2ZK)R{P-SRJgRjP*h_~V8)=27bo5pC-NxKIkSS^@Hw^G)d(1Q zDR7s}<1yb)c>cJW+W4AN7eDm3lkZ%5Nfv<$(ha%KfeQeVK+te${H z6tw`3UOR9+Gu&NSr}d};w`@k_Q>^NfM`#)qH^^I;JVe!SXhk?dyYlxL7Xr$_ke;^N zkhYLs0UXrOX_pu26`QOff+K+PL@$@nP;?I%!Hrw*zOAhnWNf|g%$j)I{-+puEphiQ zeMN@Ug_M(i&f4mO(}dxH&^h;{6Z_i}?|pRVKÐ(DO2QQ;r?v9Hw5{pgM358g3j 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 +export type AppConfig = Record + +export interface SupportedChannel { + name: string + display_name?: string + config_key: string + variant?: string +} + +interface ChannelsCatalogResponse { + channels: SupportedChannel[] +} + +interface ConfigActionResponse { + status: string + errors?: string[] +} + +const BASE_URL = "" + +async function request(path: string, options?: RequestInit): Promise { + const res = await fetch(`${BASE_URL}${path}`, options) + if (!res.ok) { + let message = `API error: ${res.status} ${res.statusText}` + try { + const body = (await res.json()) as { + error?: string + errors?: string[] + status?: string + } + if (Array.isArray(body.errors) && body.errors.length > 0) { + message = body.errors.join("; ") + } else if (typeof body.error === "string" && body.error.trim() !== "") { + message = body.error + } + } catch { + // Keep default fallback message if response body is not JSON. + } + throw new Error(message) + } + return res.json() as Promise +} + +export async function getChannelsCatalog(): Promise { + return request("/api/channels/catalog") +} + +export async function getAppConfig(): Promise { + return request("/api/config") +} + +export async function patchAppConfig( + patch: Record, +): Promise { + return request("/api/config", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(patch), + }) +} + +export type { ChannelsCatalogResponse, ConfigActionResponse } diff --git a/web/frontend/src/api/gateway.ts b/web/frontend/src/api/gateway.ts new file mode 100644 index 000000000..5a58d48f0 --- /dev/null +++ b/web/frontend/src/api/gateway.ts @@ -0,0 +1,62 @@ +// API client for gateway process management. + +interface GatewayStatusResponse { + gateway_status: "running" | "starting" | "stopped" | "error" + gateway_start_allowed?: boolean + gateway_start_reason?: string + pid?: number + logs?: string[] + log_total?: number + log_run_id?: number + [key: string]: unknown +} + +interface GatewayActionResponse { + status: string + pid?: number +} + +const BASE_URL = "" + +async function request(path: string, options?: RequestInit): Promise { + const res = await fetch(`${BASE_URL}${path}`, options) + if (!res.ok) { + throw new Error(`API error: ${res.status} ${res.statusText}`) + } + return res.json() as Promise +} + +export async function getGatewayStatus(options?: { + log_offset?: number + log_run_id?: number +}): Promise { + const params = new URLSearchParams() + if (options?.log_offset !== undefined) { + params.set("log_offset", options.log_offset.toString()) + } + if (options?.log_run_id !== undefined) { + params.set("log_run_id", options.log_run_id.toString()) + } + const queryString = params.toString() ? `?${params.toString()}` : "" + return request(`/api/gateway/status${queryString}`) +} + +export async function startGateway(): Promise { + return request("/api/gateway/start", { + method: "POST", + }) +} + +export async function stopGateway(): Promise { + return request("/api/gateway/stop", { + method: "POST", + }) +} + +export async function restartGateway(): Promise { + return request("/api/gateway/restart", { + method: "POST", + }) +} + +export type { GatewayStatusResponse, GatewayActionResponse } diff --git a/web/frontend/src/api/models.ts b/web/frontend/src/api/models.ts new file mode 100644 index 000000000..6a4544c65 --- /dev/null +++ b/web/frontend/src/api/models.ts @@ -0,0 +1,91 @@ +import { refreshGatewayState } from "@/store/gateway" + +// API client for model list management. + +export interface ModelInfo { + index: number + model_name: string + model: string + api_base?: string + api_key: string + proxy?: string + auth_method?: string + // Advanced fields + connect_mode?: string + workspace?: string + rpm?: number + max_tokens_field?: string + request_timeout?: number + thinking_level?: string + // Meta + configured: boolean + is_default: boolean +} + +interface ModelsListResponse { + models: ModelInfo[] + total: number + default_model: string +} + +interface ModelActionResponse { + status: string + index?: number + default_model?: string +} + +const BASE_URL = "" + +async function request(path: string, options?: RequestInit): Promise { + const res = await fetch(`${BASE_URL}${path}`, options) + if (!res.ok) { + throw new Error(`API error: ${res.status} ${res.statusText}`) + } + return res.json() as Promise +} + +export async function getModels(): Promise { + return request("/api/models") +} + +export async function addModel( + model: Partial, +): Promise { + return request("/api/models", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(model), + }) +} + +export async function updateModel( + index: number, + model: Partial, +): Promise { + return request(`/api/models/${index}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(model), + }) +} + +export async function deleteModel(index: number): Promise { + return request(`/api/models/${index}`, { + method: "DELETE", + }) +} + +export async function setDefaultModel( + modelName: string, +): Promise { + const response = await request("/api/models/default", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ model_name: modelName }), + }) + + void refreshGatewayState() + return response +} + +export type { ModelsListResponse, ModelActionResponse } diff --git a/web/frontend/src/api/oauth.ts b/web/frontend/src/api/oauth.ts new file mode 100644 index 000000000..a1ed1afcb --- /dev/null +++ b/web/frontend/src/api/oauth.ts @@ -0,0 +1,102 @@ +export type OAuthProvider = "openai" | "anthropic" | "google-antigravity" +export type OAuthMethod = "browser" | "device_code" | "token" + +export interface OAuthProviderStatus { + provider: OAuthProvider + display_name: string + methods: OAuthMethod[] + logged_in: boolean + status: "connected" | "expired" | "needs_refresh" | "not_logged_in" + auth_method?: string + expires_at?: string + account_id?: string + email?: string + project_id?: string +} + +export interface OAuthFlowState { + flow_id: string + provider: OAuthProvider + method: OAuthMethod + status: "pending" | "success" | "error" | "expired" + expires_at?: string + error?: string + user_code?: string + verify_url?: string + interval?: number +} + +export interface OAuthLoginRequest { + provider: OAuthProvider + method: OAuthMethod + token?: string +} + +export interface OAuthLoginResponse { + status: string + provider: OAuthProvider + method: OAuthMethod + flow_id?: string + auth_url?: string + user_code?: string + verify_url?: string + interval?: number + expires_at?: string +} + +interface OAuthProvidersResponse { + providers: OAuthProviderStatus[] +} + +const BASE_URL = "" + +async function request(path: string, options?: RequestInit): Promise { + const res = await fetch(`${BASE_URL}${path}`, options) + if (!res.ok) { + const message = await res.text() + throw new Error(message || `API error: ${res.status} ${res.statusText}`) + } + return res.json() as Promise +} + +export async function getOAuthProviders(): Promise { + return request("/api/oauth/providers") +} + +export async function loginOAuth( + payload: OAuthLoginRequest, +): Promise { + return request("/api/oauth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }) +} + +export async function getOAuthFlow(flowID: string): Promise { + return request( + `/api/oauth/flows/${encodeURIComponent(flowID)}`, + ) +} + +export async function pollOAuthFlow(flowID: string): Promise { + return request( + `/api/oauth/flows/${encodeURIComponent(flowID)}/poll`, + { + method: "POST", + }, + ) +} + +export async function logoutOAuth( + provider: OAuthProvider, +): Promise<{ status: string; provider: OAuthProvider }> { + return request<{ status: string; provider: OAuthProvider }>( + "/api/oauth/logout", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ provider }), + }, + ) +} diff --git a/web/frontend/src/api/pico.ts b/web/frontend/src/api/pico.ts new file mode 100644 index 000000000..9a1a553d5 --- /dev/null +++ b/web/frontend/src/api/pico.ts @@ -0,0 +1,38 @@ +// API client for Pico Channel configuration. + +interface PicoTokenResponse { + token: string + ws_url: string + enabled: boolean +} + +interface PicoSetupResponse { + token: string + ws_url: string + enabled: boolean + changed: boolean +} + +const BASE_URL = "" + +async function request(path: string, options?: RequestInit): Promise { + const res = await fetch(`${BASE_URL}${path}`, options) + if (!res.ok) { + throw new Error(`API error: ${res.status} ${res.statusText}`) + } + return res.json() as Promise +} + +export async function getPicoToken(): Promise { + return request("/api/pico/token") +} + +export async function regenPicoToken(): Promise { + return request("/api/pico/token", { method: "POST" }) +} + +export async function setupPico(): Promise { + return request("/api/pico/setup", { method: "POST" }) +} + +export type { PicoTokenResponse, PicoSetupResponse } diff --git a/web/frontend/src/api/sessions.ts b/web/frontend/src/api/sessions.ts new file mode 100644 index 000000000..56ef148db --- /dev/null +++ b/web/frontend/src/api/sessions.ts @@ -0,0 +1,50 @@ +// Sessions API — list and retrieve chat session history + +export interface SessionSummary { + id: string + preview: string + message_count: number + created: string + updated: string +} + +export interface SessionDetail { + id: string + messages: { role: "user" | "assistant"; content: string }[] + summary: string + created: string + updated: string +} + +export async function getSessions( + offset: number = 0, + limit: number = 20, +): Promise { + const params = new URLSearchParams({ + offset: offset.toString(), + limit: limit.toString(), + }) + + const res = await fetch(`/api/sessions?${params.toString()}`) + if (!res.ok) { + throw new Error(`Failed to fetch sessions: ${res.status}`) + } + return res.json() +} + +export async function getSessionHistory(id: string): Promise { + const res = await fetch(`/api/sessions/${encodeURIComponent(id)}`) + if (!res.ok) { + throw new Error(`Failed to fetch session ${id}: ${res.status}`) + } + return res.json() +} + +export async function deleteSession(id: string): Promise { + const res = await fetch(`/api/sessions/${encodeURIComponent(id)}`, { + method: "DELETE", + }) + if (!res.ok) { + throw new Error(`Failed to delete session ${id}: ${res.status}`) + } +} diff --git a/web/frontend/src/api/system.ts b/web/frontend/src/api/system.ts new file mode 100644 index 000000000..543c8694d --- /dev/null +++ b/web/frontend/src/api/system.ts @@ -0,0 +1,62 @@ +export interface AutoStartStatus { + enabled: boolean + supported: boolean + platform: string + message?: string +} + +export interface LauncherConfig { + port: number + public: boolean + allowed_cidrs: string[] +} + +async function request(path: string, options?: RequestInit): Promise { + const res = await fetch(path, options) + if (!res.ok) { + let message = `API error: ${res.status} ${res.statusText}` + try { + const body = (await res.json()) as { + error?: string + errors?: string[] + } + if (Array.isArray(body.errors) && body.errors.length > 0) { + message = body.errors.join("; ") + } else if (typeof body.error === "string" && body.error.trim() !== "") { + message = body.error + } + } catch { + // Keep fallback error message when response body is not JSON. + } + throw new Error(message) + } + return res.json() as Promise +} + +export async function getAutoStartStatus(): Promise { + return request("/api/system/autostart") +} + +export async function setAutoStartEnabled( + enabled: boolean, +): Promise { + return request("/api/system/autostart", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ enabled }), + }) +} + +export async function getLauncherConfig(): Promise { + return request("/api/system/launcher-config") +} + +export async function setLauncherConfig( + payload: LauncherConfig, +): Promise { + return request("/api/system/launcher-config", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }) +} diff --git a/web/frontend/src/components/app-header.tsx b/web/frontend/src/components/app-header.tsx new file mode 100644 index 000000000..7a50fe0fb --- /dev/null +++ b/web/frontend/src/components/app-header.tsx @@ -0,0 +1,193 @@ +import { + IconBook, + IconLanguage, + IconLoader2, + IconMenu2, + IconMoon, + IconPlayerPlay, + IconPower, + IconSun, +} from "@tabler/icons-react" +import { Link } from "@tanstack/react-router" +import * as React from "react" +import { useTranslation } from "react-i18next" + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog.tsx" +import { Button } from "@/components/ui/button.tsx" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu.tsx" +import { Separator } from "@/components/ui/separator.tsx" +import { SidebarTrigger } from "@/components/ui/sidebar" +import { useGateway } from "@/hooks/use-gateway.ts" +import { useTheme } from "@/hooks/use-theme.ts" + +export function AppHeader() { + const { i18n, t } = useTranslation() + const { theme, toggleTheme } = useTheme() + const { + state: gwState, + loading: gwLoading, + canStart, + start, + stop, + } = useGateway() + + const isRunning = gwState === "running" + const isStarting = gwState === "starting" + const isStopped = gwState === "stopped" || gwState === "unknown" + const showNotConnectedHint = + canStart && (gwState === "stopped" || gwState === "error") + + const [showStopDialog, setShowStopDialog] = React.useState(false) + + const handleGatewayToggle = () => { + if (gwLoading || (!isRunning && !canStart)) return + if (isRunning) { + setShowStopDialog(true) + } else { + start() + } + } + + const confirmStop = () => { + setShowStopDialog(false) + stop() + } + + return ( +
+
+ + + +
+ + Logo + +
+
+ + {/* Center prominent connection status */} +
+ {showNotConnectedHint && ( +
+ + + + {t("chat.notConnected")} +
+ )} +
+ + + + + + {t("header.gateway.stopDialog.title")} + + + {t("header.gateway.stopDialog.description")} + + + + {t("common.cancel")} + + {t("header.gateway.stopDialog.confirm")} + + + + + +
+ {/* Gateway Start/Stop */} + + + + + {/* Docs Link */} + + + {/* Language Switcher */} + + + + + + i18n.changeLanguage("en")}> + English + + i18n.changeLanguage("zh")}> + 简体中文 + + + + + {/* Theme Toggle */} + +
+
+ ) +} diff --git a/web/frontend/src/components/app-layout.tsx b/web/frontend/src/components/app-layout.tsx new file mode 100644 index 000000000..ff9877bae --- /dev/null +++ b/web/frontend/src/components/app-layout.tsx @@ -0,0 +1,27 @@ +import type { ReactNode } from "react" +import { Toaster } from "sonner" + +import { AppHeader } from "@/components/app-header" +import { AppSidebar } from "@/components/app-sidebar" +import { SidebarProvider } from "@/components/ui/sidebar" +import { TooltipProvider } from "@/components/ui/tooltip" + +export function AppLayout({ children }: { children: ReactNode }) { + return ( + + + + +
+ +
+
+ {children} +
+
+
+ +
+
+ ) +} diff --git a/web/frontend/src/components/app-sidebar.tsx b/web/frontend/src/components/app-sidebar.tsx new file mode 100644 index 000000000..dc24f8781 --- /dev/null +++ b/web/frontend/src/components/app-sidebar.tsx @@ -0,0 +1,215 @@ +import { IconChevronRight } from "@tabler/icons-react" +import { + IconAtom, + IconChevronsDown, + IconChevronsUp, + IconKey, + IconListDetails, + IconMessageCircle, + IconSettings, +} from "@tabler/icons-react" +import { Link, useRouterState } from "@tanstack/react-router" +import * as React from "react" +import { useTranslation } from "react-i18next" + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible" +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, +} from "@/components/ui/sidebar" +import { useSidebarChannels } from "@/hooks/use-sidebar-channels" + +interface NavItem { + title: string + url: string + icon: React.ComponentType<{ className?: string }> + translateTitle?: boolean +} + +interface NavGroup { + label: string + defaultOpen: boolean + items: NavItem[] + isChannelsGroup?: boolean +} + +const baseNavGroups: Omit[] = [ + { + label: "navigation.chat", + defaultOpen: true, + }, + { + label: "navigation.model_group", + defaultOpen: true, + }, + { + label: "navigation.services", + defaultOpen: true, + }, +] + +export function AppSidebar({ ...props }: React.ComponentProps) { + const routerState = useRouterState() + const { t } = useTranslation() + const currentPath = routerState.location.pathname + const { + channelItems, + hasMoreChannels, + showAllChannels, + toggleShowAllChannels, + } = useSidebarChannels({ t }) + + const navGroups: NavGroup[] = React.useMemo(() => { + return [ + { + ...baseNavGroups[0], + items: [ + { + title: "navigation.chat", + url: "/", + icon: IconMessageCircle, + translateTitle: true, + }, + ], + }, + { + ...baseNavGroups[1], + items: [ + { + title: "navigation.models", + url: "/models", + icon: IconAtom, + translateTitle: true, + }, + { + title: "navigation.credentials", + url: "/credentials", + icon: IconKey, + translateTitle: true, + }, + ], + }, + { + label: "navigation.channels_group", + defaultOpen: true, + items: channelItems.map((item) => ({ + title: item.title, + url: item.url, + icon: item.icon, + translateTitle: false, + })), + isChannelsGroup: true, + }, + { + ...baseNavGroups[2], + items: [ + { + title: "navigation.config", + url: "/config", + icon: IconSettings, + translateTitle: true, + }, + { + title: "navigation.logs", + url: "/logs", + icon: IconListDetails, + translateTitle: true, + }, + ], + }, + ] + }, [channelItems]) + + return ( + + + {navGroups.map((group) => ( + + + + + {t(group.label)} + + + + + + + {group.items.map((item) => { + const isActive = + currentPath === item.url || + (item.url !== "/" && + currentPath.startsWith(`${item.url}/`)) + return ( + + + + + + {item.translateTitle === false + ? item.title + : t(item.title)} + + + + + ) + })} + {group.isChannelsGroup && hasMoreChannels && ( + + + {showAllChannels ? ( + + ) : ( + + )} + + {showAllChannels + ? t("navigation.show_less_channels") + : t("navigation.show_more_channels")} + + + + )} + + + + + + ))} + + + + ) +} diff --git a/web/frontend/src/components/channels/channel-config-page.tsx b/web/frontend/src/components/channels/channel-config-page.tsx new file mode 100644 index 000000000..b19d11e6a --- /dev/null +++ b/web/frontend/src/components/channels/channel-config-page.tsx @@ -0,0 +1,539 @@ +import { IconLoader2 } from "@tabler/icons-react" +import { useAtomValue } from "jotai" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { useTranslation } from "react-i18next" +import { toast } from "sonner" + +import { + type ChannelConfig, + type SupportedChannel, + getAppConfig, + getChannelsCatalog, + patchAppConfig, +} from "@/api/channels" +import { getChannelDisplayName } from "@/components/channels/channel-display-name" +import { DiscordForm } from "@/components/channels/channel-forms/discord-form" +import { FeishuForm } from "@/components/channels/channel-forms/feishu-form" +import { GenericForm } from "@/components/channels/channel-forms/generic-form" +import { SlackForm } from "@/components/channels/channel-forms/slack-form" +import { TelegramForm } from "@/components/channels/channel-forms/telegram-form" +import { PageHeader } from "@/components/page-header" +import { Button } from "@/components/ui/button" +import { Switch } from "@/components/ui/switch" +import { gatewayAtom } from "@/store/gateway" + +interface ChannelConfigPageProps { + channelName: string +} + +const SECRET_FIELD_MAP: Record = { + token: "_token", + app_secret: "_app_secret", + client_secret: "_client_secret", + corp_secret: "_corp_secret", + channel_secret: "_channel_secret", + channel_access_token: "_channel_access_token", + access_token: "_access_token", + bot_token: "_bot_token", + app_token: "_app_token", + encoding_aes_key: "_encoding_aes_key", + encrypt_key: "_encrypt_key", + verification_token: "_verification_token", + password: "_password", + nickserv_password: "_nickserv_password", + sasl_password: "_sasl_password", +} + +function asRecord(value: unknown): Record { + if (value && typeof value === "object" && !Array.isArray(value)) { + return value as Record + } + return {} +} + +function asString(value: unknown): string { + return typeof value === "string" ? value : "" +} + +function asBool(value: unknown): boolean { + return value === true +} + +function buildEditConfig(config: ChannelConfig): ChannelConfig { + const edit: ChannelConfig = { ...config } + for (const secretKey of Object.keys(SECRET_FIELD_MAP)) { + if (secretKey in config) { + edit[SECRET_FIELD_MAP[secretKey]] = "" + } + } + return edit +} + +function normalizeConfig( + channel: SupportedChannel, + rawConfig: ChannelConfig, +): ChannelConfig { + const config = { ...rawConfig } + if (channel.name === "whatsapp_native") { + config.use_native = true + } + if (channel.name === "whatsapp") { + config.use_native = false + } + return config +} + +function buildSavePayload( + channel: SupportedChannel, + editConfig: ChannelConfig, + enabled: boolean, +): ChannelConfig { + const payload: ChannelConfig = { enabled } + + for (const [key, value] of Object.entries(editConfig)) { + if (key.startsWith("_")) continue + if (key === "enabled") continue + + if (key in SECRET_FIELD_MAP) { + const editKey = SECRET_FIELD_MAP[key] + const incoming = asString(editConfig[editKey]) + payload[key] = incoming !== "" ? incoming : value + continue + } + + payload[key] = value + } + + if (channel.name === "whatsapp_native") { + payload.use_native = true + } + if (channel.name === "whatsapp") { + payload.use_native = false + } + + return payload +} + +function isConfigured( + channel: SupportedChannel, + config: ChannelConfig, +): boolean { + switch (channel.name) { + case "telegram": + return asString(config.token) !== "" + case "discord": + return asString(config.token) !== "" + case "slack": + return asString(config.bot_token) !== "" + case "feishu": + return ( + asString(config.app_id) !== "" && asString(config.app_secret) !== "" + ) + case "dingtalk": + return ( + asString(config.client_id) !== "" && + asString(config.client_secret) !== "" + ) + case "line": + return asString(config.channel_access_token) !== "" + case "qq": + return ( + asString(config.app_id) !== "" && asString(config.app_secret) !== "" + ) + case "onebot": + return asString(config.ws_url) !== "" + case "wecom": + return asString(config.token) !== "" + case "wecom_app": + return ( + asString(config.corp_id) !== "" && asString(config.corp_secret) !== "" + ) + case "wecom_aibot": + return asString(config.token) !== "" + case "whatsapp": + return asString(config.bridge_url) !== "" + case "whatsapp_native": + return asBool(config.use_native) + case "pico": + return asString(config.token) !== "" + case "maixcam": + return asString(config.host) !== "" + case "matrix": + return ( + asString(config.homeserver) !== "" && + asString(config.user_id) !== "" && + asString(config.access_token) !== "" + ) + case "irc": + return asString(config.server) !== "" + default: + return false + } +} + +function getRequiredFieldKeys(channelName: string): string[] { + switch (channelName) { + case "telegram": + return ["token"] + case "discord": + return ["token"] + case "slack": + return ["bot_token"] + case "feishu": + return ["app_id", "app_secret"] + case "dingtalk": + return ["client_id", "client_secret"] + case "line": + return ["channel_secret", "channel_access_token"] + case "qq": + return ["app_id", "app_secret"] + case "onebot": + return ["ws_url"] + case "wecom": + return ["token"] + case "wecom_app": + return ["corp_id", "corp_secret"] + case "wecom_aibot": + return ["token"] + case "whatsapp": + return ["bridge_url"] + case "pico": + return ["token"] + case "maixcam": + return ["host"] + case "matrix": + return ["homeserver", "user_id", "access_token"] + case "irc": + return ["server"] + default: + return [] + } +} + +function isMissingRequiredValue(value: unknown): boolean { + if (value === null || value === undefined) { + return true + } + if (typeof value === "string") { + return value.trim() === "" + } + if (Array.isArray(value)) { + return value.length === 0 + } + return false +} + +function getChannelDocSlug(channelName: string): string { + return channelName.replaceAll("_", "-") +} + +const CHANNELS_WITHOUT_DOCS = new Set([ + "pico", + "wecom", + "matrix", + "irc", + "whatsapp", + "whatsapp_native", +]) + +export function ChannelConfigPage({ channelName }: ChannelConfigPageProps) { + const { t, i18n } = useTranslation() + const gateway = useAtomValue(gatewayAtom) + + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + const [fetchError, setFetchError] = useState("") + const [serverError, setServerError] = useState("") + const [fieldErrors, setFieldErrors] = useState>({}) + + const [channel, setChannel] = useState(null) + const [baseConfig, setBaseConfig] = useState({}) + const [editConfig, setEditConfig] = useState({}) + const [enabled, setEnabled] = useState(false) + + const loadData = useCallback(async () => { + setLoading(true) + try { + const [catalog, appConfig] = await Promise.all([ + getChannelsCatalog(), + getAppConfig(), + ]) + const matched = + catalog.channels.find((item) => item.name === channelName) ?? null + + if (!matched) { + setChannel(null) + setFetchError( + t("channels.page.notFound", { + name: channelName, + }), + ) + return + } + + const channelsConfig = asRecord(asRecord(appConfig).channels) + const raw = asRecord(channelsConfig[matched.config_key]) + const normalized = normalizeConfig(matched, raw) + + setChannel(matched) + setBaseConfig(normalized) + setEditConfig(buildEditConfig(normalized)) + setEnabled(asBool(normalized.enabled)) + setFetchError("") + setServerError("") + setFieldErrors({}) + } catch (e) { + setFetchError(e instanceof Error ? e.message : t("channels.loadError")) + } finally { + setLoading(false) + } + }, [channelName, t]) + + useEffect(() => { + loadData() + }, [loadData]) + + const previousGatewayStatusRef = useRef(gateway.status) + useEffect(() => { + const previousStatus = previousGatewayStatusRef.current + if (previousStatus !== "running" && gateway.status === "running") { + void loadData() + } + previousGatewayStatusRef.current = gateway.status + }, [gateway.status, loadData]) + + const savePayload = useMemo(() => { + if (!channel) return null + return buildSavePayload(channel, editConfig, enabled) + }, [channel, editConfig, enabled]) + + const configured = useMemo(() => { + if (!channel || !savePayload) return false + return isConfigured(channel, savePayload) + }, [channel, savePayload]) + + const docsUrl = useMemo(() => { + if (!channel) return "" + if (CHANNELS_WITHOUT_DOCS.has(channel.name)) return "" + const language = ( + i18n.resolvedLanguage ?? + i18n.language ?? + "" + ).toLowerCase() + const base = language.startsWith("zh") + ? "https://docs.picoclaw.io/zh-Hans/docs/channels" + : "https://docs.picoclaw.io/docs/channels" + return `${base}/${getChannelDocSlug(channel.name)}` + }, [channel, i18n.language, i18n.resolvedLanguage]) + + const channelDisplayName = useMemo(() => { + if (!channel) return channelName + return getChannelDisplayName(channel, t) + }, [channel, channelName, t]) + + const hiddenKeys = useMemo(() => { + if (!channel) return [] + if (channel.name === "whatsapp") { + return ["use_native"] + } + if (channel.name === "whatsapp_native") { + return ["use_native", "bridge_url"] + } + return [] + }, [channel]) + const requiredKeys = useMemo( + () => getRequiredFieldKeys(channelName), + [channelName], + ) + + const handleChange = useCallback((key: string, value: unknown) => { + const normalizedKey = key.startsWith("_") ? key.slice(1) : key + setEditConfig((prev) => ({ ...prev, [key]: value })) + setFieldErrors((prev) => { + if (!(key in prev) && !(normalizedKey in prev)) { + return prev + } + const next = { ...prev } + delete next[key] + delete next[normalizedKey] + return next + }) + }, []) + + const handleReset = () => { + setEditConfig(buildEditConfig(baseConfig)) + setEnabled(asBool(baseConfig.enabled)) + setServerError("") + setFieldErrors({}) + } + + const handleSave = async () => { + if (!channel || !savePayload) return + + const missingRequiredFields = requiredKeys.filter((key) => + isMissingRequiredValue(savePayload[key]), + ) + if (missingRequiredFields.length > 0) { + const requiredFieldError = t("channels.validation.requiredField") + const nextFieldErrors: Record = {} + for (const key of missingRequiredFields) { + nextFieldErrors[key] = requiredFieldError + } + setFieldErrors(nextFieldErrors) + setServerError("") + return + } + + setSaving(true) + setServerError("") + setFieldErrors({}) + try { + await patchAppConfig({ + channels: { + [channel.config_key]: savePayload, + }, + }) + toast.success(t("channels.page.saveSuccess")) + await loadData() + } catch (e) { + const message = + e instanceof Error ? e.message : t("channels.page.saveError") + setServerError(message) + toast.error(message) + } finally { + setSaving(false) + } + } + + const renderForm = () => { + if (!channel) return null + const isEdit = configured + + switch (channel.name) { + case "telegram": + return ( + + ) + case "discord": + return ( + + ) + case "slack": + return ( + + ) + case "feishu": + return ( + + ) + default: + return ( + + ) + } + } + + return ( +
+ + {enabled ? ( + + {t("channels.page.enabled")} + + ) : configured ? ( + + {t("channels.status.configured")} + + ) : null} +
+ ) : undefined + } + /> + +
+ {loading ? ( +
+ +
+ ) : fetchError ? ( +
+ {fetchError} +
+ ) : ( +
+
+

+ {t("channels.edit", { + name: channelDisplayName, + })} +

+ {channel && docsUrl && ( + + {t("channels.page.docLink")} + + )} +
+ +
+

+ {t("channels.page.enableLabel")} +

+ +
+ + {renderForm()} + + {serverError && ( +

{serverError}

+ )} + +
+ + +
+
+ )} +
+ + ) +} diff --git a/web/frontend/src/components/channels/channel-display-name.ts b/web/frontend/src/components/channels/channel-display-name.ts new file mode 100644 index 000000000..fe70f5f5e --- /dev/null +++ b/web/frontend/src/components/channels/channel-display-name.ts @@ -0,0 +1,23 @@ +import type { TFunction } from "i18next" + +import type { SupportedChannel } from "@/api/channels" + +export function getChannelDisplayName( + channel: Pick, + t: TFunction, +): string { + const key = `channels.name.${channel.name}` + const translated = t(key) + if (translated !== key) { + return translated + } + + if (channel.display_name && channel.display_name.trim() !== "") { + return channel.display_name + } + + return channel.name + .split("_") + .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) + .join(" ") +} diff --git a/web/frontend/src/components/channels/channel-forms/discord-form.tsx b/web/frontend/src/components/channels/channel-forms/discord-form.tsx new file mode 100644 index 000000000..300175e20 --- /dev/null +++ b/web/frontend/src/components/channels/channel-forms/discord-form.tsx @@ -0,0 +1,109 @@ +import { useTranslation } from "react-i18next" + +import type { ChannelConfig } from "@/api/channels" +import { maskedSecretPlaceholder } from "@/components/secret-placeholder" +import { Field, KeyInput, SwitchCardField } from "@/components/shared-form" +import { Input } from "@/components/ui/input" + +interface DiscordFormProps { + config: ChannelConfig + onChange: (key: string, value: unknown) => void + isEdit: boolean + fieldErrors?: Record +} + +function asString(value: unknown): string { + return typeof value === "string" ? value : "" +} + +function asStringArray(value: unknown): string[] { + if (!Array.isArray(value)) return [] + return value.filter((item): item is string => typeof item === "string") +} + +function asBool(value: unknown): boolean { + return value === true +} + +function asRecord(value: unknown): Record { + if (value && typeof value === "object" && !Array.isArray(value)) { + return value as Record + } + return {} +} + +export function DiscordForm({ + config, + onChange, + isEdit, + fieldErrors = {}, +}: DiscordFormProps) { + const { t } = useTranslation() + const groupTriggerConfig = asRecord(config.group_trigger) + const tokenExtraHint = + isEdit && asString(config.token) + ? ` ${t("channels.field.secretHintSet")}` + : "" + + return ( +
+ + onChange("_token", v)} + placeholder={maskedSecretPlaceholder( + config.token, + t("channels.field.tokenPlaceholder"), + )} + /> + + + + onChange("proxy", e.target.value)} + placeholder="http://127.0.0.1:7890" + /> + + + + onChange( + "allow_from", + e.target.value + .split(",") + .map((s: string) => s.trim()) + .filter(Boolean), + ) + } + placeholder={t("channels.field.allowFromPlaceholder")} + /> + + + { + onChange("group_trigger", { + ...groupTriggerConfig, + mention_only: checked, + }) + }} + ariaLabel={t("channels.field.mentionOnly")} + /> +
+ ) +} diff --git a/web/frontend/src/components/channels/channel-forms/feishu-form.tsx b/web/frontend/src/components/channels/channel-forms/feishu-form.tsx new file mode 100644 index 000000000..a834a65f9 --- /dev/null +++ b/web/frontend/src/components/channels/channel-forms/feishu-form.tsx @@ -0,0 +1,121 @@ +import { useTranslation } from "react-i18next" + +import type { ChannelConfig } from "@/api/channels" +import { maskedSecretPlaceholder } from "@/components/secret-placeholder" +import { Field, KeyInput } from "@/components/shared-form" +import { Input } from "@/components/ui/input" + +interface FeishuFormProps { + config: ChannelConfig + onChange: (key: string, value: unknown) => void + isEdit: boolean + fieldErrors?: Record +} + +function asString(value: unknown): string { + return typeof value === "string" ? value : "" +} + +function asStringArray(value: unknown): string[] { + if (!Array.isArray(value)) return [] + return value.filter((item): item is string => typeof item === "string") +} + +export function FeishuForm({ + config, + onChange, + isEdit, + fieldErrors = {}, +}: FeishuFormProps) { + const { t } = useTranslation() + const appSecretExtraHint = + isEdit && asString(config.app_secret) + ? ` ${t("channels.field.secretHintSet")}` + : "" + const verificationExtraHint = + isEdit && asString(config.verification_token) + ? ` ${t("channels.field.secretHintSet")}` + : "" + const encryptExtraHint = + isEdit && asString(config.encrypt_key) + ? ` ${t("channels.field.secretHintSet")}` + : "" + + return ( +
+ + onChange("app_id", e.target.value)} + placeholder="cli_xxxx" + /> + + + + onChange("_app_secret", v)} + placeholder={maskedSecretPlaceholder( + config.app_secret, + t("channels.field.secretPlaceholder"), + )} + /> + + + + onChange("_verification_token", v)} + placeholder={maskedSecretPlaceholder( + config.verification_token, + t("channels.field.secretPlaceholder"), + )} + /> + + + onChange("_encrypt_key", v)} + placeholder={maskedSecretPlaceholder( + config.encrypt_key, + t("channels.field.secretPlaceholder"), + )} + /> + + + + onChange( + "allow_from", + e.target.value + .split(",") + .map((s: string) => s.trim()) + .filter(Boolean), + ) + } + placeholder={t("channels.field.allowFromPlaceholder")} + /> + +
+ ) +} diff --git a/web/frontend/src/components/channels/channel-forms/generic-form.tsx b/web/frontend/src/components/channels/channel-forms/generic-form.tsx new file mode 100644 index 000000000..fc5a0a7fd --- /dev/null +++ b/web/frontend/src/components/channels/channel-forms/generic-form.tsx @@ -0,0 +1,377 @@ +import { useTranslation } from "react-i18next" + +import type { ChannelConfig } from "@/api/channels" +import { maskedSecretPlaceholder } from "@/components/secret-placeholder" +import { Field, KeyInput, SwitchCardField } from "@/components/shared-form" +import { Input } from "@/components/ui/input" + +interface GenericFormProps { + config: ChannelConfig + onChange: (key: string, value: unknown) => void + isEdit: boolean + hiddenKeys?: string[] + requiredKeys?: string[] + fieldErrors?: Record +} + +// Secret field names that should use masked input. +const SECRET_FIELDS = new Set([ + "token", + "app_secret", + "client_secret", + "corp_secret", + "channel_secret", + "channel_access_token", + "access_token", + "bot_token", + "app_token", + "encoding_aes_key", + "encrypt_key", + "verification_token", + "password", + "nickserv_password", + "sasl_password", +]) + +// Fields to skip in the generic form (handled by enabled toggle or internal). +const SKIP_FIELDS = new Set(["enabled", "reasoning_channel_id"]) + +// Fields that are objects/nested — show as JSON or skip. +const OBJECT_FIELDS = new Set([ + "group_trigger", + "typing", + "placeholder", + "allow_token_query", + "allow_from", + "allow_origins", +]) + +function formatLabel(key: string): string { + return key + .split("_") + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(" ") +} + +function formatSentenceFieldName(key: string): string { + const label = formatLabel(key) + return label.charAt(0).toLowerCase() + label.slice(1) +} + +function asString(value: unknown): string { + return typeof value === "string" ? value : "" +} + +function asStringArray(value: unknown): string[] { + if (!Array.isArray(value)) return [] + return value.filter((item): item is string => typeof item === "string") +} + +function asRecord(value: unknown): Record { + if (value && typeof value === "object" && !Array.isArray(value)) { + return value as Record + } + return {} +} + +function asBool(value: unknown): boolean { + return value === true +} + +export function GenericForm({ + config, + onChange, + isEdit, + hiddenKeys = [], + requiredKeys = [], + fieldErrors = {}, +}: GenericFormProps) { + const { t } = useTranslation() + const hiddenFieldSet = new Set(hiddenKeys) + const requiredFieldSet = new Set(requiredKeys) + const groupTriggerConfig = asRecord(config.group_trigger) + const typingConfig = asRecord(config.typing) + const placeholderConfig = asRecord(config.placeholder) + const placeholderEnabled = asBool(placeholderConfig.enabled) + + const fields = Object.keys(config).filter( + (k) => + !k.startsWith("_") && + !SKIP_FIELDS.has(k) && + !OBJECT_FIELDS.has(k) && + !hiddenFieldSet.has(k), + ) + + const buildHint = (key: string): string => { + const descriptions: Record = { + ws_url: t("channels.form.desc.wsUrl"), + reconnect_interval: t("channels.form.desc.reconnectInterval"), + bridge_url: t("channels.form.desc.bridgeUrl"), + session_store_path: t("channels.form.desc.sessionStorePath"), + use_native: t("channels.form.desc.useNative"), + host: t("channels.form.desc.host"), + port: t("channels.form.desc.port"), + homeserver: t("channels.form.desc.homeserver"), + user_id: t("channels.form.desc.userId"), + device_id: t("channels.form.desc.deviceId"), + join_on_invite: t("channels.form.desc.joinOnInvite"), + app_id: t("channels.form.desc.appId"), + client_id: t("channels.form.desc.clientId"), + corp_id: t("channels.form.desc.corpId"), + agent_id: t("channels.form.desc.agentId"), + webhook_url: t("channels.form.desc.webhookUrl"), + webhook_host: t("channels.form.desc.webhookHost"), + webhook_port: t("channels.form.desc.webhookPort"), + webhook_path: t("channels.form.desc.webhookPath"), + reply_timeout: t("channels.form.desc.replyTimeout"), + max_steps: t("channels.form.desc.maxSteps"), + welcome_message: t("channels.form.desc.welcomeMessage"), + allow_token_query: t("channels.form.desc.allowTokenQuery"), + ping_interval: t("channels.form.desc.pingInterval"), + read_timeout: t("channels.form.desc.readTimeout"), + write_timeout: t("channels.form.desc.writeTimeout"), + max_connections: t("channels.form.desc.maxConnections"), + server: t("channels.form.desc.server"), + tls: t("channels.form.desc.tls"), + nick: t("channels.form.desc.nick"), + user: t("channels.form.desc.user"), + real_name: t("channels.form.desc.realName"), + channels: t("channels.form.desc.channels"), + request_caps: t("channels.form.desc.requestCaps"), + } + return ( + descriptions[key] ?? + t("channels.form.desc.genericField", { + field: formatSentenceFieldName(key), + }) + ) + } + + return ( +
+ {fields.map((key) => { + const isRequired = requiredFieldSet.has(key) + if (SECRET_FIELDS.has(key)) { + const editKey = `_${key}` + const extraHint = + isEdit && config[key] ? ` ${t("channels.field.secretHintSet")}` : "" + return ( + + onChange(editKey, v)} + placeholder={maskedSecretPlaceholder(config[key])} + /> + + ) + } + + const value = config[key] + if (typeof value === "boolean") { + return ( + onChange(key, checked)} + ariaLabel={formatLabel(key)} + /> + ) + } + + if (Array.isArray(value)) { + return ( + + + onChange( + key, + e.target.value + .split(",") + .map((s: string) => s.trim()) + .filter(Boolean), + ) + } + /> + + ) + } + + return ( + + { + // Attempt to preserve number types + const v = e.target.value + if (typeof config[key] === "number") { + onChange(key, v === "" ? 0 : Number(v)) + } else { + onChange(key, v) + } + }} + /> + + ) + })} + + {/* Allow From field */} + {config.allow_from !== undefined && !hiddenFieldSet.has("allow_from") && ( + + + onChange( + "allow_from", + e.target.value + .split(",") + .map((s: string) => s.trim()) + .filter(Boolean), + ) + } + placeholder={t("channels.field.allowFromPlaceholder")} + /> + + )} + + {config.allow_origins !== undefined && + !hiddenFieldSet.has("allow_origins") && ( + + + onChange( + "allow_origins", + e.target.value + .split(",") + .map((s: string) => s.trim()) + .filter(Boolean), + ) + } + placeholder={t("channels.field.allowOriginsPlaceholder")} + /> + + )} + + {config.allow_token_query !== undefined && + !hiddenFieldSet.has("allow_token_query") && ( + + onChange("allow_token_query", checked) + } + ariaLabel={formatLabel("allow_token_query")} + /> + )} + + {config.group_trigger !== undefined && + !hiddenFieldSet.has("group_trigger") && ( + <> + + onChange("group_trigger", { + ...groupTriggerConfig, + mention_only: checked, + }) + } + ariaLabel={t("channels.field.groupTriggerMentionOnly")} + /> + + + onChange("group_trigger", { + ...groupTriggerConfig, + prefixes: e.target.value + .split(",") + .map((s: string) => s.trim()) + .filter(Boolean), + }) + } + placeholder={t("channels.field.groupTriggerPrefixes")} + /> + + + )} + + {config.typing !== undefined && !hiddenFieldSet.has("typing") && ( + + onChange("typing", { ...typingConfig, enabled: checked }) + } + ariaLabel={t("channels.field.typingEnabled")} + /> + )} + + {config.placeholder !== undefined && + !hiddenFieldSet.has("placeholder") && ( + + onChange("placeholder", { + ...placeholderConfig, + enabled: checked, + }) + } + ariaLabel={t("channels.field.placeholderEnabled")} + > + {placeholderEnabled && ( +
+ + onChange("placeholder", { + ...placeholderConfig, + text: e.target.value, + }) + } + placeholder={t("channels.field.placeholderText")} + aria-label={t("channels.field.placeholderText")} + /> +
+ )} +
+ )} +
+ ) +} diff --git a/web/frontend/src/components/channels/channel-forms/slack-form.tsx b/web/frontend/src/components/channels/channel-forms/slack-form.tsx new file mode 100644 index 000000000..54650e842 --- /dev/null +++ b/web/frontend/src/components/channels/channel-forms/slack-form.tsx @@ -0,0 +1,86 @@ +import { useTranslation } from "react-i18next" + +import type { ChannelConfig } from "@/api/channels" +import { maskedSecretPlaceholder } from "@/components/secret-placeholder" +import { Field, KeyInput } from "@/components/shared-form" +import { Input } from "@/components/ui/input" + +interface SlackFormProps { + config: ChannelConfig + onChange: (key: string, value: unknown) => void + isEdit: boolean + fieldErrors?: Record +} + +function asString(value: unknown): string { + return typeof value === "string" ? value : "" +} + +function asStringArray(value: unknown): string[] { + if (!Array.isArray(value)) return [] + return value.filter((item): item is string => typeof item === "string") +} + +export function SlackForm({ + config, + onChange, + isEdit, + fieldErrors = {}, +}: SlackFormProps) { + const { t } = useTranslation() + const botTokenExtraHint = + isEdit && asString(config.bot_token) + ? ` ${t("channels.field.secretHintSet")}` + : "" + const appTokenExtraHint = + isEdit && asString(config.app_token) + ? ` ${t("channels.field.secretHintSet")}` + : "" + + return ( +
+ + onChange("_bot_token", v)} + placeholder={maskedSecretPlaceholder(config.bot_token, "xoxb-xxxx")} + /> + + + + onChange("_app_token", v)} + placeholder={maskedSecretPlaceholder(config.app_token, "xapp-xxxx")} + /> + + + + + onChange( + "allow_from", + e.target.value + .split(",") + .map((s: string) => s.trim()) + .filter(Boolean), + ) + } + placeholder={t("channels.field.allowFromPlaceholder")} + /> + +
+ ) +} diff --git a/web/frontend/src/components/channels/channel-forms/telegram-form.tsx b/web/frontend/src/components/channels/channel-forms/telegram-form.tsx new file mode 100644 index 000000000..169ddec63 --- /dev/null +++ b/web/frontend/src/components/channels/channel-forms/telegram-form.tsx @@ -0,0 +1,147 @@ +import { useTranslation } from "react-i18next" + +import type { ChannelConfig } from "@/api/channels" +import { maskedSecretPlaceholder } from "@/components/secret-placeholder" +import { Field, KeyInput, SwitchCardField } from "@/components/shared-form" +import { Input } from "@/components/ui/input" + +interface TelegramFormProps { + config: ChannelConfig + onChange: (key: string, value: unknown) => void + isEdit: boolean + fieldErrors?: Record +} + +function asString(value: unknown): string { + return typeof value === "string" ? value : "" +} + +function asStringArray(value: unknown): string[] { + if (!Array.isArray(value)) return [] + return value.filter((item): item is string => typeof item === "string") +} + +function asRecord(value: unknown): Record { + if (value && typeof value === "object" && !Array.isArray(value)) { + return value as Record + } + return {} +} + +function asBool(value: unknown): boolean { + return value === true +} + +export function TelegramForm({ + config, + onChange, + isEdit, + fieldErrors = {}, +}: TelegramFormProps) { + const { t } = useTranslation() + const typingConfig = asRecord(config.typing) + const placeholderConfig = asRecord(config.placeholder) + const placeholderEnabled = asBool(placeholderConfig.enabled) + const tokenExtraHint = + isEdit && asString(config.token) + ? ` ${t("channels.field.secretHintSet")}` + : "" + + return ( +
+ + onChange("_token", v)} + placeholder={maskedSecretPlaceholder( + config.token, + t("channels.field.tokenPlaceholder"), + )} + /> + + + + onChange("base_url", e.target.value)} + placeholder="https://api.telegram.org" + /> + + + onChange("proxy", e.target.value)} + placeholder="http://127.0.0.1:7890" + /> + + + + onChange( + "allow_from", + e.target.value + .split(",") + .map((s: string) => s.trim()) + .filter(Boolean), + ) + } + placeholder={t("channels.field.allowFromPlaceholder")} + /> + + + + onChange("typing", { ...typingConfig, enabled: checked }) + } + ariaLabel={t("channels.field.typingEnabled")} + /> + + + onChange("placeholder", { + ...placeholderConfig, + enabled: checked, + }) + } + ariaLabel={t("channels.field.placeholderEnabled")} + > + {placeholderEnabled && ( +
+ + onChange("placeholder", { + ...placeholderConfig, + text: e.target.value, + }) + } + placeholder={t("channels.field.placeholderText")} + aria-label={t("channels.field.placeholderText")} + /> +
+ )} +
+
+ ) +} diff --git a/web/frontend/src/components/chat/assistant-message.tsx b/web/frontend/src/components/chat/assistant-message.tsx new file mode 100644 index 000000000..150f2f87d --- /dev/null +++ b/web/frontend/src/components/chat/assistant-message.tsx @@ -0,0 +1,62 @@ +import { IconCheck, IconCopy } from "@tabler/icons-react" +import { useState } from "react" +import ReactMarkdown from "react-markdown" +import remarkGfm from "remark-gfm" + +import { Button } from "@/components/ui/button" +import { formatMessageTime } from "@/hooks/use-pico-chat" + +interface AssistantMessageProps { + content: string + timestamp?: string | number +} + +export function AssistantMessage({ + content, + timestamp = "", +}: AssistantMessageProps) { + const [isCopied, setIsCopied] = useState(false) + const formattedTimestamp = + timestamp !== "" ? formatMessageTime(timestamp) : "" + + const handleCopy = () => { + navigator.clipboard.writeText(content).then(() => { + setIsCopied(true) + setTimeout(() => setIsCopied(false), 2000) + }) + } + + return ( +
+
+
+ PicoClaw + {formattedTimestamp && ( + <> + + {formattedTimestamp} + + )} +
+
+ +
+
+ {content} +
+ +
+
+ ) +} diff --git a/web/frontend/src/components/chat/chat-composer.tsx b/web/frontend/src/components/chat/chat-composer.tsx new file mode 100644 index 000000000..e8bae89b8 --- /dev/null +++ b/web/frontend/src/components/chat/chat-composer.tsx @@ -0,0 +1,67 @@ +import { IconArrowUp } from "@tabler/icons-react" +import type { KeyboardEvent } from "react" +import { useTranslation } from "react-i18next" +import TextareaAutosize from "react-textarea-autosize" + +import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" + +interface ChatComposerProps { + input: string + onInputChange: (value: string) => void + onSend: () => void + isConnected: boolean + hasDefaultModel: boolean +} + +export function ChatComposer({ + input, + onInputChange, + onSend, + isConnected, + hasDefaultModel, +}: ChatComposerProps) { + const { t } = useTranslation() + const canInput = isConnected && hasDefaultModel + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.nativeEvent.isComposing) return + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault() + onSend() + } + } + + return ( +
+
+ onInputChange(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={t("chat.placeholder")} + disabled={!canInput} + className={cn( + "max-h-[200px] min-h-[60px] resize-none border-0 bg-transparent px-2 py-1 text-[15px] shadow-none transition-colors focus-visible:ring-0 focus-visible:outline-none dark:bg-transparent", + !canInput && "cursor-not-allowed", + )} + minRows={1} + maxRows={8} + /> + +
+
{/* action buttons */}
+ + +
+
+
+ ) +} diff --git a/web/frontend/src/components/chat/chat-empty-state.tsx b/web/frontend/src/components/chat/chat-empty-state.tsx new file mode 100644 index 000000000..624ff9c59 --- /dev/null +++ b/web/frontend/src/components/chat/chat-empty-state.tsx @@ -0,0 +1,87 @@ +import { + IconPlugConnectedX, + IconRobot, + IconRobotOff, + IconStar, +} from "@tabler/icons-react" +import { Link } from "@tanstack/react-router" +import { useTranslation } from "react-i18next" + +import { Button } from "@/components/ui/button" + +interface ChatEmptyStateProps { + hasConfiguredModels: boolean + defaultModelName: string + isConnected: boolean +} + +export function ChatEmptyState({ + hasConfiguredModels, + defaultModelName, + isConnected, +}: ChatEmptyStateProps) { + const { t } = useTranslation() + + if (!hasConfiguredModels) { + return ( +
+
+ +
+

+ {t("chat.empty.noConfiguredModel")} +

+

+ {t("chat.empty.noConfiguredModelDescription")} +

+ +
+ ) + } + + if (!defaultModelName) { + return ( +
+
+ +
+

+ {t("chat.empty.noSelectedModel")} +

+

+ {t("chat.empty.noSelectedModelDescription")} +

+
+ ) + } + + if (!isConnected) { + return ( +
+
+ +
+

+ {t("chat.empty.notRunning")} +

+

+ {t("chat.empty.notRunningDescription")} +

+
+ ) + } + + return ( +
+
+ +
+

{t("chat.welcome")}

+

+ {t("chat.welcomeDesc")} +

+
+ ) +} diff --git a/web/frontend/src/components/chat/chat-page.tsx b/web/frontend/src/components/chat/chat-page.tsx new file mode 100644 index 000000000..0fd23a6a5 --- /dev/null +++ b/web/frontend/src/components/chat/chat-page.tsx @@ -0,0 +1,150 @@ +import { IconPlus } from "@tabler/icons-react" +import { useEffect, useRef, useState } from "react" +import { useTranslation } from "react-i18next" + +import { AssistantMessage } from "@/components/chat/assistant-message" +import { ChatComposer } from "@/components/chat/chat-composer" +import { ChatEmptyState } from "@/components/chat/chat-empty-state" +import { ModelSelector } from "@/components/chat/model-selector" +import { SessionHistoryMenu } from "@/components/chat/session-history-menu" +import { TypingIndicator } from "@/components/chat/typing-indicator" +import { UserMessage } from "@/components/chat/user-message" +import { PageHeader } from "@/components/page-header" +import { Button } from "@/components/ui/button" +import { useChatModels } from "@/hooks/use-chat-models" +import { useGateway } from "@/hooks/use-gateway" +import { usePicoChat } from "@/hooks/use-pico-chat" +import { useSessionHistory } from "@/hooks/use-session-history" + +export function ChatPage() { + const { t } = useTranslation() + const scrollRef = useRef(null) + const [isAtBottom, setIsAtBottom] = useState(true) + const [input, setInput] = useState("") + + const { + messages, + isTyping, + activeSessionId, + sendMessage, + switchSession, + newChat, + } = usePicoChat() + + const { state: gwState } = useGateway() + const isConnected = gwState === "running" + + const { + defaultModelName, + hasConfiguredModels, + apiKeyModels, + oauthModels, + localModels, + handleSetDefault, + } = useChatModels({ isConnected }) + + const { sessions, hasMore, observerRef, loadSessions, handleDeleteSession } = + useSessionHistory({ + activeSessionId, + onDeletedActiveSession: newChat, + }) + + const handleScroll = (e: React.UIEvent) => { + const { scrollTop, scrollHeight, clientHeight } = e.currentTarget + setIsAtBottom(scrollHeight - scrollTop <= clientHeight + 10) + } + + useEffect(() => { + if (isAtBottom && scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight + } + }, [messages, isTyping, isAtBottom]) + + const handleSend = () => { + if (!input.trim() || !isConnected) return + sendMessage(input.trim()) + setInput("") + } + + return ( +
+ + ) + } + > + + + { + if (open) { + void loadSessions(true) + } + }} + onSwitchSession={switchSession} + onDeleteSession={handleDeleteSession} + /> + + +
+
+ {messages.length === 0 && !isTyping && ( + + )} + + {messages.map((msg) => ( +
+ {msg.role === "assistant" ? ( + + ) : ( + + )} +
+ ))} + + {isTyping && } +
+
+ + +
+ ) +} diff --git a/web/frontend/src/components/chat/model-selector.tsx b/web/frontend/src/components/chat/model-selector.tsx new file mode 100644 index 000000000..30afc5d04 --- /dev/null +++ b/web/frontend/src/components/chat/model-selector.tsx @@ -0,0 +1,84 @@ +import { useTranslation } from "react-i18next" + +import type { ModelInfo } from "@/api/models" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectSeparator, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" + +interface ModelSelectorProps { + defaultModelName: string + apiKeyModels: ModelInfo[] + oauthModels: ModelInfo[] + localModels: ModelInfo[] + onValueChange: (modelName: string) => void +} + +export function ModelSelector({ + defaultModelName, + apiKeyModels, + oauthModels, + localModels, + onValueChange, +}: ModelSelectorProps) { + const { t } = useTranslation() + + return ( + + ) +} diff --git a/web/frontend/src/components/chat/session-history-menu.tsx b/web/frontend/src/components/chat/session-history-menu.tsx new file mode 100644 index 000000000..f2e93295c --- /dev/null +++ b/web/frontend/src/components/chat/session-history-menu.tsx @@ -0,0 +1,98 @@ +import { IconHistory, IconTrash } from "@tabler/icons-react" +import dayjs from "dayjs" +import type { RefObject } from "react" +import { useTranslation } from "react-i18next" + +import type { SessionSummary } from "@/api/sessions" +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { ScrollArea } from "@/components/ui/scroll-area" + +interface SessionHistoryMenuProps { + sessions: SessionSummary[] + activeSessionId: string + hasMore: boolean + observerRef: RefObject + onOpenChange: (open: boolean) => void + onSwitchSession: (sessionId: string) => void + onDeleteSession: (sessionId: string) => void +} + +export function SessionHistoryMenu({ + sessions, + activeSessionId, + hasMore, + observerRef, + onOpenChange, + onSwitchSession, + onDeleteSession, +}: SessionHistoryMenuProps) { + const { t } = useTranslation() + + return ( + + + + + + + {sessions.length === 0 ? ( + + + {t("chat.noHistory")} + + + ) : ( + sessions.map((session) => ( + onSwitchSession(session.id)} + > + + {session.preview} + + + {t("chat.messagesCount", { + count: session.message_count, + })}{" "} + · {dayjs(session.updated).fromNow()} + + + + )) + )} + {hasMore && sessions.length > 0 && ( +
+ + {t("chat.loadingMore")} + +
+ )} +
+
+
+ ) +} diff --git a/web/frontend/src/components/chat/typing-indicator.tsx b/web/frontend/src/components/chat/typing-indicator.tsx new file mode 100644 index 000000000..98580963d --- /dev/null +++ b/web/frontend/src/components/chat/typing-indicator.tsx @@ -0,0 +1,47 @@ +import { useEffect, useState } from "react" +import { useTranslation } from "react-i18next" + +export function TypingIndicator() { + const { t } = useTranslation() + const thinkingSteps = [ + t("chat.thinking.step1"), + t("chat.thinking.step2"), + t("chat.thinking.step3"), + t("chat.thinking.step4"), + ] + const [stepIndex, setStepIndex] = useState(0) + + useEffect(() => { + const stepsCount = thinkingSteps.length + const interval = setInterval(() => { + setStepIndex((prev) => (prev + 1) % stepsCount) + }, 3000) + return () => clearInterval(interval) + }, [thinkingSteps.length]) + + return ( +
+
+ PicoClaw +
+
+
+ + + +
+ +
+
+
+ +

+ {thinkingSteps[stepIndex]} +

+
+
+ ) +} diff --git a/web/frontend/src/components/chat/user-message.tsx b/web/frontend/src/components/chat/user-message.tsx new file mode 100644 index 000000000..b47806f49 --- /dev/null +++ b/web/frontend/src/components/chat/user-message.tsx @@ -0,0 +1,13 @@ +interface UserMessageProps { + content: string +} + +export function UserMessage({ content }: UserMessageProps) { + return ( +
+
+ {content} +
+
+ ) +} diff --git a/web/frontend/src/components/config/config-page.tsx b/web/frontend/src/components/config/config-page.tsx new file mode 100644 index 000000000..c2d502079 --- /dev/null +++ b/web/frontend/src/components/config/config-page.tsx @@ -0,0 +1,337 @@ +import { IconCode, IconDeviceFloppy } from "@tabler/icons-react" +import { useQuery, useQueryClient } from "@tanstack/react-query" +import { Link } from "@tanstack/react-router" +import { useEffect, useState } from "react" +import { useTranslation } from "react-i18next" +import { toast } from "sonner" + +import { patchAppConfig } from "@/api/channels" +import { + getAutoStartStatus, + getLauncherConfig, + setAutoStartEnabled as updateAutoStartEnabled, + setLauncherConfig as updateLauncherConfig, +} from "@/api/system" +import { + AdvancedSection, + AgentDefaultsSection, + DevicesSection, + LauncherSection, + RuntimeSection, +} from "@/components/config/config-sections" +import { + type CoreConfigForm, + EMPTY_FORM, + EMPTY_LAUNCHER_FORM, + type LauncherForm, + buildFormFromConfig, + parseCIDRText, + parseIntField, +} from "@/components/config/form-model" +import { PageHeader } from "@/components/page-header" +import { Button } from "@/components/ui/button" +import { Separator } from "@/components/ui/separator" + +export function ConfigPage() { + const { t } = useTranslation() + const queryClient = useQueryClient() + const [form, setForm] = useState(EMPTY_FORM) + const [baseline, setBaseline] = useState(EMPTY_FORM) + const [launcherForm, setLauncherForm] = + useState(EMPTY_LAUNCHER_FORM) + const [launcherBaseline, setLauncherBaseline] = + useState(EMPTY_LAUNCHER_FORM) + const [autoStartEnabled, setAutoStartEnabled] = useState(false) + const [autoStartBaseline, setAutoStartBaseline] = useState(false) + const [saving, setSaving] = useState(false) + + const { data, isLoading, error } = useQuery({ + queryKey: ["config"], + queryFn: async () => { + const res = await fetch("/api/config") + if (!res.ok) { + throw new Error("Failed to load config") + } + return res.json() + }, + }) + + const { + data: launcherConfig, + isLoading: isLauncherLoading, + error: launcherError, + } = useQuery({ + queryKey: ["system", "launcher-config"], + queryFn: getLauncherConfig, + }) + + const { + data: autoStartStatus, + isLoading: isAutoStartLoading, + error: autoStartError, + } = useQuery({ + queryKey: ["system", "autostart"], + queryFn: getAutoStartStatus, + }) + + useEffect(() => { + if (!data) return + const parsed = buildFormFromConfig(data) + setForm(parsed) + setBaseline(parsed) + }, [data]) + + useEffect(() => { + if (!launcherConfig) return + const parsed: LauncherForm = { + port: String(launcherConfig.port), + publicAccess: launcherConfig.public, + allowedCIDRsText: (launcherConfig.allowed_cidrs ?? []).join("\n"), + } + setLauncherForm(parsed) + setLauncherBaseline(parsed) + }, [launcherConfig]) + + useEffect(() => { + if (!autoStartStatus) return + setAutoStartEnabled(autoStartStatus.enabled) + setAutoStartBaseline(autoStartStatus.enabled) + }, [autoStartStatus]) + + const configDirty = JSON.stringify(form) !== JSON.stringify(baseline) + const launcherDirty = + JSON.stringify(launcherForm) !== JSON.stringify(launcherBaseline) + const autoStartDirty = autoStartEnabled !== autoStartBaseline + const isDirty = configDirty || launcherDirty || autoStartDirty + + const autoStartSupported = autoStartStatus?.supported !== false + const autoStartHint = autoStartError + ? t("pages.config.autostart_load_error") + : !autoStartSupported + ? t("pages.config.autostart_unsupported") + : t("pages.config.autostart_hint") + + const launcherHint = launcherError + ? t("pages.config.launcher_load_error") + : t("pages.config.launcher_restart_hint") + + const updateField = ( + key: K, + value: CoreConfigForm[K], + ) => { + setForm((prev) => ({ ...prev, [key]: value })) + } + + const updateLauncherField = ( + key: K, + value: LauncherForm[K], + ) => { + setLauncherForm((prev) => ({ ...prev, [key]: value })) + } + + const handleReset = () => { + setForm(baseline) + setLauncherForm(launcherBaseline) + setAutoStartEnabled(autoStartBaseline) + toast.info(t("pages.config.reset_success")) + } + + const handleSave = async () => { + try { + setSaving(true) + + if (configDirty) { + const workspace = form.workspace.trim() + const dmScope = form.dmScope.trim() + + if (!workspace) { + throw new Error("Workspace path is required.") + } + if (!dmScope) { + throw new Error("Session scope is required.") + } + + const maxTokens = parseIntField(form.maxTokens, "Max tokens", { + min: 1, + }) + const maxToolIterations = parseIntField( + form.maxToolIterations, + "Max tool iterations", + { min: 1 }, + ) + const summarizeMessageThreshold = parseIntField( + form.summarizeMessageThreshold, + "Summarize message threshold", + { min: 1 }, + ) + const summarizeTokenPercent = parseIntField( + form.summarizeTokenPercent, + "Summarize token percent", + { min: 1, max: 100 }, + ) + const heartbeatInterval = parseIntField( + form.heartbeatInterval, + "Heartbeat interval", + { min: 1 }, + ) + + await patchAppConfig({ + agents: { + defaults: { + workspace, + restrict_to_workspace: form.restrictToWorkspace, + max_tokens: maxTokens, + max_tool_iterations: maxToolIterations, + summarize_message_threshold: summarizeMessageThreshold, + summarize_token_percent: summarizeTokenPercent, + }, + }, + session: { + dm_scope: dmScope, + }, + heartbeat: { + enabled: form.heartbeatEnabled, + interval: heartbeatInterval, + }, + devices: { + enabled: form.devicesEnabled, + monitor_usb: form.monitorUSB, + }, + }) + + setBaseline(form) + queryClient.invalidateQueries({ queryKey: ["config"] }) + } + + if (launcherDirty) { + const port = parseIntField(launcherForm.port, "Service port", { + min: 1, + max: 65535, + }) + const allowedCIDRs = parseCIDRText(launcherForm.allowedCIDRsText) + const savedLauncherConfig = await updateLauncherConfig({ + port, + public: launcherForm.publicAccess, + allowed_cidrs: allowedCIDRs, + }) + const parsedLauncher: LauncherForm = { + port: String(savedLauncherConfig.port), + publicAccess: savedLauncherConfig.public, + allowedCIDRsText: (savedLauncherConfig.allowed_cidrs ?? []).join( + "\n", + ), + } + setLauncherForm(parsedLauncher) + setLauncherBaseline(parsedLauncher) + queryClient.setQueryData( + ["system", "launcher-config"], + savedLauncherConfig, + ) + } + + if (autoStartDirty) { + if (!autoStartSupported) { + throw new Error(t("pages.config.autostart_unsupported")) + } + const status = await updateAutoStartEnabled(autoStartEnabled) + setAutoStartEnabled(status.enabled) + setAutoStartBaseline(status.enabled) + queryClient.setQueryData(["system", "autostart"], status) + } + + toast.success(t("pages.config.save_success")) + } catch (err) { + toast.error( + err instanceof Error ? err.message : t("pages.config.save_error"), + ) + } finally { + setSaving(false) + } + } + + return ( +
+ + + + {t("pages.config.open_raw")} + + + } + /> +
+
+ {isLoading ? ( +
+ {t("labels.loading")} +
+ ) : error ? ( +
+ {t("pages.config.load_error")} +
+ ) : ( +
+ {isDirty && ( +
+ {t("pages.config.unsaved_changes")} +
+ )} + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ )} +
+
+
+ ) +} diff --git a/web/frontend/src/components/config/config-sections.tsx b/web/frontend/src/components/config/config-sections.tsx new file mode 100644 index 000000000..340ece333 --- /dev/null +++ b/web/frontend/src/components/config/config-sections.tsx @@ -0,0 +1,326 @@ +import { IconCode } from "@tabler/icons-react" +import { Link } from "@tanstack/react-router" +import { useTranslation } from "react-i18next" + +import { + type CoreConfigForm, + DM_SCOPE_OPTIONS, + type LauncherForm, +} from "@/components/config/form-model" +import { Field, SwitchCardField } from "@/components/shared-form" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Textarea } from "@/components/ui/textarea" + +type UpdateCoreField = ( + key: K, + value: CoreConfigForm[K], +) => void + +type UpdateLauncherField = ( + key: K, + value: LauncherForm[K], +) => void + +interface AgentDefaultsSectionProps { + form: CoreConfigForm + onFieldChange: UpdateCoreField +} + +export function AgentDefaultsSection({ + form, + onFieldChange, +}: AgentDefaultsSectionProps) { + const { t } = useTranslation() + + return ( +
+
+ + onFieldChange("workspace", e.target.value)} + placeholder="~/.picoclaw/workspace" + /> + + + + onFieldChange("restrictToWorkspace", checked) + } + /> + + + onFieldChange("maxTokens", e.target.value)} + /> + + + + onFieldChange("maxToolIterations", e.target.value)} + /> + + + + + onFieldChange("summarizeMessageThreshold", e.target.value) + } + /> + + + + + onFieldChange("summarizeTokenPercent", e.target.value) + } + /> + +
+
+ ) +} + +interface RuntimeSectionProps { + form: CoreConfigForm + onFieldChange: UpdateCoreField +} + +export function RuntimeSection({ form, onFieldChange }: RuntimeSectionProps) { + const { t } = useTranslation() + const selectedDmScopeOption = DM_SCOPE_OPTIONS.find( + (scope) => scope.value === form.dmScope, + ) + + return ( +
+
+ + + + + + onFieldChange("heartbeatEnabled", checked) + } + /> + + {form.heartbeatEnabled && ( + + + onFieldChange("heartbeatInterval", e.target.value) + } + /> + + )} +
+
+ ) +} + +interface LauncherSectionProps { + launcherForm: LauncherForm + onFieldChange: UpdateLauncherField + launcherHint: string + disabled: boolean +} + +export function LauncherSection({ + launcherForm, + onFieldChange, + launcherHint, + disabled, +}: LauncherSectionProps) { + const { t } = useTranslation() + + return ( +
+
+ + onFieldChange("port", e.target.value)} + /> + + + onFieldChange("publicAccess", checked)} + /> + + +