diff --git a/.eslintrc.json b/.eslintrc.json index ccf7009c..e85261db 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -20,7 +20,9 @@ "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-unused-vars": [ "error", - { "argsIgnorePattern": "^_" } + { + "argsIgnorePattern": "^_" + } ] }, "overrides": [ @@ -30,6 +32,15 @@ "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-explicit-any": "off" } + }, + { + "files": ["web/**/*.ts", "web/**/*.tsx"], + "env": { + "browser": true + }, + "parserOptions": { + "project": "./web/tsconfig.json" + } } ] } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 676dc822..0f445b21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,8 +15,6 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 - with: - version: 10 - name: Use Node.js uses: actions/setup-node@v3 @@ -25,10 +23,29 @@ jobs: cache: 'pnpm' - name: Install dependencies - run: pnpm install + run: pnpm install --frozen-lockfile - name: Build run: pnpm run build - name: Test - run: pnpm test \ No newline at end of file + run: pnpm test + + - name: Lint + run: pnpm lint + + - name: Install browser dependencies + run: npx --yes agent-browser@0.38.1 install --with-deps + + - name: Browser regressions + run: | + node scripts/preview-usage-ui.mjs & + preview_pid=$! + trap 'kill "$preview_pid"' EXIT + ready=false + for attempt in $(seq 1 30); do + if curl --fail --silent http://127.0.0.1:4173/ > /dev/null; then ready=true; break; fi + sleep 1 + done + if [ "$ready" != true ]; then echo "Preview server did not start" >&2; exit 1; fi + node scripts/verify-usage-preview.mjs http://127.0.0.1:4173 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7b2a5348..0e4b7ece 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,8 +21,6 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 - with: - version: 10 - name: Set up Node.js uses: actions/setup-node@v6 diff --git a/README.md b/README.md index 5bf08075..58a478bd 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,10 @@ A Model Context Protocol (MCP) server that brings [Firecrawl](https://github.com - Use `firecrawl_interact` when a page needs a click, type, or navigate action before you can read it — pass a `url` for a fresh page or a `scrapeId` to continue on one you already scraped. - Use the `firecrawl_monitor_*` tools when the same page needs to be checked on a recurring schedule with diffs and change alerts, rather than fetched once. - Use `firecrawl_credit_usage` to check credits left or monthly consumption, optionally broken down by API key. +- Use `firecrawl_usage_dashboard` to open the usage interface in an MCP Apps host; ChatGPT can also expose it as a global sidebar entrypoint. - Consider something else when you need to hold a browser session open across many of your own steps with your own retry and termination logic: each `firecrawl_interact` call runs one `prompt` or `code` turn to completion and returns control — the session can persist across calls via `scrapeId` and ends with `firecrawl_interact_stop`, but you cannot drive it interactively step-by-step from the client side within a single call. -This server lists 26 tools when the full profile registers with default settings (feedback tools included, not running in local-keyless mode). Setting `FIRECRAWL_NO_SEARCH_FEEDBACK=1` and/or `FIRECRAWL_NO_ENDPOINT_FEEDBACK=1` removes the corresponding feedback tools and reduces this count, as does local keyless startup. For clients with a tool-slot limit: the hosted keyless endpoint (`https://mcp.firecrawl.dev/v2/mcp`, no API key) exposes only 3 — `firecrawl_scrape`, `firecrawl_search`, `firecrawl_parse` — and the dedicated [search-only endpoint](#search-only-endpoint) (`https://mcp.firecrawl.dev/v2/mcp-search`) exposes a fixed set of 8 tools (search, developer and research search, plus Alexandria catalogue lookup and execution). +This server lists 27 tools when the full profile registers with default settings (feedback tools included, not running in local-keyless mode). Setting `FIRECRAWL_NO_SEARCH_FEEDBACK=1` and/or `FIRECRAWL_NO_ENDPOINT_FEEDBACK=1` removes the corresponding feedback tools and reduces this count, as does local keyless startup. For clients with a tool-slot limit: the hosted keyless endpoint (`https://mcp.firecrawl.dev/v2/mcp`, no API key) exposes only 3 — `firecrawl_scrape`, `firecrawl_search`, `firecrawl_parse` — and the dedicated [search-only endpoint](#search-only-endpoint) (`https://mcp.firecrawl.dev/v2/mcp-search`) exposes a fixed set of 8 tools (search, developer and research search, plus Alexandria catalogue lookup and execution). ## Installation @@ -1293,3 +1294,36 @@ The existing `firecrawl_feedback` tool accepts `endpoint: "alexandria"`: This uses authenticated `POST /v2/feedback`, without a job ID, job-age deadline, or credit refund. Optional `providerFeedback` and `capabilityFeedback` arrays describe coverage gaps and execution issues; the tool schema lists supported issue values. A `new_capability_request` requires `requestedFunctionality`; `missing_capability` (the provider exists but lacks the capability) does not. Existing feedback opt-out and authentication controls apply. Eligible Alexandria execution and discovery results include a `feedbackTool` pointer with the tool name and a skeleton of the arguments. The pointer is omitted for Firecrawl-internal calls such as `bash` and when `firecrawl_feedback` is not registered (`FIRECRAWL_NO_ENDPOINT_FEEDBACK` or keyless startup). + +### Usage interface + +The full and account profiles expose `firecrawl_usage_dashboard`, with a global +ChatGPT sidebar entrypoint and the `ui://firecrawl/usage.html` MCP Apps resource. +The search-only profile and hosted anonymous keyless tool list do not include it. +The app-only launcher opens fullscreen. The interface uses the Firecrawl web design system, including Suisse fonts and +light/dark tokens, and refreshes balance and monthly usage through the existing +`firecrawl_credit_usage` tool. Its Providers tab browses the complete live +Alexandria catalog with collection tabs, category shelves and bundled provider +logos. It searches and filters providers, and supports whole-provider +or individual-tool selections. Selecting attaches the tool context to the native +chat composer using MCP Apps model-context support. Type your request in the +chat composer; the app does not send a message or provide a chatbox. Discovery +is free; capability execution happens through the chat's Firecrawl tools under +existing account access and pricing. Selections guide the chat without enforcing +an exclusive provider allowlist. It never handles API keys in the browser. + +`pnpm build` embeds the SDK, styles, and fonts into `dist/usage.html`; no external +asset host is needed. Run `pnpm typecheck:ui` for the browser source, and +`pnpm preview:usage` for a local mock-host preview with explicitly labeled sample +data. This preview does not require credentials or call the Firecrawl API. + +For the desktop plugin sidebar flow, build and run +`node scripts/install-usage-plugin.mjs` on macOS. This installs **Firecrawl Usage +Dev** from a dedicated local marketplace with a Keychain-backed MCP server. +Restart the desktop app, open its Plugins entry, open the plugin app, and choose +**Pin to sidebar**. See the [local plugin setup](plugins/openai/firecrawl-usage-dev/README.md). + +To enable the interface in ChatGPT, deploy this server build behind the registered +Firecrawl account connection and refresh the connection's tools/resources in +developer mode. The existing OpenAI package keeps its registered app reference; +this change does not publish a plugin or change marketplace/submission settings. diff --git a/package.json b/package.json index 372bca4c..51eed151 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "3.27.2", "description": "MCP server for Firecrawl — search, scrape, and interact with the web, and search scientific papers. Supports both cloud and self-hosted instances. Features include web search, scraping, page interaction, batch processing, LLM-powered content analysis, and research paper search over biomedical and arXiv literature (PubMed, bioRxiv, medRxiv, arXiv) with citation-graph expansion and full-text reading.", "type": "module", + "packageManager": "pnpm@10.17.1", "mcpName": "io.github.firecrawl/firecrawl-mcp-server", "bin": { "firecrawl-mcp": "dist/index.js" @@ -14,16 +15,19 @@ "access": "public" }, "scripts": { - "build": "rm -rf dist && tsup && node -e \"require('fs').chmodSync('dist/index.js', '755')\"", - "test": "npm run build && node --test --test-concurrency=1 tests/*.test.mjs", + "build": "rm -rf dist && tsup && node scripts/build-usage-ui.mjs && node -e \"require('fs').chmodSync('dist/index.js', '755')\"", + "test": "npm run build && npm run typecheck:ui && node --test --test-concurrency=1 tests/*.test.mjs", "start": "node dist/index.js", "start:cloud": "CLOUD_SERVICE=true node dist/index.js", - "lint": "eslint src/**/*.ts", - "lint:fix": "eslint src/**/*.ts --fix", + "lint": "eslint src/**/*.ts \"web/**/*.{ts,tsx}\"", + "lint:fix": "eslint src/**/*.ts \"web/**/*.{ts,tsx}\" --fix", "format": "prettier --write .", "prepare": "npm run build", "publish-prod": "npm run build && npm publish", - "publish-beta": "npm run build && npm publish --tag beta" + "publish-beta": "npm run build && npm publish --tag beta", + "typecheck:ui": "tsc -p web/tsconfig.json", + "preview:usage": "npm run build && node scripts/preview-usage-ui.mjs", + "plugin:prepare:openai": "node scripts/prepare-openai-plugin.mjs" }, "license": "MIT", "dependencies": { @@ -65,11 +69,27 @@ }, "homepage": "https://github.com/firecrawl/firecrawl-mcp-server#readme", "devDependencies": { + "@modelcontextprotocol/ext-apps": "^2.0.3", + "@radix-ui/react-dialog": "1.1.6", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-slot": "1.1.2", + "@tailwindcss/typography": "0.5.19", "@types/node": "^24.3.1", + "@types/react": "19.2.8", + "@types/react-dom": "19.2.3", "@typescript-eslint/eslint-plugin": "8.48.1", "@typescript-eslint/parser": "8.48.1", + "classnames": "2.5.1", + "esbuild": "^0.27.7", "eslint": "8.57.1", "eslint-config-prettier": "10.1.8", + "lucide-react": "0.539.0", + "motion": "12.20.2", + "postcss": "8.5.6", + "react": "19.2.3", + "react-dom": "19.2.3", + "tailwind-gradient-mask-image": "1.2.0", + "tailwindcss": "3.4.17", "tsup": "^8.5.0", "typescript": "^5.9.2" } diff --git a/patches/fastmcp@4.3.2.patch b/patches/fastmcp@4.3.2.patch index 91de9390..d9d50da7 100644 --- a/patches/fastmcp@4.3.2.patch +++ b/patches/fastmcp@4.3.2.patch @@ -1,31 +1,35 @@ diff --git a/dist/FastMCP.d.cts b/dist/FastMCP.d.cts -index 8eba099cc20f7ae5f70060bebb3871c387cfb2de..c3031da662933c366c7320171db47c246cd3191a 100644 +index 8eba099cc20f7ae5f70060bebb3871c387cfb2de..bd4417c78cf53b8f3b8e6152fd940eca4feabf7f 100644 --- a/dist/FastMCP.d.cts +++ b/dist/FastMCP.d.cts -@@ -605,6 +605,8 @@ type Tool boolean; + beforeValidate?: (args: unknown, auth: T) => ContentResult | Promise | undefined; + canList?: (auth: T) => boolean; description?: string; ++ icons?: { src: string; mimeType?: string; sizes?: string[]; theme?: "light" | "dark" }[]; execute: (args: StandardSchemaV1.InferOutput, context: Context) => Promise | string | TextContent | void>; name: string; + outputSchema?: OutputParams; diff --git a/dist/FastMCP.d.ts b/dist/FastMCP.d.ts -index 810df0c5b6511f34685a0651e6df83acd0239e7b..5c82680c32900c1ca39679e1be25e64c3408aee9 100644 +index 810df0c5b6511f34685a0651e6df83acd0239e7b..519e4bbba760ff914bc8d451995b1cd1cb21a5b4 100644 --- a/dist/FastMCP.d.ts +++ b/dist/FastMCP.d.ts -@@ -605,6 +605,8 @@ type Tool boolean; + beforeValidate?: (args: unknown, auth: T) => ContentResult | Promise | undefined; + canList?: (auth: T) => boolean; description?: string; ++ icons?: { src: string; mimeType?: string; sizes?: string[]; theme?: "light" | "dark" }[]; execute: (args: StandardSchemaV1.InferOutput, context: Context) => Promise | string | TextContent | void>; name: string; + outputSchema?: OutputParams; diff --git a/dist/chunk-LWU5CQGW.js b/dist/chunk-LWU5CQGW.js -index 474670585c1fff7d9609d0f900d0743df14a7688..86d120ca21d74d6c335a8c3c9763e2f4ac835d10 100644 +index 474670585c1fff7d9609d0f900d0743df14a7688..8f3b93f6fc8eb9de279d2bd7a8e98d95075facdb 100644 --- a/dist/chunk-LWU5CQGW.js +++ b/dist/chunk-LWU5CQGW.js @@ -986,6 +986,9 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}` @@ -38,7 +42,7 @@ index 474670585c1fff7d9609d0f900d0743df14a7688..86d120ca21d74d6c335a8c3c9763e2f4 let cachedToolsList = null; this.#server.setRequestHandler(ListToolsRequestSchema, async () => { if (cachedToolsList) { -@@ -994,9 +997,12 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}` +@@ -994,9 +997,13 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}` }; } cachedToolsList = await Promise.all( @@ -49,10 +53,11 @@ index 474670585c1fff7d9609d0f900d0743df14a7688..86d120ca21d74d6c335a8c3c9763e2f4 + // Top-level Tool.title (MCP 2025-06-18). Clients such as Codex index this + // for tool search and do not read annotations.title. + ...tool.annotations?.title && { title: tool.annotations.title }, ++ ...tool.icons && { icons: tool.icons }, description: tool.description, inputSchema: tool.parameters ? strictJsonSchema(await toJsonSchema(tool.parameters)) : { additionalProperties: false, -@@ -1026,6 +1032,15 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}` +@@ -1026,6 +1033,15 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}` `Unknown tool: ${request.params.name}` ); } @@ -69,7 +74,7 @@ index 474670585c1fff7d9609d0f900d0743df14a7688..86d120ca21d74d6c335a8c3c9763e2f4 if (tool.parameters) { const parsed = await tool.parameters["~standard"].validate( diff --git a/dist/chunk-UYG7NPM6.cjs b/dist/chunk-UYG7NPM6.cjs -index 3b695bf493c4f54fd970e145cf14fdd8effc9c6d..3e17b1d1f71f3bf41eb5e589fa272e670f27c150 100644 +index 3b695bf493c4f54fd970e145cf14fdd8effc9c6d..1141ef0c4a9323709200bd166e7a678e2d493c44 100644 --- a/dist/chunk-UYG7NPM6.cjs +++ b/dist/chunk-UYG7NPM6.cjs @@ -986,6 +986,9 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}` @@ -82,7 +87,7 @@ index 3b695bf493c4f54fd970e145cf14fdd8effc9c6d..3e17b1d1f71f3bf41eb5e589fa272e67 let cachedToolsList = null; this.#server.setRequestHandler(_typesjs.ListToolsRequestSchema, async () => { if (cachedToolsList) { -@@ -994,9 +997,12 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}` +@@ -994,9 +997,13 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}` }; } cachedToolsList = await Promise.all( @@ -93,10 +98,11 @@ index 3b695bf493c4f54fd970e145cf14fdd8effc9c6d..3e17b1d1f71f3bf41eb5e589fa272e67 + // Top-level Tool.title (MCP 2025-06-18). Clients such as Codex index this + // for tool search and do not read annotations.title. + ...tool.annotations?.title && { title: tool.annotations.title }, ++ ...tool.icons && { icons: tool.icons }, description: tool.description, inputSchema: tool.parameters ? _xsschema.strictJsonSchema.call(void 0, await _xsschema.toJsonSchema.call(void 0, tool.parameters)) : { additionalProperties: false, -@@ -1026,6 +1032,15 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}` +@@ -1026,6 +1033,15 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}` `Unknown tool: ${request.params.name}` ); } diff --git a/plugins/README.md b/plugins/README.md index 0e1608f2..c73a44f0 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -43,6 +43,12 @@ plugin submission and registered app mapping. Replace the previous skill set with the package's `skills/` contents, preserving the reference directories. Test the draft in ChatGPT and Codex before submitting it for publication. +The maintained OpenAI source preserves its registered app mapping. For the +current remote-MCP submission format, use `pnpm plugin:prepare:openai` with a new +output directory; it preserves the published identity, metadata and skills while +omitting app-reference files and development launchers. See the +[production package guide](openai/app-6a314a73f8ac819195b0d55e36b9c609/README.md). + For a plugin archive, include the manifest, connection file, and `skills/` directory at the archive root. Include dotfiles such as `.app.json`, `.mcp.json`, `.codex-plugin/`, and `.claude-plugin/` as applicable. A skill-only upload contains diff --git a/plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/.app.json b/plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/.app.json index ace6e803..32689390 100644 --- a/plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/.app.json +++ b/plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/.app.json @@ -1,7 +1,8 @@ { "apps": { "app-6a314a73f8ac819195b0d55e36b9c609": { - "id": "asdk_app_6a314a73f8ac819195b0d55e36b9c609" + "id": "asdk_app_6a314a73f8ac819195b0d55e36b9c609", + "required": true } } } diff --git a/plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/.codex-plugin/plugin.json b/plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/.codex-plugin/plugin.json index f81c9573..0ffaf18e 100644 --- a/plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/.codex-plugin/plugin.json +++ b/plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/.codex-plugin/plugin.json @@ -3,24 +3,28 @@ "author": { "name": "SideGuide Technologies, Inc." }, - "description": "Firecrawl helps users search the web, extract page and document content, discover structured data providers, and research developer documentation and scientific papers. It also supports browser interactions, multi-page crawls, structured research jobs, website change monitoring, and account credit-usage checks.", + "description": "Search the web and access data providers and specialized indexes through Alexandria. Research companies, compare products, and find the data you need for your work. Access any page into clean content, and explore scientific papers and code repositories from your conversation.", "interface": { - "capabilities": ["Read", "Write"], + "capabilities": [], "category": "Productivity", "defaultPrompt": [ - "Find current sources for a topic and summarize them", - "Read a webpage and extract its main points", - "Find research papers and verify their key claims" + "Search the web for this week's biggest AI announcements and link to the original sources.", + "Scrape Stripe's API changelog and summarize the changes from the last 30 days.", + "Find sources in Alexandria for US inflation data and pull the latest available figures." ], "developerName": "SideGuide Technologies, Inc.", "displayName": "Firecrawl", - "longDescription": "Firecrawl helps users search the web, extract page and document content, discover structured data providers, and research developer documentation and scientific papers. It also supports browser interactions, multi-page crawls, structured research jobs, website change monitoring, and account credit-usage checks.", + "longDescription": "Search the web and access data providers and specialized indexes through Alexandria. Research companies, compare products, and find the data you need for your work. Access any page into clean content, and explore scientific papers and code repositories from your conversation.", "privacyPolicyURL": "https://www.firecrawl.dev/privacy-policy", - "shortDescription": "Search and extract web data", + "shortDescription": "Search and access more data", + "supportURL": "https://www.firecrawl.dev/support", "termsOfServiceURL": "https://www.firecrawl.dev/terms-of-service", - "websiteURL": "https://firecrawl.dev" + "websiteURL": "https://firecrawl.dev", + "logo": "./assets/firecrawl.svg", + "composerIcon": "./assets/firecrawl.svg", + "brandColor": "#fa5d19" }, "name": "app-6a314a73f8ac819195b0d55e36b9c609", "skills": "./skills", - "version": "2.1.1" + "version": "2.2.1" } diff --git a/plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/README.md b/plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/README.md new file mode 100644 index 00000000..30ac7dfc --- /dev/null +++ b/plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/README.md @@ -0,0 +1,64 @@ +# Firecrawl production plugin + +This source package retains the published Firecrawl identity +`app-6a314a73f8ac819195b0d55e36b9c609` and its registered +`asdk_app_6a314a73f8ac819195b0d55e36b9c609` mapping in `.app.json`. +Version 2.2.1 carries forward the published 2.2.0 listing text, publisher, +starter prompts, support/legal URLs, and all workflow references. The skill adds +the sidebar launcher to the existing account-usage guidance. The listing and +composer icon are the square 128px Firecrawl brand SVG; the MCP sidebar launcher +embeds that same artwork. + +## Production connection and submission format + +`mcp-production.json` selects the full authenticated account endpoint: +`https://mcp.firecrawl.dev/v2/mcp-oauth`. Its public protected-resource metadata +identifies that resource and `https://www.firecrawl.dev` as the authorization +server. Authentication stays in the host's Firecrawl account connection. +There are no bundled credentials, local process launchers, or Keychain settings. + +The source package continues to work through its registered app mapping. Current +OpenAI ZIP uploads cannot contain `apps`/`.app.json` references. Prepare a +separate, self-contained directory for a future upload with: + +```sh +pnpm plugin:prepare:openai /path/to/new-output-directory +``` + +The output retains the same plugin name, publisher, listing metadata, skills and +references. It includes a portable `plugin.json`, `mcp.json`, square icon, and a +Codex compatibility manifest/configuration. It excludes registered app-reference +files and development launchers. Preparation refuses an existing destination; +it does not create a ZIP, install a plugin, submit a draft, or deploy a server. +Use the existing Firecrawl plugin in the developer dashboard when later uploading +an update. Preserve its account connection and publication settings there. + +The UI resource declares `https://mcp.firecrawl.dev` as this plugin's dedicated +component origin, fullscreen display, an empty network/asset CSP, a human-readable +description, and the Firecrawl dashboard origin as an external-link destination. +The app bundles its fonts and provider logos and makes authenticated calls only +through the MCP bridge. Native selections carry exact provider/tool context and +never send a message or execute provider capabilities while browsing. + +## Validation and remaining release work + +Run `pnpm test`, `pnpm lint`, and the browser regressions documented in +`web/README.md`. The automated submission checks verify endpoint selection, +identity, preserved metadata and skill files, icon paths/dimensions, and the +absence of app references or development files in the generated directory. + +Preparing this directory does not enable the UI at the production endpoint. +The branch has been deployed to the private staging service, with the running +image and UI bytes verified against the build. Read-only live checks cover the +launcher metadata/icon, resource CSP, current credits, historical usage, +provider discovery, and invalid-credential recovery. See +[staging testing](../../../staging-testing.md) for the deployment command, +repeatable live checks, and the manual sidebar/composer acceptance flow. +The real ChatGPT Developer Mode check remains a user-run acceptance step. +Final ZIP assembly, reviewer cases, credentials, +and video preparation are separate release steps. No review/publication settings +or test credentials are fabricated in this package. + +References: [Plugin packaging](https://developers.openai.com/plugins/build/plugins), +[submission requirements](https://developers.openai.com/plugins/deploy/submission), +and [UI resource metadata](https://developers.openai.com/plugins/reference). diff --git a/plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/assets/firecrawl.svg b/plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/assets/firecrawl.svg new file mode 100644 index 00000000..b50b39e7 --- /dev/null +++ b/plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/assets/firecrawl.svg @@ -0,0 +1 @@ + diff --git a/plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/mcp-production.json b/plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/mcp-production.json new file mode 100644 index 00000000..5f5af48e --- /dev/null +++ b/plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/mcp-production.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "mcpServers": { + "firecrawl": { + "type": "streamable-http", + "url": "https://mcp.firecrawl.dev/v2/mcp-oauth" + } + } +} diff --git a/plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/skills/firecrawl/SKILL.md b/plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/skills/firecrawl/SKILL.md index 4dcbec49..86174920 100644 --- a/plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/skills/firecrawl/SKILL.md +++ b/plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/skills/firecrawl/SKILL.md @@ -43,7 +43,8 @@ Read only the reference relevant to the operation. The live tool schema is the authority for accepted parameters and limits. For account questions, `firecrawl_credit_usage` reports current or historical -usage. Web, developer, and paper searches are billed per request. Provider-only +usage. Use `firecrawl_usage_dashboard` to open a visual account dashboard +with credits remaining and monthly usage. Web, developer, and paper searches are billed per request. Provider-only discovery is free. Page retrieval is billed per URL; provider execution uses the capability's listed price. diff --git a/plugins/openai/firecrawl-usage-dev/.codex-plugin/plugin.json b/plugins/openai/firecrawl-usage-dev/.codex-plugin/plugin.json new file mode 100644 index 00000000..e20c0737 --- /dev/null +++ b/plugins/openai/firecrawl-usage-dev/.codex-plugin/plugin.json @@ -0,0 +1,22 @@ +{ + "name": "firecrawl-usage-dev", + "version": "0.1.0", + "description": "Browse Alexandria providers and Firecrawl usage in a fullscreen sidebar app. Local development build with macOS Keychain authentication.", + "mcpServers": "./.mcp.json", + "interface": { + "displayName": "Firecrawl Usage Dev", + "shortDescription": "Alexandria providers, credits and usage", + "longDescription": "Browse and select Alexandria providers, pass tool context to a chat, and view credits and usage using the Firecrawl design system.", + "developerName": "SideGuide Technologies, Inc.", + "category": "Productivity", + "capabilities": [ + "Read" + ], + "brandColor": "#fa5d19", + "logo": "./assets/firecrawl.svg", + "composerIcon": "./assets/firecrawl.svg", + "websiteURL": "https://firecrawl.dev", + "privacyPolicyURL": "https://www.firecrawl.dev/privacy-policy", + "termsOfServiceURL": "https://www.firecrawl.dev/terms-of-service" + } +} diff --git a/plugins/openai/firecrawl-usage-dev/.mcp.json b/plugins/openai/firecrawl-usage-dev/.mcp.json new file mode 100644 index 00000000..de31bc9f --- /dev/null +++ b/plugins/openai/firecrawl-usage-dev/.mcp.json @@ -0,0 +1,10 @@ +{ + "mcpServers": { + "firecrawl-usage": { + "command": "python3", + "args": [ + "./scripts/run-keychain.py" + ] + } + } +} diff --git a/plugins/openai/firecrawl-usage-dev/README.md b/plugins/openai/firecrawl-usage-dev/README.md new file mode 100644 index 00000000..87ca6d93 --- /dev/null +++ b/plugins/openai/firecrawl-usage-dev/README.md @@ -0,0 +1,58 @@ +# Firecrawl usage sidebar app: local plugin + +This package tests the new Plugin Extensions global entrypoint. It installs a +named plugin with a bundled MCP server, so the desktop plugin manager can offer +the app-opening and sidebar-pinning controls. The dashboard opens fullscreen; +its launcher is app-only and does not render an inline card in chat. + +The portable root manifest also includes a `.codex-plugin/plugin.json` OpenAI +compatibility overlay and `.mcp.json` for installed desktop runtimes that do not +yet discover the portable `mcp.json` automatically. + +From the MCP repository, run: + +```sh +pnpm build +node scripts/install-usage-plugin.mjs +``` + +The installer stages this package in a dedicated personal marketplace and +installs it using the Codex plugin CLI. It writes only the development worktree +path (and an optional Keychain account selector), never the API key. The server +reads the `FIRECRAWL_API_KEY` generic-password item from macOS Keychain at launch. +Set `FIRECRAWL_KEYCHAIN_ACCOUNT` when installing if multiple accounts use that +service name. + +Restart the desktop app, open Plugins, select **Firecrawl Usage Dev**, open its +app, and choose **Pin to sidebar**. Select that sidebar item to return to the +fullscreen dashboard without sending a chat message. Its **Providers** tab lets +you browse/search the live Alexandria catalog, inspect contracts, and select +providers or individual tools. Selecting attaches the exact tool context and +execution guidance to the native chat composer. Type your request there when +ready. Deselecting updates the context; **Clear** removes the app's attachment. +This requires the host's MCP Apps model-context support. Selection never sends a +message or starts a new chat. Browsing only calls free discovery. + +After updating this existing installation, rebuild and reopen the sidebar app +(or restart MCP servers if the app cached the previous HTML). The plugin/server +identity and resource URI stay the same so the existing sidebar pin can remain. + +The local plugin loads the built server from the worktree. Rebuild the server +after code changes and restart MCP servers. Reinstall after changing package +metadata or its launcher. This does not update the public Firecrawl plugin; +that requires shipping the MCP server with these UI resources and extensions. + +References: [Plugin Extensions](https://developers.openai.com/plugins/build/extensions) +and [plugin packaging](https://developers.openai.com/plugins/build/plugins). + +The plugin listing logo uses this package's `assets/firecrawl.svg`. The sidebar +launcher's icon is built from the published plugin's +`plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/assets/firecrawl.svg`, copied to +`dist/firecrawl-sidebar.svg` by `scripts/build-usage-ui.mjs` and embedded in the +MCP `icons` descriptor. Rebuild after changing that source, then restart MCP +servers to refresh the cached descriptor. The sidebar pin identity stays the same. + +Provider logos are bundled for offline rendering, with initials for missing or +broken artwork. The Providers directory includes collection tabs, category +shelves and search using the Firecrawl web design system. This build contains +Providers and Usage only. See `web/README.md` for logo refresh instructions. diff --git a/plugins/openai/firecrawl-usage-dev/assets/firecrawl.svg b/plugins/openai/firecrawl-usage-dev/assets/firecrawl.svg new file mode 100644 index 00000000..b50b39e7 --- /dev/null +++ b/plugins/openai/firecrawl-usage-dev/assets/firecrawl.svg @@ -0,0 +1 @@ + diff --git a/plugins/openai/firecrawl-usage-dev/mcp.json b/plugins/openai/firecrawl-usage-dev/mcp.json new file mode 100644 index 00000000..949feea9 --- /dev/null +++ b/plugins/openai/firecrawl-usage-dev/mcp.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "mcpServers": { + "firecrawl-usage": { + "type": "stdio", + "command": "python3", + "args": [ + "./scripts/run-keychain.py" + ] + } + } +} diff --git a/plugins/openai/firecrawl-usage-dev/plugin.json b/plugins/openai/firecrawl-usage-dev/plugin.json new file mode 100644 index 00000000..bf1b3b9a --- /dev/null +++ b/plugins/openai/firecrawl-usage-dev/plugin.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "firecrawl-usage-dev", + "version": "0.1.0", + "description": "Browse Alexandria providers and Firecrawl usage in a fullscreen sidebar app. Local development build with macOS Keychain authentication.", + "author": { + "name": "SideGuide Technologies, Inc." + }, + "homepage": "https://firecrawl.dev", + "license": "MIT" +} diff --git a/plugins/openai/firecrawl-usage-dev/scripts/run-keychain.py b/plugins/openai/firecrawl-usage-dev/scripts/run-keychain.py new file mode 100644 index 00000000..e6c1a5ae --- /dev/null +++ b/plugins/openai/firecrawl-usage-dev/scripts/run-keychain.py @@ -0,0 +1,39 @@ +"""Launch the development server with its API key held only in process memory.""" +import os +from pathlib import Path +import shutil +import subprocess +import sys + +worktree = Path(os.environ.get('FIRECRAWL_MCP_WORKTREE', Path(__file__).resolve().parents[4])).expanduser().resolve() +node = shutil.which('node') or next((path for path in ('/opt/homebrew/bin/node', '/usr/local/bin/node') if Path(path).is_file() and os.access(path, os.X_OK)), None) +if not node: + print('Firecrawl: install Node.js 22 or newer and restart MCP servers.', file=sys.stderr) + sys.exit(1) +if not (worktree / 'dist/index.js').is_file(): + print('Firecrawl: build the MCP worktree and reinstall the local plugin.', file=sys.stderr) + sys.exit(1) + +command = ['/usr/bin/security', 'find-generic-password', '-s', 'FIRECRAWL_API_KEY'] +account = os.environ.get('FIRECRAWL_KEYCHAIN_ACCOUNT') +if account: + command.extend(['-a', account]) +command.append('-w') +try: + result = subprocess.run(command, capture_output=True, text=True, check=True, timeout=60) +except (subprocess.SubprocessError, OSError): + print('Firecrawl: could not read the API key from macOS Keychain.', file=sys.stderr) + sys.exit(1) +key = result.stdout.strip() +if not key.startswith('fc-') or len(key) < 12: + print('Firecrawl: the Keychain item does not contain a valid API key.', file=sys.stderr) + sys.exit(1) + +env = os.environ.copy() +for name in ('FIRECRAWL_API_URL', 'FIRECRAWL_OAUTH_TOKEN', 'SSE_LOCAL', + 'FASTMCP_ENDPOINT', 'FIRECRAWL_MCP_PROFILE'): + env.pop(name, None) +env.update(FIRECRAWL_API_KEY=key, HTTP_STREAMABLE_SERVER='false', + CLOUD_SERVICE='false', FIRECRAWL_MCP_SEARCH_ENABLED='false') +os.chdir(worktree) +os.execve(node, [node, str(worktree / 'dist/index.js')], env) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47867d1a..6aea884b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,7 @@ settings: patchedDependencies: fastmcp@4.3.2: - hash: 08f77b633292d9ae4cb022c0fa5bd722dbd5c1aaaad600204f8ed38339bfd37b + hash: d89c17e76e644f13a012c495412dea27591090f3fcf6de78efeeddfa01890b2f path: patches/fastmcp@4.3.2.patch importers: @@ -21,7 +21,7 @@ importers: version: 17.2.2 fastmcp: specifier: 4.3.2 - version: 4.3.2(patch_hash=08f77b633292d9ae4cb022c0fa5bd722dbd5c1aaaad600204f8ed38339bfd37b) + version: 4.3.2(patch_hash=d89c17e76e644f13a012c495412dea27591090f3fcf6de78efeeddfa01890b2f) firecrawl: specifier: 4.42.0 version: 4.42.0 @@ -42,35 +42,87 @@ importers: version: 0.2.0 xsschema: specifier: ^0.4.4 - version: 0.4.4(zod-to-json-schema@3.25.2(zod@4.2.1))(zod@4.2.1) + version: 0.4.4(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.2.1) zod: specifier: ^4.2.1 version: 4.2.1 devDependencies: + '@modelcontextprotocol/ext-apps': + specifier: ^2.0.3 + version: 2.0.3(@modelcontextprotocol/client@2.2.0)(@modelcontextprotocol/core@2.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1) + '@radix-ui/react-dialog': + specifier: 1.1.6 + version: 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-select': + specifier: 2.2.6 + version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': + specifier: 1.1.2 + version: 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@tailwindcss/typography': + specifier: 0.5.19 + version: 0.5.19(tailwindcss@3.4.17) '@types/node': specifier: ^24.3.1 version: 24.5.2 + '@types/react': + specifier: 19.2.8 + version: 19.2.8 + '@types/react-dom': + specifier: 19.2.3 + version: 19.2.3(@types/react@19.2.8) '@typescript-eslint/eslint-plugin': specifier: 8.48.1 version: 8.48.1(@typescript-eslint/parser@8.48.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2) '@typescript-eslint/parser': specifier: 8.48.1 version: 8.48.1(eslint@8.57.1)(typescript@5.9.2) + classnames: + specifier: 2.5.1 + version: 2.5.1 + esbuild: + specifier: ^0.27.7 + version: 0.27.7 eslint: specifier: 8.57.1 version: 8.57.1 eslint-config-prettier: specifier: 10.1.8 version: 10.1.8(eslint@8.57.1) + lucide-react: + specifier: 0.539.0 + version: 0.539.0(react@19.2.3) + motion: + specifier: 12.20.2 + version: 12.20.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + postcss: + specifier: 8.5.6 + version: 8.5.6 + react: + specifier: 19.2.3 + version: 19.2.3 + react-dom: + specifier: 19.2.3 + version: 19.2.3(react@19.2.3) + tailwind-gradient-mask-image: + specifier: 1.2.0 + version: 1.2.0 + tailwindcss: + specifier: 3.4.17 + version: 3.4.17 tsup: specifier: ^8.5.0 - version: 8.5.1(typescript@5.9.2) + version: 8.5.1(jiti@1.21.7)(postcss@8.5.6)(typescript@5.9.2)(yaml@2.9.1) typescript: specifier: ^5.9.2 version: 5.9.2 packages: + '@alloc/quick-lru@5.3.0': + resolution: {integrity: sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==} + engines: {node: '>=10'} + '@borewit/text-codec@0.1.1': resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==} @@ -248,6 +300,21 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} + + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} + + '@floating-ui/react-dom@2.1.9': + resolution: {integrity: sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + '@hono/node-server@1.19.14': resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} engines: {node: '>=18.14.1'} @@ -280,6 +347,32 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@modelcontextprotocol/client@2.2.0': + resolution: {integrity: sha512-LxCou/CSYQ6dwEnjhLZY0KnEuc8V4UJ3IEQCl/uR2yHobSQIEdJKUlKLRYRF5i5FqRGHy1eK7une3aAWDHLtig==} + engines: {node: '>=20'} + + '@modelcontextprotocol/core@2.2.0': + resolution: {integrity: sha512-iLhmprRmWI8EcosOA3wVvww22z02NkgqhV4fBH6f/odQBsi7xnJc0HGmi21yWO+/iKw2yzqVE127Zhna5/+JXw==} + engines: {node: '>=20'} + + '@modelcontextprotocol/ext-apps@2.0.3': + resolution: {integrity: sha512-Tn+4+cyhO4f4cQSDxNyLiIfEE+qlTTuIbWBJB8JTinEXOhDYgCT4wDTQplxD9MBYZH5pCYGC59GaxiwIT4bu8w==} + engines: {node: '>=20'} + peerDependencies: + '@modelcontextprotocol/client': ^2.0.0 + '@modelcontextprotocol/core': ^2.0.0 + '@modelcontextprotocol/server': ^2.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + zod: ^4.2.0 + peerDependenciesMeta: + '@modelcontextprotocol/server': + optional: true + react: + optional: true + react-dom: + optional: true + '@modelcontextprotocol/sdk@1.29.0': resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} engines: {node: '>=18'} @@ -302,6 +395,420 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.1': + resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@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-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.1': + resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + 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@1.1.1': + resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} + 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@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.6': + resolution: {integrity: sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==} + 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-dismissable-layer@1.1.5': + resolution: {integrity: sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==} + 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.1': + resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} + 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-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.2': + resolution: {integrity: sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==} + 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-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-id@1.1.0': + resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + 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-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.4': + resolution: {integrity: sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==} + 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.2': + resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==} + 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.0.2': + resolution: {integrity: sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==} + 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-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-slot@1.1.2': + resolution: {integrity: sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + 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-use-callback-ref@1.1.0': + resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} + 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-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.1.0': + resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} + 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.0': + resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} + 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-layout-effect@1.1.0': + resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} + 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==} + '@rollup/rollup-android-arm-eabi@4.62.2': resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} cpu: [arm] @@ -336,79 +843,66 @@ packages: resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.62.2': resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.62.2': resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.62.2': resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.62.2': resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.62.2': resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.62.2': resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.62.2': resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.62.2': resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.62.2': resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.62.2': resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.62.2': resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.62.2': resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.62.2': resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} @@ -450,6 +944,14 @@ packages: '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@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' + '@tokenizer/inflate@0.4.1': resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} engines: {node: '>=18'} @@ -463,6 +965,14 @@ packages: '@types/node@24.5.2': resolution: {integrity: sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==} + '@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.8': + resolution: {integrity: sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==} + '@typescript-eslint/eslint-plugin@8.48.1': resolution: {integrity: sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -580,9 +1090,20 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + 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'} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -592,6 +1113,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + body-parser@2.3.0: resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} @@ -602,6 +1127,10 @@ packages: brace-expansion@2.1.1: resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -628,14 +1157,25 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + cliui@9.0.1: resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} engines: {node: '>=20'} @@ -697,6 +1237,14 @@ packages: 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==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -731,6 +1279,15 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} @@ -861,6 +1418,10 @@ packages: 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==} @@ -903,6 +1464,10 @@ packages: resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} engines: {node: '>=20'} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + finalhandler@2.1.0: resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} engines: {node: '>= 0.8'} @@ -942,6 +1507,20 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} + framer-motion@12.43.0: + resolution: {integrity: sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -977,6 +1556,10 @@ packages: 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-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -985,6 +1568,10 @@ packages: resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} engines: {node: '>=18'} + 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'} @@ -1020,6 +1607,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + hono@4.12.27: resolution: {integrity: sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==} engines: {node: '>=16.9.0'} @@ -1085,6 +1676,14 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.17.0: + resolution: {integrity: sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1093,6 +1692,10 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} @@ -1115,6 +1718,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + jose@6.2.3: resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} @@ -1185,6 +1792,11 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lucide-react@0.539.0: + resolution: {integrity: sha512-VVISr+VF2krO91FeuCrm1rSOLACQUYVy7NQkzrOty52Y8TlTPcXcMdQFj9bYzBgXbWCiywlwSZ3Z8u6a+6bMlg==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1204,6 +1816,14 @@ packages: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -1230,12 +1850,37 @@ packages: mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + motion-dom@12.43.0: + resolution: {integrity: sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==} + + motion-utils@12.39.0: + resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==} + + motion@12.20.2: + resolution: {integrity: sha512-V7dEQYvETH+0OHZLPFHxV3nYc0tELBtjDMQlZXFO8Zctnr582XrjPYqm348iTFgahFYd1QxBbzmE4KYilDyk2A==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -1247,6 +1892,10 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + npm-run-path@6.0.0: resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} engines: {node: '>=18'} @@ -1255,6 +1904,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -1306,6 +1959,9 @@ packages: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} @@ -1315,6 +1971,10 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} @@ -1335,6 +1995,30 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + postcss-load-config@6.0.1: resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} engines: {node: '>= 18'} @@ -1353,6 +2037,27 @@ packages: yaml: optional: true + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + + postcss-selector-parser@6.1.4: + resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -1391,6 +2096,52 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} + react-dom@19.2.3: + resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} + peerDependencies: + react: ^19.2.3 + + 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@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.2: + resolution: {integrity: sha512-/peqiBB/n07gQGLsWaHho3WfvUyRscw0gYTsEFMhrIe/nWLkYaf5SbKYjGYqtRV3aPwykJgF2VEMo1ac4bnsGA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -1407,6 +2158,11 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -1431,6 +2187,9 @@ packages: 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@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -1475,6 +2234,10 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + source-map@0.7.6: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} @@ -1523,6 +2286,18 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwind-gradient-mask-image@1.2.0: + resolution: {integrity: sha512-tUJaGhvqbJFiVKJu6EU5n//KvGdVvY3L3VOFNqjztk13+ifAk00pcSNHBTgHfUiBGOEzDn0gFRbSmsftUV1lXA==} + + tailwindcss@3.4.17: + resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} + engines: {node: '>=14.0.0'} + hasBin: true + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -1544,6 +2319,10 @@ packages: resolution: {integrity: sha512-EORDwFMSZKrHPUVDhejCMDeAovRS5d8jZKiqALFiPp3cjKjEldPkxBY39ZSx3c45awz3RpKwJD1cCgGxEfy8/A==} engines: {node: '>= 20'} + 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'} @@ -1565,6 +2344,9 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsscmp@1.0.6: resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} engines: {node: '>=0.6.x'} @@ -1636,6 +2418,29 @@ packages: uri-templates@0.2.0: resolution: {integrity: sha512-EWkjYEN0L6KOfEoOH6Wj4ghQqU7eBZMJqRHQnxQAq+dSEzRPClkWjf8557HkWQXF6BrAUoLSAyy9i3RVTliaNg==} + 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-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 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -1683,6 +2488,11 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@22.0.0: resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} @@ -1715,6 +2525,8 @@ packages: snapshots: + '@alloc/quick-lru@5.3.0': {} + '@borewit/text-codec@0.1.1': {} '@esbuild/aix-ppc64@0.27.7': @@ -1816,7 +2628,24 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@8.57.1': {} + '@eslint/js@8.57.1': {} + + '@floating-ui/core@1.8.0': + dependencies: + '@floating-ui/utils': 0.2.12 + + '@floating-ui/dom@1.8.0': + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 + + '@floating-ui/react-dom@2.1.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@floating-ui/dom': 1.8.0 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + '@floating-ui/utils@0.2.12': {} '@hono/node-server@1.19.14(hono@4.12.27)': dependencies: @@ -1848,6 +2677,30 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@modelcontextprotocol/client@2.2.0': + dependencies: + '@modelcontextprotocol/core': 2.2.0 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + jose: 6.2.3 + pkce-challenge: 5.0.0 + zod: 4.4.3 + + '@modelcontextprotocol/core@2.2.0': + dependencies: + zod: 4.4.3 + + '@modelcontextprotocol/ext-apps@2.0.3(@modelcontextprotocol/client@2.2.0)(@modelcontextprotocol/core@2.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.2.1)': + dependencies: + '@modelcontextprotocol/client': 2.2.0 + '@modelcontextprotocol/core': 2.2.0 + '@standard-schema/spec': 1.1.0 + zod: 4.2.1 + optionalDependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + '@modelcontextprotocol/sdk@1.29.0(zod@4.2.1)': dependencies: '@hono/node-server': 1.19.14(hono@4.12.27) @@ -1904,6 +2757,359 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) + + '@radix-ui/react-compose-refs@1.1.1(@types/react@19.2.8)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.8)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-context@1.1.1(@types/react@19.2.8)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-context@1.1.2(@types/react@19.2.8)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-dialog@1.1.6(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.0(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.2.8)(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.8)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.8)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) + + '@radix-ui/react-dismissable-layer@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) + + '@radix-ui/react-focus-guards@1.1.1(@types/react@19.2.8)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.8)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-focus-scope@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) + + '@radix-ui/react-id@1.1.0(@types/react@19.2.8)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-id@1.1.1(@types/react@19.2.8)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@floating-ui/react-dom': 2.1.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/rect': 1.1.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) + + '@radix-ui/react-portal@1.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) + + '@radix-ui/react-presence@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) + + '@radix-ui/react-primitive@2.0.2(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-slot': 1.1.2(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) + + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + 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.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.8)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) + + '@radix-ui/react-slot@1.1.2(@types/react@19.2.8)(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.8)(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@19.2.8)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.8)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@19.2.8)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.8)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.8)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@19.2.8)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.8)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@19.2.8)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.8)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.8)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.8)(react@19.2.3)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.8)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.8 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) + + '@radix-ui/rect@1.1.1': {} + '@rollup/rollup-android-arm-eabi@4.62.2': optional: true @@ -1985,6 +3191,13 @@ snapshots: '@standard-schema/spec@1.0.0': {} + '@standard-schema/spec@1.1.0': {} + + '@tailwindcss/typography@0.5.19(tailwindcss@3.4.17)': + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 3.4.17 + '@tokenizer/inflate@0.4.1': dependencies: debug: 4.4.3 @@ -2000,6 +3213,14 @@ snapshots: dependencies: undici-types: 7.12.0 + '@types/react-dom@19.2.3(@types/react@19.2.8)': + dependencies: + '@types/react': 19.2.8 + + '@types/react@19.2.8': + dependencies: + csstype: 3.2.3 + '@typescript-eslint/eslint-plugin@8.48.1(@typescript-eslint/parser@8.48.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -2146,8 +3367,19 @@ snapshots: any-promise@1.3.0: {} + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + arg@5.0.2: {} + argparse@2.0.1: {} + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + asynckit@0.4.0: {} axios@1.18.0(debug@4.4.3): @@ -2162,6 +3394,8 @@ snapshots: balanced-match@1.0.2: {} + binary-extensions@2.3.0: {} + body-parser@2.3.0: dependencies: bytes: 3.1.2 @@ -2185,6 +3419,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + bundle-require@5.1.0(esbuild@0.27.7): dependencies: esbuild: 0.27.7 @@ -2206,15 +3444,31 @@ snapshots: callsites@3.1.0: {} + camelcase-css@2.0.1: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + 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 + chokidar@4.0.3: dependencies: readdirp: 4.1.2 + classnames@2.5.1: {} + cliui@9.0.1: dependencies: string-width: 7.2.0 @@ -2265,6 +3519,10 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + cssesc@3.0.0: {} + + csstype@3.2.3: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -2283,6 +3541,12 @@ snapshots: destroy@1.2.0: {} + detect-node-es@1.1.0: {} + + didyoumean@1.2.2: {} + + dlv@1.1.3: {} + doctrine@3.0.0: dependencies: esutils: 2.0.3 @@ -2492,13 +3756,21 @@ snapshots: 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.3: {} - fastmcp@4.3.2(patch_hash=08f77b633292d9ae4cb022c0fa5bd722dbd5c1aaaad600204f8ed38339bfd37b): + fastmcp@4.3.2(patch_hash=d89c17e76e644f13a012c495412dea27591090f3fcf6de78efeeddfa01890b2f): dependencies: '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) '@standard-schema/spec': 1.0.0 @@ -2547,6 +3819,10 @@ snapshots: transitivePeerDependencies: - supports-color + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + finalhandler@2.1.0: dependencies: debug: 4.4.3 @@ -2601,6 +3877,15 @@ snapshots: forwarded@0.2.0: {} + framer-motion@12.43.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + motion-dom: 12.43.0 + motion-utils: 12.39.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + fresh@0.5.2: {} fresh@2.0.0: {} @@ -2631,6 +3916,8 @@ snapshots: hasown: 2.0.2 math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -2641,6 +3928,10 @@ snapshots: '@sec-ant/readable-stream': 0.4.1 is-stream: 4.0.1 + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -2674,6 +3965,10 @@ snapshots: dependencies: function-bind: 1.1.2 + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + hono@4.12.27: {} http-assert@1.5.0: @@ -2738,12 +4033,22 @@ snapshots: ipaddr.js@1.9.1: {} + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.17.0: + dependencies: + hasown: 2.0.4 + is-extglob@2.1.1: {} is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-number@7.0.0: {} + is-path-inside@3.0.3: {} is-plain-obj@4.1.0: {} @@ -2756,6 +4061,8 @@ snapshots: isexe@2.0.0: {} + jiti@1.21.7: {} + jose@6.2.3: {} joycon@3.1.1: {} @@ -2833,6 +4140,10 @@ snapshots: lodash.merge@4.6.2: {} + lucide-react@0.539.0(react@19.2.3): + dependencies: + react: 19.2.3 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2849,6 +4160,13 @@ snapshots: merge-descriptors@2.0.0: {} + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + mime-db@1.52.0: {} mime-db@1.54.0: {} @@ -2876,6 +4194,20 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.4 + motion-dom@12.43.0: + dependencies: + motion-utils: 12.39.0 + + motion-utils@12.39.0: {} + + motion@12.20.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + framer-motion: 12.43.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + tslib: 2.8.1 + optionalDependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + ms@2.1.3: {} mz@2.7.0: @@ -2884,12 +4216,16 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + nanoid@3.3.19: {} + natural-compare@1.4.0: {} negotiator@0.6.3: {} negotiator@1.0.0: {} + normalize-path@3.0.0: {} + npm-run-path@6.0.0: dependencies: path-key: 4.0.0 @@ -2897,6 +4233,8 @@ snapshots: object-assign@4.1.1: {} + object-hash@3.0.0: {} + object-inspect@1.13.4: {} on-finished@2.4.1: @@ -2940,12 +4278,16 @@ snapshots: path-key@4.0.0: {} + path-parse@1.0.7: {} + path-to-regexp@8.3.0: {} pathe@2.0.3: {} picocolors@1.1.1: {} + picomatch@2.3.2: {} + picomatch@4.0.4: {} pipenet@1.4.2: @@ -2971,9 +4313,55 @@ snapshots: mlly: 1.8.2 pathe: 2.0.3 - postcss-load-config@6.0.1: + postcss-import@15.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + read-cache: 1.0.2 + resolve: 1.22.12 + + postcss-js@4.1.0(postcss@8.5.6): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.6 + + postcss-load-config@4.0.2(postcss@8.5.6): dependencies: lilconfig: 3.1.3 + yaml: 2.9.1 + optionalDependencies: + postcss: 8.5.6 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.9.1): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.6 + yaml: 2.9.1 + + postcss-nested@6.2.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.4 + + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@6.1.4: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.2 prelude-ls@1.2.1: {} @@ -3011,6 +4399,46 @@ snapshots: iconv-lite: 0.7.2 unpipe: 1.0.0 + react-dom@19.2.3(react@19.2.3): + dependencies: + react: 19.2.3 + scheduler: 0.27.0 + + react-remove-scroll-bar@2.3.8(@types/react@19.2.8)(react@19.2.3): + dependencies: + react: 19.2.3 + react-style-singleton: 2.2.3(@types/react@19.2.8)(react@19.2.3) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.8 + + react-remove-scroll@2.7.2(@types/react@19.2.8)(react@19.2.3): + dependencies: + react: 19.2.3 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.8)(react@19.2.3) + react-style-singleton: 2.2.3(@types/react@19.2.8)(react@19.2.3) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.8)(react@19.2.3) + use-sidecar: 1.1.3(@types/react@19.2.8)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.8 + + react-style-singleton@2.2.3(@types/react@19.2.8)(react@19.2.3): + dependencies: + get-nonce: 1.0.1 + react: 19.2.3 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.8 + + react@19.2.3: {} + + read-cache@1.0.2: {} + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + readdirp@4.1.2: {} require-from-string@2.0.2: {} @@ -3019,6 +4447,13 @@ snapshots: resolve-from@5.0.0: {} + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.17.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + reusify@1.1.0: {} rimraf@3.0.2: @@ -3072,6 +4507,8 @@ snapshots: safer-buffer@2.1.2: {} + scheduler@0.27.0: {} + semver@7.8.5: {} send@1.2.0: @@ -3137,6 +4574,8 @@ snapshots: signal-exit@4.1.0: {} + source-map-js@1.2.2: {} + source-map@0.7.6: {} statuses@1.5.0: {} @@ -3181,6 +4620,37 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} + + tailwind-gradient-mask-image@1.2.0: {} + + tailwindcss@3.4.17: + dependencies: + '@alloc/quick-lru': 5.3.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.1.0(postcss@8.5.6) + postcss-load-config: 4.0.2(postcss@8.5.6) + postcss-nested: 6.2.0(postcss@8.5.6) + postcss-selector-parser: 6.1.4 + resolve: 1.22.12 + sucrase: 3.35.1 + transitivePeerDependencies: + - ts-node + text-table@0.2.0: {} thenify-all@1.6.0: @@ -3202,6 +4672,10 @@ snapshots: dependencies: punycode: 2.3.1 + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + toidentifier@1.0.1: {} token-types@6.1.1: @@ -3218,9 +4692,11 @@ snapshots: ts-interface-checker@0.1.13: {} + tslib@2.8.1: {} + tsscmp@1.0.6: {} - tsup@8.5.1(typescript@5.9.2): + tsup@8.5.1(jiti@1.21.7)(postcss@8.5.6)(typescript@5.9.2)(yaml@2.9.1): dependencies: bundle-require: 5.1.0(esbuild@0.27.7) cac: 6.7.14 @@ -3231,7 +4707,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1 + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.9.1) resolve-from: 5.0.0 rollup: 4.62.2 source-map: 0.7.6 @@ -3240,6 +4716,7 @@ snapshots: tinyglobby: 0.2.17 tree-kill: 1.2.2 optionalDependencies: + postcss: 8.5.6 typescript: 5.9.2 transitivePeerDependencies: - jiti @@ -3281,6 +4758,23 @@ snapshots: uri-templates@0.2.0: {} + use-callback-ref@1.3.3(@types/react@19.2.8)(react@19.2.3): + dependencies: + react: 19.2.3 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.8 + + use-sidecar@1.1.3(@types/react@19.2.8)(react@19.2.3): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.3 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.8 + + util-deprecate@1.0.2: {} + vary@1.1.2: {} which@2.0.2: @@ -3297,7 +4791,7 @@ snapshots: wrappy@1.0.2: {} - xsschema@0.4.4(zod-to-json-schema@3.25.2(zod@4.2.1))(zod@4.2.1): + xsschema@0.4.4(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.2.1): optionalDependencies: zod: 4.2.1 zod-to-json-schema: 3.25.2(zod@4.4.3) @@ -3309,6 +4803,8 @@ snapshots: y18n@5.0.8: {} + yaml@2.9.1: {} + yargs-parser@22.0.0: {} yargs@18.0.0: diff --git a/scripts/build-usage-ui.mjs b/scripts/build-usage-ui.mjs new file mode 100644 index 00000000..50ad9c3b --- /dev/null +++ b/scripts/build-usage-ui.mjs @@ -0,0 +1,93 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { build, transform } from 'esbuild'; +import { createRequire } from 'node:module'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { createElement } from 'react'; +import postcss from 'postcss'; +import tailwindcss from 'tailwindcss'; + +const root = new URL('../', import.meta.url); +const read = (path) => readFile(new URL(path, root), 'utf8'); +const result = await build({ + entryPoints: [fileURLToPath(new URL('web/usage.tsx', root))], + bundle: true, + format: 'iife', + platform: 'browser', + target: 'es2022', + minify: true, + write: false, + legalComments: 'inline', + define: { 'process.env.NODE_ENV': '"production"' }, + loader: { '.css': 'empty', '.svg': 'text' }, +}); +const fonts = await Promise.all( + [400, 450, 500].map(async (weight) => { + const data = await readFile(new URL(`web/fonts/${weight}.woff2`, root)); + return `@font-face{font-family:SuisseIntl;font-style:normal;font-weight:${weight};font-display:swap;src:url(data:font/woff2;base64,${data.toString('base64')}) format('woff2')}`; + }) +); +const require = createRequire(import.meta.url); +async function loadTs(path) { + const { code } = await transform(await read(path), { + loader: 'ts', + format: 'cjs', + }); + const module = { exports: {} }; + new Function('require', 'module', 'exports', code)( + require, + module, + module.exports + ); + return module.exports.default; +} +const config = await loadTs('web/ui/tailwind.config.ts'); +const baseCss = await postcss([tailwindcss(config)]).process( + '@tailwind base;', + { from: undefined } +); +const css = await postcss([tailwindcss(config)]).process( + '@tailwind components; @tailwind utilities;', + { from: undefined } +); +const colors = await loadTs('web/ui/colors.ts'); +const tokens = (theme) => + Object.entries(colors) + .map(([name, values]) => `--${name}: #${values[theme].hex};`) + .join('\n'); +const tokenCss = `:root {${tokens('light')}} :root[data-theme="dark"] {${tokens('dark')}}`; +const server = await build({ + entryPoints: [fileURLToPath(new URL('web/usage.tsx', root))], + bundle: true, + format: 'cjs', + platform: 'node', + target: 'es2022', + external: ['react', 'react-dom/client', 'react/jsx-runtime'], + write: false, + loader: { '.css': 'empty', '.svg': 'text' }, + define: { 'process.env.NODE_ENV': '"production"' }, +}); +const serverModule = { exports: {} }; +new Function('require', 'module', 'exports', server.outputFiles[0].text)( + require, + serverModule, + serverModule.exports +); +const appHtml = renderToStaticMarkup( + createElement(serverModule.exports.Dashboard) +); +const styles = ``; +const script = ``; +// Function replacements preserve literal dollar sequences in bundled code. +const html = (await read('web/usage.html')) + .replace('', () => appHtml) + .replace('', () => styles) + .replace('', () => script); +await writeFile(new URL('dist/usage.html', root), html); +// MCP launcher icons must travel in tools/list, independently of package logos. +await writeFile( + new URL('dist/firecrawl-sidebar.svg', root), + await read( + 'plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/assets/firecrawl.svg' + ) +); diff --git a/scripts/install-usage-plugin.mjs b/scripts/install-usage-plugin.mjs new file mode 100644 index 00000000..294ab811 --- /dev/null +++ b/scripts/install-usage-plugin.mjs @@ -0,0 +1,37 @@ +import { cp, mkdir, readFile, writeFile } from 'node:fs/promises'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { execFileSync } from 'node:child_process'; + +const worktree = fileURLToPath(new URL('../', import.meta.url)); +const source = join(worktree, 'plugins/openai/firecrawl-usage-dev'); +const root = join(homedir(), '.codex/local-marketplaces/firecrawl-sidebar'); +const plugin = join(root, 'plugins/firecrawl-usage-dev'); +await readFile(join(worktree, 'dist/usage.html')); +await mkdir(join(root, '.agents/plugins'), { recursive: true }); +await cp(source, plugin, { recursive: true }); +const mcp = JSON.parse(await readFile(join(plugin, 'mcp.json'), 'utf8')); +mcp.mcpServers['firecrawl-usage'].env = { FIRECRAWL_MCP_WORKTREE: worktree }; +if (process.env.FIRECRAWL_KEYCHAIN_ACCOUNT) { + mcp.mcpServers['firecrawl-usage'].env.FIRECRAWL_KEYCHAIN_ACCOUNT = process.env.FIRECRAWL_KEYCHAIN_ACCOUNT; +} +await writeFile(join(plugin, 'mcp.json'), JSON.stringify(mcp, null, 2) + '\n'); +const legacyMcp = { mcpServers: Object.fromEntries(Object.entries(mcp.mcpServers).map(([name, server]) => { + const { type, ...config } = server; + return [name, config]; +})) }; +await writeFile(join(plugin, '.mcp.json'), JSON.stringify(legacyMcp, null, 2) + '\n'); +await writeFile(join(root, '.agents/plugins/marketplace.json'), JSON.stringify({ + name: 'firecrawl-local', + interface: { displayName: 'Firecrawl Local Development' }, + plugins: [{ + name: 'firecrawl-usage-dev', + source: { source: 'local', path: './plugins/firecrawl-usage-dev' }, + policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' }, + category: 'Productivity', + }], +}, null, 2) + '\n'); +execFileSync('codex', ['plugin', 'marketplace', 'add', root], { stdio: 'inherit' }); +execFileSync('codex', ['plugin', 'add', 'firecrawl-usage-dev@firecrawl-local'], { stdio: 'inherit' }); +console.log('Installed Firecrawl Usage Dev. Restart the desktop app, open Plugins, then pin its app to the sidebar.'); diff --git a/scripts/logo-snapshot.mjs b/scripts/logo-snapshot.mjs new file mode 100644 index 00000000..8c33434d --- /dev/null +++ b/scripts/logo-snapshot.mjs @@ -0,0 +1,21 @@ +import { randomUUID } from 'node:crypto'; +import { writeFile, rename, rm } from 'node:fs/promises'; + +export function snapshotProviders(snapshot) { + const providers = snapshot?.providers; + return providers && typeof providers === 'object' && !Array.isArray(providers) + ? providers + : {}; +} + +export async function writeLogoSnapshot(output, snapshot) { + const temporary = new URL(`.provider-logos-${randomUUID()}.tmp`, output); + try { + await writeFile(temporary, JSON.stringify(snapshot, null, 2) + '\n', { + flag: 'wx', + }); + await rename(temporary, output); + } finally { + await rm(temporary, { force: true }); + } +} diff --git a/scripts/prepare-openai-plugin.mjs b/scripts/prepare-openai-plugin.mjs new file mode 100644 index 00000000..b59bb9d8 --- /dev/null +++ b/scripts/prepare-openai-plugin.mjs @@ -0,0 +1,75 @@ +import assert from 'node:assert/strict'; +import { cp, mkdir, readFile, writeFile } from 'node:fs/promises'; +import { resolve, join, dirname } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +export const pluginSource = fileURLToPath( + new URL( + '../plugins/openai/app-6a314a73f8ac819195b0d55e36b9c609/', + import.meta.url + ) +); +const json = async (path) => JSON.parse(await readFile(path, 'utf8')); +const writeJson = (path, value) => + writeFile(path, JSON.stringify(value, null, 2) + '\n'); + +// Keep registered app mapping in the source package. Public ZIP uploads accept +// remote MCP configurations, not apps/.app.json references. Never create a new +// Firecrawl identity or include the Keychain development launcher in this output. +export async function prepareOpenAIPlugin(output) { + const manifest = await json(join(pluginSource, '.codex-plugin/plugin.json')); + const mapping = await json(join(pluginSource, '.app.json')); + const connection = await json(join(pluginSource, 'mcp-production.json')); + assert.equal(manifest.name, 'app-6a314a73f8ac819195b0d55e36b9c609'); + assert.equal( + mapping.apps[manifest.name].id, + 'asdk_app_6a314a73f8ac819195b0d55e36b9c609' + ); + assert.deepEqual(connection.mcpServers, { + firecrawl: { + type: 'streamable-http', + url: 'https://mcp.firecrawl.dev/v2/mcp-oauth', + }, + }); + const destination = resolve(output); + // Refuse existing destinations instead of erasing user files or stale builds. + await mkdir(dirname(destination), { recursive: true }); + await mkdir(destination); + await cp(join(pluginSource, 'skills'), join(destination, 'skills'), { + recursive: true, + }); + await cp(join(pluginSource, 'assets'), join(destination, 'assets'), { + recursive: true, + }); + const { apps, skills, ...compatibility } = manifest; + await writeJson(join(destination, 'plugin.json'), { + $schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json', + name: manifest.name, + version: manifest.version, + description: manifest.description, + author: manifest.author, + extensions: { 'com.openai': { interface: manifest.interface } }, + }); + await writeJson(join(destination, 'mcp.json'), connection); + await writeJson(join(destination, '.mcp.json'), { + mcpServers: { firecrawl: { url: connection.mcpServers.firecrawl.url } }, + }); + await mkdir(join(destination, '.codex-plugin')); + await writeJson(join(destination, '.codex-plugin/plugin.json'), { + ...compatibility, + skills, + mcpServers: './.mcp.json', + }); + return destination; +} + +if ( + process.argv[1] && + import.meta.url === pathToFileURL(resolve(process.argv[1])).href +) { + if (!process.argv[2]) + throw new Error( + 'Pass a new output directory: pnpm plugin:prepare:openai /path/to/output' + ); + console.log(await prepareOpenAIPlugin(process.argv[2])); +} diff --git a/scripts/preview-usage-ui.mjs b/scripts/preview-usage-ui.mjs new file mode 100644 index 00000000..711b8771 --- /dev/null +++ b/scripts/preview-usage-ui.mjs @@ -0,0 +1,82 @@ +import { createServer } from 'node:http'; +import { readFile } from 'node:fs/promises'; +import { previewCatalog } from './usage-preview-fixtures.mjs'; + +// Only this local host supplies fixtures. The production HTML always calls MCP. +const preview = `Firecrawl usage preview + +
Local preview · Sample data
+ +`; +const server = createServer(async (request, response) => { + try { + const path = new URL(request.url, 'http://localhost').pathname; + if (path === '/favicon.ico') { + response.writeHead(204).end(); + return; + } + if (path !== '/' && path !== '/usage.html') { + response.writeHead(404).end(); + return; + } + response.setHeader('Content-Type', 'text/html; charset=utf-8'); + // Model the host's no-network sandbox policy, while allowing embedded assets. + if (path === '/usage.html') + response.setHeader( + 'Content-Security-Policy', + "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; font-src data:; img-src data:; connect-src 'none'; frame-ancestors 'self'" + ); + response.end( + path === '/' + ? preview + : await readFile(new URL('../dist/usage.html', import.meta.url), 'utf8') + ); + } catch { + response.writeHead(500).end('Build the usage interface first.'); + } +}); +const port = Number(process.env.USAGE_PREVIEW_PORT ?? 4173); +server.listen(port, '127.0.0.1', () => + console.log('Usage preview (sample data): http://127.0.0.1:' + port) +); diff --git a/scripts/refresh-provider-logos.mjs b/scripts/refresh-provider-logos.mjs new file mode 100644 index 00000000..e59620d9 --- /dev/null +++ b/scripts/refresh-provider-logos.mjs @@ -0,0 +1,191 @@ +import { execFileSync } from 'node:child_process'; +import { readFile } from 'node:fs/promises'; +import { snapshotProviders, writeLogoSnapshot } from './logo-snapshot.mjs'; + +// Explicit maintenance step; normal builds never require credentials or network. +// Only discovery is authenticated. Public favicon requests never receive a key. +const account = process.env.FIRECRAWL_KEYCHAIN_ACCOUNT; +const key = + process.env.FIRECRAWL_API_KEY?.trim() || + execFileSync( + '/usr/bin/security', + [ + 'find-generic-password', + '-s', + 'FIRECRAWL_API_KEY', + ...(account ? ['-a', account] : []), + '-w', + ], + { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] } + ).trim(); +if (!key) + throw new Error('A Firecrawl credential is required to refresh logos'); +async function discovery(path = '') { + const response = await fetch( + 'https://api.firecrawl.dev/exchange/discover' + path, + { + headers: { Authorization: 'Bearer ' + key }, + redirect: 'error', + signal: AbortSignal.timeout(15000), + } + ); + if (!response.ok) + throw new Error('Provider discovery failed: ' + response.status); + return response.json(); +} +async function parallel(items, concurrency, run) { + let next = 0; + const results = new Array(items.length); + await Promise.all( + Array.from({ length: Math.min(concurrency, items.length) }, async () => { + while (next < items.length) { + const index = next++; + results[index] = await run(items[index]); + } + }) + ); + return results; +} +const index = await discovery(); +if (!Array.isArray(index.cohorts)) throw new Error('Invalid discovery index'); +const cohorts = await parallel(index.cohorts, 5, ({ cohort }) => + discovery('/' + encodeURIComponent(cohort)) +); +const providers = new Map(); +for (const page of cohorts) { + if ( + !Array.isArray(page.providers) || + page.catalogueVersion !== index.catalogueVersion + ) + throw new Error('Catalog changed or returned an invalid page; retry'); + for (const provider of page.providers) + if (typeof provider.provider === 'string') + providers.set(provider.provider, provider); +} +let previous = {}; +try { + previous = snapshotProviders( + JSON.parse( + await readFile( + new URL('../web/provider-logos.json', import.meta.url), + 'utf8' + ) + ) + ); +} catch { + /* Initial snapshot. */ +} +const missing = []; +const logos = await parallel([...providers.values()], 8, async (provider) => { + let website; + try { + website = new URL( + /^https?:\/\//i.test(provider.website) + ? provider.website + : 'https://' + provider.website + ); + if ( + !['https:', 'http:'].includes(website.protocol) || + !website.hostname.includes('.') + ) + return undefined; + } catch { + return undefined; + } + const domain = website.hostname.toLowerCase().replace(/^www\./, ''); + // Brand overrides mirror firecrawl-web and are included in this checkout. + let override; + if (domain === 'datalegion.ai') { + const bytes = await readFile( + new URL( + '../web/provider-icon-overrides/data-legion.webp', + import.meta.url + ) + ); + override = 'data:image/webp;base64,' + bytes.toString('base64'); + } else if (domain === 'firecrawl.dev') { + const svg = ( + await readFile( + new URL('../web/firecrawl-icon.svg', import.meta.url), + 'utf8' + ) + ) + .replace(' 65536) + throw new Error('Invalid icon size'); + return [ + provider.provider, + { + website: website.href, + src: 'data:' + mime + ';base64,' + bytes.toString('base64'), + }, + ]; + } catch { + // A transient image outage must not remove previously working artwork. + if (previous[provider.provider]?.website === website.href) + return [provider.provider, previous[provider.provider]]; + missing.push(provider.provider); + return undefined; + } +}); +const snapshot = { + catalogueVersion: index.catalogueVersion, + providers: Object.fromEntries( + logos.filter(Boolean).sort(([a], [b]) => a.localeCompare(b)) + ), +}; +await writeLogoSnapshot( + new URL('../web/provider-logos.json', import.meta.url), + snapshot +); + +console.log( + JSON.stringify({ + discovered: providers.size, + bundled: Object.keys(snapshot.providers).length, + missing: missing.sort(), + }) +); diff --git a/scripts/usage-preview-fixtures.mjs b/scripts/usage-preview-fixtures.mjs new file mode 100644 index 00000000..09e1e1cd --- /dev/null +++ b/scripts/usage-preview-fixtures.mjs @@ -0,0 +1,147 @@ +// Synthetic local data; production calls the connected MCP server instead. +export function previewCatalog(args, mode) { + args = { + ...args, + level: + args.level ?? + (args.capabilities?.length || args.providers?.length + ? 'tools' + : args.categories?.length + ? 'providers' + : 'categories'), + }; + if ( + mode === 'catalog-error' || + (mode === 'categories-error' && args.level === 'categories') || + (mode === 'tools-error' && args.level === 'tools') + ) + return { + isError: true, + content: [{ type: 'text', text: 'Fixture discovery failed' }], + }; + const providers = [ + { + id: 'allbirds-com', + name: 'Allbirds', + description: 'Sample catalog: discover shoes and product information.', + category: 'shopping', + }, + { + id: 'amazon-com', + name: 'Amazon US', + description: 'Sample catalog: product offers and photos.', + category: 'shopping', + }, + { + id: 'amtrak-com', + name: 'Amtrak', + description: 'Sample catalog: travel information.', + category: 'travel', + }, + { + id: 'fred', + name: 'FRED', + description: 'Sample catalog: economic series and observations.', + category: 'finance', + }, + { + id: 'github-com', + name: 'GitHub', + description: 'Sample catalog: software repositories.', + category: 'software', + }, + { + id: 'pubmed-ncbi-nlm-nih-gov', + name: 'PubMed', + description: 'Sample catalog: scientific research.', + category: 'research', + }, + ]; + let rows; + if (args.level === 'categories') + rows = ['shopping', 'travel', 'finance', 'software', 'research'].map( + (id) => ({ id, description: `Sample ${id} providers` }) + ); + else if (args.level === 'providers') + rows = providers + .filter( + (p) => !args.categories?.length || args.categories.includes(p.category) + ) + .map((p) => ({ + ...p, + provider: p.id, + toolCount: mode === 'zero-tools' && p.id === 'allbirds-com' ? 0 : 2, + })); + else + rows = providers + .filter((p) => !(mode === 'empty-tools' && p.id === 'allbirds-com')) + .filter((p) => args.providers?.includes(p.id)) + .flatMap((p) => + ['products/offer', 'products/photos'].map((capability) => ({ + provider: p.id, + capability, + name: capability === 'products/offer' ? 'Offer' : 'Photos', + description: 'Sample tool description.', + creditsCost: 5, + perRecord: false, + ...(args.expand?.length + ? { + options: [ + { + name: 'asin', + type: 'string', + in: 'body', + about: 'Sample product identifier.', + }, + { + name: 'url', + type: 'string', + in: 'body', + about: 'Sample product URL.', + }, + ], + requiresOneOf: [['asin', 'url']], + response: { fields: [{ key: 'price', type: 'number' }] }, + } + : {}), + })) + ) + .filter( + (t) => + !args.capabilities?.length || args.capabilities.includes(t.capability) + ); + const offset = args.offset ?? 0, + limit = Math.min(args.limit ?? 100, 2); + const page = { + level: args.level, + items: rows.slice(offset, offset + limit), + total: rows.length, + catalogueVersion: 'fixture-v1', + ...(offset + limit < rows.length + ? { + nextTool: { + name: 'firecrawl_find_tools', + arguments: { ...args, offset: offset + limit }, + }, + } + : {}), + }; + const envelope = { + success: true, + data: { + creditsCost: 0, + alexandria: [ + { + provider: 'firecrawl', + capability: 'find-tools', + creditsCost: 0, + data: page, + }, + ], + }, + }; + return { + structuredContent: envelope, + content: [{ type: 'text', text: JSON.stringify(envelope) }], + }; +} diff --git a/scripts/verify-staging-mcp.mjs b/scripts/verify-staging-mcp.mjs new file mode 100644 index 00000000..6dcdd693 --- /dev/null +++ b/scripts/verify-staging-mcp.mjs @@ -0,0 +1,142 @@ +// Read-only staging checks. Reads FIRECRAWL_API_KEY from the environment or macOS Keychain. +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { createHash } from 'node:crypto'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +const endpoint = 'http://mcp-server-staging-service:8080/v2/mcp'; +const key = + process.env.FIRECRAWL_API_KEY || + execFileSync( + '/usr/bin/security', + [ + 'find-generic-password', + '-s', + 'FIRECRAWL_API_KEY', + ...(process.env.FIRECRAWL_KEYCHAIN_ACCOUNT + ? ['-a', process.env.FIRECRAWL_KEYCHAIN_ACCOUNT] + : []), + '-w', + ], + { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] } + ).trim(); +const client = new Client( + { name: 'firecrawl-staging-regression', version: '0.1.0' }, + { capabilities: {} } +); +const boundedFetch = (url, init) => + fetch(url, { + ...init, + redirect: 'error', + signal: AbortSignal.any([ + ...(init?.signal ? [init.signal] : []), + AbortSignal.timeout(20000), + ]), + }); +async function close(client) { + let timer; + try { + await Promise.race([ + client.close(), + new Promise((_, reject) => { + timer = setTimeout( + () => reject(new Error('MCP close timed out')), + 5000 + ); + }), + ]); + } finally { + clearTimeout(timer); + } +} +const transport = new StreamableHTTPClientTransport(new URL(endpoint), { + requestInit: { headers: { Authorization: `Bearer ${key}` } }, + fetch: boundedFetch, +}); +const report = { endpoint }; +try { + await client.connect(transport); + const tools = await client.listTools(); + const launcher = tools.tools.find( + (t) => t.name === 'firecrawl_usage_dashboard' + ); + assert.ok(launcher, 'Staging does not yet advertise the sidebar launcher'); + assert.deepEqual(launcher._meta['openai/ui'].entrypoints, [ + { type: 'global' }, + ]); + assert.deepEqual(launcher._meta.ui, { + resourceUri: 'ui://firecrawl/usage.html', + visibility: ['app'], + }); + assert.ok(launcher.icons[0].src.startsWith('data:image/svg+xml;base64,')); + report.toolCount = tools.tools.length; + report.launcherMetadata = 'passed'; + const resources = await client.listResources(); + assert.ok( + resources.resources.some((r) => r.uri === 'ui://firecrawl/usage.html') + ); + const resource = await client.readResource({ + uri: 'ui://firecrawl/usage.html', + }); + const html = resource.contents[0].text; + const expected = readFileSync( + new URL('../dist/usage.html', import.meta.url), + 'utf8' + ); + assert.equal(html, expected, 'Deployed UI differs from the tested branch'); + report.uiSha256 = createHash('sha256').update(html).digest('hex'); + report.uiMatchesBranch = true; + report.uiMetadata = resource.contents[0]._meta; + + for (const [name, args, label] of [ + ['firecrawl_usage_dashboard', {}, 'dashboard'], + ['firecrawl_credit_usage', { view: 'historical' }, 'historicalUsage'], + [ + 'firecrawl_find_tools', + { level: 'providers', limit: 5 }, + 'providerDiscovery', + ], + ]) { + const result = await client.callTool({ name, arguments: args }); + report[label] = { + isError: result.isError === true, + structuredKeys: Object.keys(result.structuredContent ?? {}), + }; + assert.notEqual(result.isError, true, `${label} failed`); + } + const invalid = new StreamableHTTPClientTransport(new URL(endpoint), { + requestInit: { + headers: { Authorization: 'Bearer fc-invalid-staging-regression' }, + }, + fetch: boundedFetch, + }); + const invalidClient = new Client( + { name: 'firecrawl-invalid-auth-regression', version: '0.1.0' }, + { capabilities: {} } + ); + try { + await invalidClient.connect(invalid); + const rejected = await invalidClient.callTool({ + name: 'firecrawl_credit_usage', + arguments: {}, + }); + assert.equal(rejected.isError, true); + assert.equal(rejected.structuredContent?.code, 'CREDENTIAL_INVALID'); + report.invalidAuth = 'CREDENTIAL_INVALID'; + } finally { + await close(invalidClient); + } + report.success = true; +} catch (e) { + report.success = false; + report.error = e.message.replaceAll(key, '[redacted]'); + process.exitCode = 1; +} finally { + await close(client).catch((error) => { + report.success = false; + report.error = error.message; + process.exitCode = 1; + }); + console.log(JSON.stringify(report, null, 2)); +} diff --git a/scripts/verify-usage-preview.mjs b/scripts/verify-usage-preview.mjs new file mode 100644 index 00000000..5adfec60 --- /dev/null +++ b/scripts/verify-usage-preview.mjs @@ -0,0 +1,619 @@ +import { execFileSync } from 'node:child_process'; + +// Run with the fixture preview already listening. No paid calls or real chats. +const url = process.argv[2] ?? 'http://127.0.0.1:4173'; +const session = 'firecrawl-provider-verification'; +function browser(...args) { + return execFileSync( + 'npx', + ['--yes', 'agent-browser@0.38.1', '--session', session, ...args], + { encoding: 'utf8', timeout: 60000 } + ); +} +async function verify() { + const doc = () => document.getElementById('view').contentDocument; + const get = (id) => doc()?.getElementById(id); + const latest = () => window.modelContexts.at(-1); + const selection = () => + latest()?.content.length + ? { + providers: latest().content.map((item) => + JSON.parse( + item.text.split('Selected Alexandria provider and tools:\n')[1] + ) + ), + } + : undefined; + const check = (condition, reason) => { + if (!condition) throw new Error(reason); + }; + const wait = async (predicate, reason = 'preview state') => { + const start = Date.now(); + while (!predicate()) { + if (Date.now() - start > 6000) + throw new Error('Timed out waiting for ' + reason); + await new Promise((resolve) => setTimeout(resolve, 40)); + } + }; + const change = async (id, value) => { + if (id === 'provider-category') { + get('provider-category').dispatchEvent( + new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }) + ); + await wait( + () => doc().querySelector(`[role="option"][data-category="${value}"]`), + 'category dropdown' + ); + doc().querySelector(`[role="option"][data-category="${value}"]`).click(); + await wait( + () => + get('provider-category').getAttribute('aria-expanded') === 'false', + 'category selected' + ); + return; + } + const input = get(id); + Object.getOwnPropertyDescriptor( + doc().defaultView.HTMLInputElement.prototype, + 'value' + ).set.call(input, value); + get(id).dispatchEvent( + new Event(id === 'provider-search' ? 'input' : 'change', { + bubbles: true, + }) + ); + }; + const choose = (id) => + get('provider-grid') + .querySelector(`[data-provider="${id}"] [role="checkbox"]`) + .click(); + const inspect = (id) => + get('provider-grid') + .querySelector(`[data-provider="${id}"] .provider-actions button.button`) + .click(); + const safeCalls = () => { + check(window.messages.length === 0, 'Selection must never send a message'); + check( + window.calls.every((c) => + ['firecrawl_find_tools', 'firecrawl_credit_usage'].includes(c.name) + ), + 'Browsing must not execute paid capabilities' + ); + }; + const load = async (mode = 'normal') => { + safeCalls(); + document.getElementById('scenario').value = mode; + const previous = doc(); + document.getElementById('reload').click(); + await wait( + () => + doc() !== previous && + get('providers-tab')?.getAttribute('aria-selected') === 'true', + 'usage' + ); + check( + !get('providers-view').hidden && get('usage-view').hidden, + 'Providers opens by default' + ); + check( + get('providers-tab').getAttribute('aria-selected') === 'true' && + get('usage-tab').getAttribute('aria-selected') === 'false', + 'Default tab matches visible view' + ); + await wait( + () => + !get('catalog-error').hidden || + get('provider-count').textContent === '6 providers', + 'catalog' + ); + check( + !doc().querySelector( + 'textarea, .selection-tray, #use-in-chat, #use-in-new-chat' + ), + 'No custom chat composer or sticky tray' + ); + }; + await load(); + check( + !window.calls.some((c) => c.name === 'firecrawl_credit_usage'), + 'Provider landing defers usage requests' + ); + get('providers-tab').focus(); + get('providers-tab').dispatchEvent( + new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }) + ); + await wait(() => !get('usage-view').hidden, 'keyboard tab switch'); + check( + doc().activeElement.id === 'usage-tab', + 'Tabs move keyboard focus with selection' + ); + get('usage-tab').dispatchEvent( + new KeyboardEvent('keydown', { key: 'Home', bubbles: true }) + ); + await wait(() => !get('providers-view').hidden, 'keyboard tabs return'); + get('provider-category').focus(); + get('provider-category').dispatchEvent( + new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }) + ); + await wait( + () => doc().querySelector('[role="listbox"]'), + 'keyboard dropdown open' + ); + doc() + .querySelector('[role="listbox"]') + .dispatchEvent( + new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }) + ); + await wait( + () => + !doc().querySelector('[role="listbox"]') && + doc().activeElement.id === 'provider-category', + 'dropdown escape restores focus' + ); + const logos = [...doc().querySelectorAll('.provider-logo')]; + logos.forEach((image) => { + image.loading = 'eager'; + }); + await wait( + () => logos.length === 6 && logos.every((image) => image.naturalWidth > 0), + 'bundled logos' + ); + check( + !get('results-tab') && !get('results-view'), + 'Providers and Usage only' + ); + check( + window.calls.filter( + (c) => + c.name === 'firecrawl_find_tools' && c.arguments.level === 'providers' + ).length === 3, + 'Catalog must follow all pages' + ); + choose('allbirds-com'); + inspect('amazon-com'); + await wait( + () => + get('provider-tools')?.querySelectorAll('[role="checkbox"]').length === 2 + ); + get('provider-tools') + .querySelector('[aria-label="Select tool Offer"]') + .click(); + get('provider-tools').querySelector('summary').focus(); + get('provider-tools').querySelector('summary').click(); + await wait(() => + get('provider-tools').querySelector('pre')?.textContent.includes('asin') + ); + check( + doc().activeElement.tagName === 'SUMMARY', + 'Contract inspection keeps keyboard focus' + ); + get('close-provider-dialog').click(); + await wait( + () => + selection()?.providers.length === 2 && + get('selection-status').textContent.includes('Added'), + 'mixed composer attachment' + ); + check( + latest() + .content.map((c) => c._meta['openai/title']) + .join(',') === 'Allbirds,Amazon US', + 'Provider names label native annotations' + ); + check( + !latest().structuredContent, + 'Removing a native chip must not leave a hidden duplicate of its context' + ); + check( + selection().providers[0].capabilities.length === 2 && + selection().providers[0].scope === 'provider', + 'Whole provider tool summaries' + ); + check( + selection().providers[1].capabilities.length === 1 && + selection().providers[1].capabilities[0].options[0].name === 'asin', + 'Exact subset and full contract' + ); + check( + latest().content.every( + (c) => + c.text.includes('firecrawl_scrape') && c.text.includes('contractLookup') + ), + 'Execution guidance reaches native chat context' + ); + await change('provider-search', 'PubMed'); + check( + get('provider-grid').children.length === 1, + 'Search finds provider from the last page' + ); + await change('provider-search', ''); + await change('provider-category', 'finance'); + await wait(() => get('provider-count').textContent === '1 provider'); + check( + get('selection-count').textContent === '2 selected', + 'Selections survive filters' + ); + get('clear-selection').click(); + await wait(() => latest()?.content.length === 0, 'clear context'); + check( + !latest().structuredContent && get('selection-controls').hidden, + 'Clearing removes context attachment and compact controls' + ); + document.getElementById('scenario').value = 'category-slow'; + window.discoveryResponses = []; + await change('provider-category', 'shopping'); + await change('provider-category', 'research'); + await wait( + () => + get('provider-count').textContent === '1 provider' && + get('provider-grid').textContent.includes('PubMed'), + 'latest category response' + ); + await wait( + () => window.discoveryResponses.includes('shopping'), + 'older category response' + ); + check( + window.discoveryResponses.indexOf('research') < + window.discoveryResponses.indexOf('shopping') && + get('provider-grid').textContent.includes('PubMed') && + !get('provider-grid').textContent.includes('Allbirds'), + 'Reversed category responses preserve latest filter' + ); + document.getElementById('scenario').value = 'normal'; + + await load('context-slow'); + choose('allbirds-com'); + // Change selection while the first host write is still awaiting acknowledgement. + await wait(() => window.calls.some((c) => c.arguments.level === 'tools')); + await new Promise((resolve) => setTimeout(resolve, 250)); + choose('amazon-com'); + choose('allbirds-com'); + await wait( + () => + selection()?.providers.length === 1 && + selection().providers[0].id === 'amazon-com' && + get('selection-status').textContent.includes('Added'), + 'latest serialized context' + ); + await new Promise((resolve) => setTimeout(resolve, 700)); + check( + selection().providers.length === 1 && + selection().providers[0].id === 'amazon-com', + 'Stale preparation and host acknowledgements cannot replace the latest selection' + ); + choose('amazon-com'); + await wait(() => latest()?.content.length === 0, 'deselection clear'); + + await load('catalog-error'); + check(!get('catalog-error').hidden, 'Catalog failure visible'); + document.getElementById('scenario').value = 'normal'; + get('catalog-retry').click(); + await wait(() => get('provider-count').textContent === '6 providers'); + document.getElementById('scenario').value = 'context-error'; + choose('allbirds-com'); + await wait(() => !get('retry-selection').hidden, 'context failure'); + check( + get('selection-status').textContent.includes('Could not update') && + get('selection-count').textContent === '1 selected', + 'Context error preserves local selection without claiming success' + ); + document.getElementById('scenario').value = 'normal'; + get('retry-selection').click(); + await wait( + () => + selection()?.providers.length === 1 && + get('selection-status').textContent.includes('Added'), + 'context retry' + ); + + await load('zero-tools'); + check( + get('provider-grid').querySelector( + '[data-provider="allbirds-com"] [role="checkbox"]' + ).disabled, + 'Known zero-tool provider cannot be selected' + ); + await load('empty-tools'); + choose('allbirds-com'); + await wait( + () => + get('provider-grid').querySelector( + '[data-provider="allbirds-com"] [role="checkbox"]' + ).disabled, + 'removed provider tools' + ); + check( + get('selection-controls').hidden && + get('retry-selection').hidden && + latest()?.content.length === 0, + 'Empty tool lookup removes selection and clears context without retry' + ); + + await load('tools-error'); + choose('allbirds-com'); + await wait( + () => !get('retry-selection').hidden, + 'selection preparation failure' + ); + inspect('amtrak-com'); + await wait(() => + get('provider-tools')?.textContent.includes('Could not load') + ); + document.getElementById('scenario').value = 'normal'; + get('provider-tools').querySelector('button').click(); + await wait( + () => + get('provider-tools')?.querySelectorAll('[role="checkbox"]').length === 2 + ); + document.getElementById('scenario').value = 'tools-error'; + get('provider-tools').querySelector('summary').click(); + await wait(() => + get('provider-tools') + .querySelector('pre') + ?.textContent.includes('Could not load') + ); + const details = get('provider-tools').querySelector('details'); + details.open = false; + await new Promise((resolve) => setTimeout(resolve, 100)); + document.getElementById('scenario').value = 'normal'; + details.open = true; + await wait(() => details.querySelector('pre')?.textContent.includes('asin')); + get('close-provider-dialog').click(); + get('retry-selection').click(); + await wait( + () => selection()?.providers[0].id === 'allbirds-com', + 'preparation retry' + ); + + await load('categories-error'); + await wait(() => get('category-note').textContent.includes('unavailable')); + check( + get('provider-grid').children.length === 6, + 'Category failure allows full catalog' + ); + await load('unsupported-message'); + choose('allbirds-com'); + await wait( + () => selection()?.providers.length === 1, + 'context without messaging support' + ); + await load('unsupported-context'); + check( + [...get('provider-grid').querySelectorAll('[role="checkbox"]')].every( + (input) => input.disabled + ), + 'Selection disabled without model-context capability' + ); + inspect('allbirds-com'); + await wait( + () => + get('provider-tools')?.querySelectorAll('[role="checkbox"]').length === 2 + ); + check( + [...get('provider-tools')?.querySelectorAll('[role="checkbox"]')].every( + (input) => input.disabled + ) && get('select-provider').disabled, + 'Tool selection also capability gated' + ); + check( + window.modelContexts.length === 0, + 'Unsupported hosts never receive context writes' + ); + + const usageMode = async (mode) => { + document.getElementById('scenario').value = mode; + const previous = doc(); + document.getElementById('reload').click(); + // Catalog results confirm React is ready; the initial shell has inert tabs. + await wait( + () => + doc() !== previous && + get('usage-tab') && + get('provider-count')?.textContent === '6 providers', + 'usage scenario ' + mode + ); + get('usage-tab').click(); + await wait( + () => + !get('usage-view').hidden && + get('balance-section').getAttribute('aria-busy') === 'false' && + get('usage-section').getAttribute('aria-busy') === 'false' && + window.calls.some((c) => c.arguments.view === 'historical'), + 'usage loaded' + ); + }; + for (const [mode, remaining, columns, balanceError, historyError] of [ + ['normal', '8,750', 3, false, false], + ['extra', '1,250', 3, false, false], + ['zero', '0', 3, false, false], + ['empty', '8,750', 0, false, false], + ['metadata', '8,750', 3, false, false], + ['balance-error', '—', 3, true, false], + ['history-error', '8,750', 0, false, true], + ['error', '—', 0, true, true], + ]) { + await usageMode(mode); + check(get('remaining').textContent === remaining, mode + ' balance'); + check( + get('history').querySelectorAll('.month').length === columns, + mode + ' monthly usage' + ); + check( + get('balance-error').hidden === !balanceError, + mode + ' balance error' + ); + check( + get('history-error').hidden === !historyError, + mode + ' history error' + ); + if (mode === 'extra') + check( + get('plan').textContent === '1,000', + 'Top-ups do not inflate plan allowance' + ); + if (mode === 'zero') + check( + get('history').querySelectorAll('.bar[hidden]').length === 1, + 'Zero usage has no filled bar' + ); + if (mode === 'empty') + check( + get('history').textContent.includes('No usage history yet'), + 'Empty history' + ); + if (mode === 'metadata') + check( + get('billing').textContent === 'Unavailable', + 'Missing billing dates' + ); + } + await usageMode('normal'); + await usageMode('normal'); + document.getElementById('scenario').value = 'error'; + get('refresh').click(); + await wait( + () => !get('balance-error').hidden && !get('history-error').hidden, + 'failed refresh' + ); + check( + get('remaining').textContent === '—' && + !get('history').querySelector('.month'), + 'Failed refresh clears stale data' + ); + document.getElementById('scenario').value = 'normal'; + get('refresh').click(); + await wait( + () => get('remaining').textContent === '8,750', + 'refresh recovery' + ); + const previousWidth = document.getElementById('width').value; + document.getElementById('width').value = '320'; + document.getElementById('width').dispatchEvent(new Event('change')); + check( + get('usage-section').scrollWidth <= get('usage-section').clientWidth && + get('history').scrollWidth <= get('history').clientWidth, + 'Monthly chart fits narrow screens' + ); + document.getElementById('width').value = previousWidth; + document.getElementById('width').dispatchEvent(new Event('change')); + get('open-dashboard').click(); + await wait(() => window.lastOpenedUrl, 'dashboard link'); + check( + window.lastOpenedUrl === 'https://www.firecrawl.dev/app/usage', + 'Production dashboard destination' + ); + const usageWidth = doc() + .querySelector('.dashboard') + .getBoundingClientRect().width; + get('providers-tab').click(); + await wait( + () => get('provider-count').textContent === '6 providers', + 'providers for visual checks' + ); + check( + doc().querySelector('.dashboard').getBoundingClientRect().width === + usageWidth, + 'Consistent Providers and Usage width' + ); + inspect('allbirds-com'); + await wait( + () => get('provider-dialog')?.getAttribute('data-state') === 'open', + 'provider modal' + ); + for (const mode of ['dark', 'light']) { + const control = document.getElementById('theme'); + control.value = mode; + control.dispatchEvent(new Event('change')); + await wait( + () => doc().documentElement.dataset.theme === mode, + 'live theme change' + ); + const style = doc().defaultView.getComputedStyle( + doc().querySelector('[data-state="open"].backdrop-blur-md') + ); + const canvas = doc().createElement('canvas'); + canvas.width = canvas.height = 1; + const context = canvas.getContext('2d'); + context.fillStyle = style.backgroundColor; + context.fillRect(0, 0, 1, 1); + const [red, , , alpha] = context.getImageData(0, 0, 1, 1).data; + check(mode === 'dark' ? red < 30 : red > 230, mode + ' modal backdrop'); + check( + alpha > 195 && alpha < 210 && style.backdropFilter === 'blur(12px)', + 'Firecrawl modal overlay' + ); + } + document.getElementById('view').style.height = '420px'; + const toolsPane = get('provider-tools'); + await wait( + () => toolsPane.scrollHeight > toolsPane.clientHeight, + 'dialog scroll region' + ); + const footer = get('provider-dialog').querySelector('.dialog-footer'); + await wait( + () => + doc().defaultView.innerHeight === 420 && + footer.getBoundingClientRect().bottom <= 420, + 'Dialog actions fit a short viewport' + ); + toolsPane.scrollTop = toolsPane.scrollHeight; + check( + footer.getBoundingClientRect().bottom <= 420, + 'Dialog actions remain visible while scrolling' + ); + get('provider-dialog').dispatchEvent( + new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }) + ); + await wait(() => !get('provider-dialog'), 'dialog closes on Escape'); + await wait( + () => doc().activeElement?.textContent === 'View tools', + 'dialog restores focus' + ); + document.getElementById('view').style.height = '940px'; + document.getElementById('width').value = '320'; + document.getElementById('width').dispatchEvent(new Event('change')); + check( + doc().documentElement.scrollWidth <= 320, + 'No horizontal page overflow on mobile' + ); + get('usage-tab').click(); + check(doc().documentElement.scrollWidth <= 320, 'Usage fits mobile width'); + safeCalls(); + check(window.previewErrors.length === 0, 'No preview JavaScript errors'); + return { + passed: true, + checks: [ + 'zero-tool and empty-tool providers', + 'reversed category responses', + 'providers opens by default', + 'keyboard tabs, dropdown Escape and focus restoration', + 'short viewport dialog scrolling and visible actions', + 'pagination and search', + 'category races', + 'mixed named context annotations', + 'exact contracts', + 'replacement and clearing', + 'concurrent selection', + 'context and discovery retries', + 'host capability gates', + 'no message or paid execution', + 'no custom chatbox or sticky tray', + 'all usage states and partial failures', + 'refresh clearing and recovery', + 'production dashboard link', + 'consistent tab widths and mobile layout', + 'live theme changes and modal backdrops', + ], + }; +} + +try { + browser('open', url); + console.log(browser('eval', '(' + verify.toString() + ')()')); +} catch (error) { + console.error(error.stderr || error.message); + process.exitCode = 1; +} finally { + browser('close'); +} diff --git a/src/index.ts b/src/index.ts index c11fa946..e3196a50 100644 --- a/src/index.ts +++ b/src/index.ts @@ -63,6 +63,7 @@ import { checkKeylessSignupUrl } from './keyless-signup-link'; import { registerMonitorTools } from './monitor'; import { registerResearchTools } from './research'; import { registerUsageTools } from './usage'; +import { registerUsageDashboard } from './usage-ui'; import { escapeWWWAuthenticateValue } from './www-authenticate'; import { createIntrospectionCache, @@ -4394,6 +4395,9 @@ registerMonitorTools(server); registerResearchTools(server, getClient); registerDeveloperTools(server, getClient); registerUsageTools(server, getClient); +if (primaryProfile.id !== 'search') { + registerUsageDashboard(server, getClient); +} if ( process.env.CLOUD_SERVICE === 'true' && diff --git a/src/usage-ui.ts b/src/usage-ui.ts new file mode 100644 index 00000000..c2120664 --- /dev/null +++ b/src/usage-ui.ts @@ -0,0 +1,82 @@ +import { readFile } from 'node:fs/promises'; +import { readFileSync } from 'node:fs'; +import type { FastMCP } from 'fastmcp'; +import { z } from 'zod'; +import { originHeaders, requestOrigin, type McpClient } from './origin'; +import { readCurrentUsage } from './usage'; +import { creditUsageOutputSchema, structuredText } from './tool-output'; + +interface SessionData { + firecrawlApiKey?: string; + clientUserAgent?: string; + [key: string]: unknown; +} + +const resourceUri = 'ui://firecrawl/usage.html'; +const launcherUi = { resourceUri, visibility: ['app' as const] }; + +export function registerUsageDashboard( + server: Pick, 'addTool' | 'addResource'>, + getClient: (session?: SessionData) => unknown +): void { + server.addResource({ + uri: resourceUri, + name: 'Firecrawl usage dashboard', + description: + 'Alexandria provider browser, account credit balance, and monthly usage in the Firecrawl design system.', + mimeType: 'text/html;profile=mcp-app', + load: async () => ({ + text: await readFile(new URL('./usage.html', import.meta.url), 'utf8'), + _meta: { + ui: { + domain: 'https://mcp.firecrawl.dev', + csp: { connectDomains: [], resourceDomains: [] }, + }, + 'openai/widgetDescription': + 'Browse Alexandria providers, select their tools for the native chat composer, and view Firecrawl account credits and monthly usage.', + 'openai/widgetCSP': { + redirect_domains: ['https://www.firecrawl.dev'], + }, + 'openai/ui': { + preferredDisplayMode: 'fullscreen', + availableDisplayModes: ['fullscreen'], + }, + }, + }), + }); + + server.addTool({ + name: 'firecrawl_usage_dashboard', + icons: [ + { + src: `data:image/svg+xml;base64,${Buffer.from(readFileSync(new URL('./firecrawl-sidebar.svg', import.meta.url))).toString('base64')}`, + mimeType: 'image/svg+xml', + sizes: ['any'], + }, + ], + description: + 'Open the Firecrawl sidebar app to browse Alexandria providers, select tools for a chat, and see credits remaining, plan credits, billing dates, and monthly credit usage. Requires a connected Firecrawl account. Use firecrawl_credit_usage for data-only reporting.', + annotations: { + title: 'Firecrawl usage', + readOnlyHint: true, + destructiveHint: false, + openWorldHint: false, + }, + _meta: { + ui: launcherUi, + 'openai/ui': { entrypoints: [{ type: 'global' }] }, + }, + parameters: z.object({}), + outputSchema: creditUsageOutputSchema, + execute: async (_args, { session, client }) => { + const headers = originHeaders( + requestOrigin(client as McpClient, session) + ); + const usage = await readCurrentUsage( + getClient(session) as Parameters[0], + headers + ); + return structuredText(usage); + }, + }); +} diff --git a/src/usage.ts b/src/usage.ts index 849e7329..f0f24ae9 100644 --- a/src/usage.ts +++ b/src/usage.ts @@ -59,6 +59,32 @@ function assertSuccessful( throw new Error(response.error || `Failed to ${operation}`); } +interface CurrentCreditUsage { + remainingCredits: number; + planCredits?: number; + billingPeriodStart: string | null; + billingPeriodEnd: string | null; +} + +export async function readCurrentUsage( + client: UsageClient, + headers: Record +): Promise { + const response = await client.http.get( + '/v2/team/credit-usage', + headers + ); + assertSuccessful(response.status, response.data, 'get credit usage'); + const data = response.data.data ?? response.data; + return { + remainingCredits: data.remainingCredits ?? data.remaining_credits ?? 0, + planCredits: data.planCredits ?? data.plan_credits, + billingPeriodStart: + data.billingPeriodStart ?? data.billing_period_start ?? null, + billingPeriodEnd: data.billingPeriodEnd ?? data.billing_period_end ?? null, + }; +} + export function registerUsageTools( server: Pick, 'addTool'>, getClient: GetClient @@ -108,7 +134,9 @@ The historical view returns periods sorted by start date with \`startDate\`, \`e } const client = getClient(session) as UsageClient; - const headers = originHeaders(requestOrigin(mcpClient as McpClient, session)); + const headers = originHeaders( + requestOrigin(mcpClient as McpClient, session) + ); const historical = view === 'historical' || byApiKey === true; if (historical) { @@ -125,21 +153,7 @@ The historical view returns periods sorted by start date with \`startDate\`, \`e return structuredText(response.data); } - const response = await client.http.get( - '/v2/team/credit-usage', - headers - ); - assertSuccessful(response.status, response.data, 'get credit usage'); - const data = response.data.data ?? response.data; - return structuredText({ - remainingCredits: - data.remainingCredits ?? data.remaining_credits ?? 0, - planCredits: data.planCredits ?? data.plan_credits, - billingPeriodStart: - data.billingPeriodStart ?? data.billing_period_start ?? null, - billingPeriodEnd: - data.billingPeriodEnd ?? data.billing_period_end ?? null, - }); + return structuredText(await readCurrentUsage(client, headers)); }, }); } diff --git a/staging-testing.md b/staging-testing.md new file mode 100644 index 00000000..9aeb2c13 --- /dev/null +++ b/staging-testing.md @@ -0,0 +1,104 @@ +# Providers and Usage staging testing + +The staging deployment workflow is +[image-staging.yml](https://github.com/firecrawl/firecrawl-mcp-server/actions/workflows/image-staging.yml). +Run it against `codex/mcp-providers-usage`: + +```sh +gh workflow run image-staging.yml --ref codex/mcp-providers-usage +``` + +It publishes `ghcr.io/firecrawl/firecrawl-mcp-server-alpha:latest`. +Keel polls this tag every minute and rolls out both `scalable` and `sse` +in the `mcp-server-staging` namespace. A successful workflow only confirms +image publication; check both rollouts and the running image digest separately. +These are shared staging services. Production deployments are separate. + +```sh +kubectl -n mcp-server-staging rollout status deployment/scalable +kubectl -n mcp-server-staging rollout status deployment/sse +kubectl -n mcp-server-staging get pods -o custom-columns='NAME:.metadata.name,READY:.status.containerStatuses[*].ready,IMAGE_ID:.status.containerStatuses[*].imageID' +``` + +## Live server checks + +With Tailscale connected, the streamable HTTP endpoint is: + +```text +http://mcp-server-staging-service:8080/v2/mcp +``` + +Build the branch, then run: + +```sh +pnpm build +node scripts/verify-staging-mcp.mjs +``` + +The script reads `FIRECRAWL_API_KEY` from the environment, or the existing +macOS Keychain item with that service name. It never prints the key or account +credit values. It verifies the native sidebar entrypoint and icon, compares +the deployed UI bytes against `dist/usage.html`, and checks current credits, +historical usage, free provider discovery, and invalid-key recovery. It does +not execute provider capabilities, scrape pages, or send chat messages. +Discovery accepts API keys before Core validates them; invalid keys are +rejected at tool execution with `CREDENTIAL_INVALID`, rather than at initialize. + +## Manual app acceptance + +For a quick native host check in the Codex app, refresh the already installed +**Firecrawl Usage Dev** plugin and start a fresh chat. That development package +runs this checkout through its Keychain/stdio launcher. It tests the host UI +against the branch locally; it does not connect to the staged container. + +1. Open the Firecrawl sidebar entry and pin it. Check the Firecrawl icon. +2. Browse providers, search, filter categories, and check logos and fallbacks. +3. Select a provider, then specific tools from another provider. Check the + native composer context chips, without an automatic chat message. +4. Remove a chip and verify the next prompt no longer carries that selection. +5. Type a task in the native composer and verify the model knows the selected + provider and tool IDs. Provider execution may consume credits; a context-only + prompt such as "List the providers and tools I selected, without running them" + verifies handoff without executing those capabilities. +6. Open Usage, refresh it, and check the balance/history. Compare the width with + Providers and inspect a tool dialog in light and dark themes. + +For a **ChatGPT Developer Mode check of the deployed staging server**, the +private Tailscale URL requires a Secure MCP Tunnel (or an approved public HTTPS +test endpoint). Entering that private HTTP URL as a public server does not +make it reachable by ChatGPT. See OpenAI's +[connection guide](https://developers.openai.com/plugins/deploy/connect-chatgpt) +and [Secure MCP Tunnel setup](https://developers.openai.com/api/docs/guides/secure-mcp-tunnels). + +Create/select a tunnel in +[Platform tunnel settings](https://platform.openai.com/settings/organization/tunnels), +associate it with the ChatGPT workspace, and run the official `tunnel-client` +on a machine with Tailscale access to the staging endpoint. The client needs +its own OpenAI runtime key as `CONTROL_PLANE_API_KEY` and the existing Firecrawl +key for upstream authorization. The values stay in process environment variables. + +```sh +brew install openai/tools/tunnel-client +read -r -s "CONTROL_PLANE_API_KEY?OpenAI runtime key: " +export CONTROL_PLANE_API_KEY +tunnel-client init --sample sample_mcp_with_dcr --profile firecrawl-staging \ + --tunnel-id YOUR_TUNNEL_ID \ + --mcp-server-url http://mcp-server-staging-service:8080/v2/mcp +export FIRECRAWL_STAGING_AUTH="Bearer $(security find-generic-password -s FIRECRAWL_API_KEY -w)" +tunnel-client run --profile firecrawl-staging \ + --mcp.extra-headers 'Authorization: env:FIRECRAWL_STAGING_AUTH' \ + --mcp.discovery-extra-headers 'Authorization: env:FIRECRAWL_STAGING_AUTH' +``` + +Keep the client running. In ChatGPT, enable Developer Mode under +**Settings → Security and login**, open **Plugins → +**, choose **Tunnel**, +and select that tunnel. Use the API-key-backed tunnel connection for this test; +the current staging service runs the `/v2/mcp` profile, not the production +`/v2/mcp-oauth` profile. Refresh connection metadata, start a new conversation, +and repeat the sidebar/composer checklist above. Verify the native chips, +pinning, and tool selection in the actual host; local preview and wire checks +do not substitute for this acceptance step. + +The published production package remains on the production OAuth endpoint. +Do not replace the published Firecrawl connection with this staging test. +ZIP assembly, reviewer cases, review credentials, and video are separate steps. diff --git a/tests/build-bundle.test.mjs b/tests/build-bundle.test.mjs index 715db6dd..9a547368 100644 --- a/tests/build-bundle.test.mjs +++ b/tests/build-bundle.test.mjs @@ -9,6 +9,21 @@ test('dist bundles the patched fastmcp instead of importing it', async () => { assert.doesNotMatch(dist, /from\s+["']fastmcp["']/); assert.match(dist, /title: tool\.annotations\.title/); assert.match(dist, /tool\.canList/); + assert.match(dist, /icons: tool\.icons/); +}); + +test('npm package includes the self-contained usage resource', async () => { + const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')); + assert.ok(pkg.files.includes('dist')); + const html = await readFile(new URL('../dist/usage.html', import.meta.url), 'utf8'); + assert.match(html, /ui\/initialize/); + assert.match(html, /firecrawl_credit_usage/); + assert.match(html, /--heat-100: #fa5d19ff/); + assert.doesNotMatch(html, /]+src=|]+href=/); + const styles = [...html.matchAll(/]*>([\s\S]*?)<\/style>/gi)].map(([, css]) => css).join('\n'); + assert.doesNotMatch(styles, /@import\s|url\(\s*["']?(?:https?:|\/\/)/i); + assert.doesNotMatch(html, /<(?:img|source)\b[^>]*(?:src|srcset)\s*=\s*["']?(?:https?:|\/\/)/i); + assert.doesNotMatch(html, /Fixture account request failed|Local preview · Sample data/); }); test('every package the bundled fastmcp imports is a direct dependency', async () => { diff --git a/tests/dev-launcher.test.mjs b/tests/dev-launcher.test.mjs new file mode 100644 index 00000000..cdccad93 --- /dev/null +++ b/tests/dev-launcher.test.mjs @@ -0,0 +1,48 @@ +import test from 'node:test'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +const launcher = fileURLToPath(new URL('../plugins/openai/firecrawl-usage-dev/scripts/run-keychain.py', import.meta.url)); + +test('development launcher resolves relative paths and supports Intel Node without exposing credentials', () => { + execFileSync('python3', ['-c', ` +import os, runpy, subprocess, tempfile +from pathlib import Path +from unittest.mock import patch +secret = 'fc-synthetic-launcher-test' +with tempfile.TemporaryDirectory() as root: + worktree = (Path(root) / 'relative').resolve() + (worktree / 'dist').mkdir(parents=True) + (worktree / 'dist/index.js').write_text('') + original = Path.is_file + def exists(path): + return str(path) == '/usr/local/bin/node' if str(path) in ['/opt/homebrew/bin/node','/usr/local/bin/node'] else original(path) + with patch.dict(os.environ, {'FIRECRAWL_MCP_WORKTREE':'relative', 'FIRECRAWL_KEYCHAIN_ACCOUNT':'selected-account'}), patch('shutil.which', return_value=None), patch.object(Path,'is_file',exists), patch('os.access',return_value=True), patch('subprocess.run',return_value=subprocess.CompletedProcess([],0,stdout=secret,stderr='')) as security, patch('os.execve') as execute: + os.chdir(root) + runpy.run_path(${JSON.stringify(launcher)}) + assert execute.call_args.args[0] == '/usr/local/bin/node' + assert execute.call_args.args[1][1] == str(worktree / 'dist/index.js') + assert os.getcwd() == str(worktree) + assert '-a' in security.call_args.args[0] + assert 'selected-account' in security.call_args.args[0] + assert all(secret not in arg for arg in security.call_args.args[0]) + executable, argv, env = execute.call_args.args + assert secret not in executable + assert all(secret not in arg for arg in argv) + assert env['FIRECRAWL_API_KEY'] == secret + assert all(secret not in value for name, value in env.items() if name != 'FIRECRAWL_API_KEY') +`], {stdio:'pipe'}); +}); + +test('development launcher gives a controlled error when Node cannot be found', () => { + execFileSync('python3', ['-c', ` +import io, runpy +from contextlib import redirect_stderr +from pathlib import Path +from unittest.mock import patch +with patch('shutil.which',return_value=None), patch.object(Path,'is_file',return_value=False), redirect_stderr(io.StringIO()) as error: + try: runpy.run_path(${JSON.stringify(launcher)}) + except SystemExit as exit: assert exit.code == 1 + else: raise AssertionError('missing Node was not rejected') + assert 'install Node.js 22' in error.getvalue() +`], {stdio:'pipe'}); +}); diff --git a/tests/mcp-search-profile.test.mjs b/tests/mcp-search-profile.test.mjs index 08556f19..10e188bf 100644 --- a/tests/mcp-search-profile.test.mjs +++ b/tests/mcp-search-profile.test.mjs @@ -39,6 +39,7 @@ const SEARCH_TOOLS = [ // A representative sample of the full-surface tools that must NOT leak here. const EXCLUDED_TOOLS = [ + 'firecrawl_usage_dashboard', 'firecrawl_map', 'firecrawl_crawl', 'firecrawl_check_crawl_status', diff --git a/tests/mcp-smoke.test.mjs b/tests/mcp-smoke.test.mjs index 2e280ead..c970747c 100644 --- a/tests/mcp-smoke.test.mjs +++ b/tests/mcp-smoke.test.mjs @@ -1216,6 +1216,8 @@ test('stdio transport initializes and lists Firecrawl tools', async (t) => { assert.ok(toolNames.includes('firecrawl_search')); assert.ok(toolNames.includes('firecrawl_parse')); assert.ok(toolNames.includes('firecrawl_credit_usage')); + assert.ok(toolNames.includes('firecrawl_usage_dashboard')); + assert.equal(toolNames.some((name) => name.startsWith('firecrawl_results_')), false); assert.equal(toolNames.includes('firecrawl_credit_usage_historical'), false); assert.equal(toolNames.includes('firecrawl_extract'), false); // Codex tool search indexes the top-level Tool.title, not annotations.title. @@ -1241,6 +1243,19 @@ test('stdio transport initializes and lists Firecrawl tools', async (t) => { ); const byName = new Map(tools.tools.map((tool) => [tool.name, tool])); + assert.deepEqual(byName.get('firecrawl_usage_dashboard')._meta, { + ui: { resourceUri: 'ui://firecrawl/usage.html', visibility: ['app'] }, + 'openai/ui': { entrypoints: [{ type: 'global' }] }, + }); + assert.equal(byName.get('firecrawl_usage_dashboard').annotations.readOnlyHint, true); + const [sidebarIcon] = byName.get('firecrawl_usage_dashboard').icons; + assert.equal(sidebarIcon.mimeType, 'image/svg+xml'); + assert.deepEqual(sidebarIcon.sizes, ['any']); + assert.match(sidebarIcon.src, /^data:image\/svg\+xml;base64,/); + const svg = Buffer.from(sidebarIcon.src.split(',')[1], 'base64').toString('utf8'); + assert.match(svg, /xmlns="http:\/\/www.w3.org\/2000\/svg"/); + assert.match(svg, /fill="#fa5d19"/); + assert.match(svg, /viewBox="0 0 128 128"/); for (const name of ['firecrawl_search', 'firecrawl_scrape']) { assert.equal(byName.get(name)?._meta?.['anthropic/alwaysLoad'], true, name); } @@ -1446,6 +1461,38 @@ test('credit usage tool exposes current balance and both historical request shap remainingCredits: 1250, }); + const dashboard = await client.request('tools/call', { + arguments: {}, + name: 'firecrawl_usage_dashboard', + }); + assert.notEqual(dashboard.isError, true); + assert.deepEqual(dashboard.structuredContent, current.structuredContent); + + const resources = await client.request('resources/list'); + const resource = resources.resources.find((item) => item.uri === 'ui://firecrawl/usage.html'); + assert.equal(resource.mimeType, 'text/html;profile=mcp-app'); + const loaded = await client.request('resources/read', { uri: resource.uri }); + assert.equal(loaded.contents[0].mimeType, 'text/html;profile=mcp-app'); + assert.deepEqual(loaded.contents[0]._meta.ui.csp, { + connectDomains: [], resourceDomains: [], + }); + assert.equal(loaded.contents[0]._meta.ui.domain, 'https://mcp.firecrawl.dev'); + assert.deepEqual(loaded.contents[0]._meta['openai/widgetCSP'], { + redirect_domains: ['https://www.firecrawl.dev'], + }); + assert.match(loaded.contents[0]._meta['openai/widgetDescription'], /Alexandria.*composer.*credits/); + assert.deepEqual(loaded.contents[0]._meta['openai/ui'], { + preferredDisplayMode: 'fullscreen', + availableDisplayModes: ['fullscreen'], + }); + assert.match(loaded.contents[0].text, /

Explore providers<\/h1>/); + assert.match(loaded.contents[0].text, /id="providers-view"/); + assert.doesNotMatch(loaded.contents[0].text, /id="results-(?:tab|view)"|firecrawl_results_/); + assert.match(loaded.contents[0].text, /id="selection-status"/); + assert.doesNotMatch(loaded.contents[0].text, /provider-prompt|use-in-new-chat|selection-tray/); + assert.match(loaded.contents[0].text, /data:font\/woff2;base64,/); + assert.equal(//.test(loaded.contents[0].text), false); + const historical = await client.request('tools/call', { arguments: { view: 'historical' }, name: 'firecrawl_credit_usage', @@ -2371,6 +2418,17 @@ test('HTTP cloud keyless blocks account-only tools instead of continuing keyless assert.doesNotMatch(result.content[0].text, /remain available/); assert.doesNotMatch(result.content[0].text, /continue with those/); assert.equal(backend.requests.some((r) => r.url === '/v2/crawl'), false); + + const dashboardResponse = await httpToolCall(port, { + id: 'keyless-dashboard', + headers: { 'x-forwarded-for': '8.8.8.7' }, + params: { arguments: {}, name: 'firecrawl_usage_dashboard' }, + }); + assertKeylessAccountRecovery(parseSseJson(await dashboardResponse.text()).result, { + code: 'KEYLESS_TOOL_NOT_AVAILABLE', + message: KEYLESS_TOOL_MESSAGE, + }); + assert.equal(backend.requests.some((r) => r.url === '/v2/team/credit-usage'), false); }); test('HTTP cloud keyless quota stays 200 isError without inlining retry_after_seconds', async (t) => { diff --git a/tests/openai-submission.test.mjs b/tests/openai-submission.test.mjs new file mode 100644 index 00000000..9580963c --- /dev/null +++ b/tests/openai-submission.test.mjs @@ -0,0 +1,61 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, readFile, readdir, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import test from 'node:test'; +import { pluginSource, prepareOpenAIPlugin } from '../scripts/prepare-openai-plugin.mjs'; + +const json = async (path) => JSON.parse(await readFile(path, 'utf8')); +const files = async (path, prefix = '') => (await Promise.all( + (await readdir(path, { withFileTypes: true })).map(async (entry) => { + const name = prefix + entry.name; + assert(!entry.isSymbolicLink(), 'Submission files must be self-contained'); + return entry.isDirectory() ? files(join(path, entry.name), name + '/') : [name]; + }) +)).flat().sort(); + +test('production submission preserves Firecrawl metadata and every skill/reference', async (t) => { + const root = await mkdtemp(join(tmpdir(), 'firecrawl-submission-')); + t.after(() => rm(root, { recursive: true, force: true })); + const destination = await prepareOpenAIPlugin(join(root, 'nested', 'output', 'plugin')); + const source = await json(join(pluginSource, '.codex-plugin/plugin.json')); + const portable = await json(join(destination, 'plugin.json')); + const overlay = await json(join(destination, '.codex-plugin/plugin.json')); + assert.equal(portable.name, 'app-6a314a73f8ac819195b0d55e36b9c609'); + assert.equal(portable.version, '2.2.1'); + assert.equal(portable.description, source.description); + assert.deepEqual(portable.author, source.author); + assert.deepEqual(portable.extensions['com.openai'].interface, source.interface); + assert.deepEqual(overlay.interface, source.interface); + assert.equal(source.interface.shortDescription, 'Search and access more data'); + assert.equal(source.interface.supportURL, 'https://www.firecrawl.dev/support'); + assert.equal(source.interface.defaultPrompt.length, 3); + const sourceFiles = await files(join(pluginSource, 'skills')); + assert.deepEqual(await files(join(destination, 'skills')), sourceFiles); + for (const file of sourceFiles) { + assert.deepEqual(await readFile(join(destination, 'skills', file)), await readFile(join(pluginSource, 'skills', file))); + } + const included = await files(destination); + assert(!included.includes('.app.json')); + assert(!included.some((path) => /keychain|\.py$|\.env|results|usage-dev/.test(path))); + assert.equal(overlay.apps, undefined); + assert.equal(overlay.mcpServers, './.mcp.json'); + assert.deepEqual(await json(join(destination, 'mcp.json')), { + $schema: 'https://agent-plugins.org/schemas/1.0.0/mcp.schema.json', + mcpServers: { firecrawl: { type: 'streamable-http', url: 'https://mcp.firecrawl.dev/v2/mcp-oauth' } }, + }); + assert.deepEqual(await json(join(destination, '.mcp.json')), { + mcpServers: { firecrawl: { url: 'https://mcp.firecrawl.dev/v2/mcp-oauth' } }, + }); + for (const key of ['logo', 'composerIcon']) { + const icon = await readFile(join(destination, source.interface[key]), 'utf8'); + assert.match(icon, /width="128" height="128" viewBox="0 0 128 128"/); + } +}); + +test('preparation refuses to overwrite an existing directory', async (t) => { + const root = await mkdtemp(join(tmpdir(), 'firecrawl-submission-')); + t.after(() => rm(root, { recursive: true, force: true })); + await assert.rejects(prepareOpenAIPlugin(root), { code: 'EEXIST' }); + assert.deepEqual(await readdir(root), []); +}); diff --git a/tests/plugin-packages.test.mjs b/tests/plugin-packages.test.mjs index 52b49111..3d154cc3 100644 --- a/tests/plugin-packages.test.mjs +++ b/tests/plugin-packages.test.mjs @@ -69,6 +69,7 @@ test('OpenAI package uses the registered Firecrawl app connection', () => { apps: { 'app-6a314a73f8ac819195b0d55e36b9c609': { id: 'asdk_app_6a314a73f8ac819195b0d55e36b9c609', + required: true, }, }, }); diff --git a/tests/usage-catalog.test.mjs b/tests/usage-catalog.test.mjs new file mode 100644 index 00000000..079f8acc --- /dev/null +++ b/tests/usage-catalog.test.mjs @@ -0,0 +1,188 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { transform } from 'esbuild'; +import { previewCatalog } from '../scripts/usage-preview-fixtures.mjs'; +const { code } = await transform( + await readFile(new URL('../web/catalog.ts', import.meta.url), 'utf8'), + { loader: 'ts', format: 'esm' } +); +const { + catalogPage, + nextPage, + providerFrom, + capabilityFrom, + buildSelectionContext, +} = await import( + 'data:text/javascript;base64,' + Buffer.from(code).toString('base64') +); + +test('catalog follows discovery pagination and rejects failures or executable navigation', () => { + let args = { level: 'providers', limit: 100 }, + rows = []; + while (args) { + const page = catalogPage(previewCatalog(args, 'normal'), 'providers'); + rows.push(...page.items); + args = nextPage(page, 'providers'); + } + assert.equal(rows.length, 6); + assert.equal(providerFrom(rows.at(-1)).id, 'pubmed-ncbi-nlm-nih-gov'); + assert.throws( + () => + catalogPage( + previewCatalog({ level: 'providers' }, 'catalog-error'), + 'providers' + ), + /Catalog request failed/ + ); + assert.throws( + () => + catalogPage(previewCatalog({ level: 'tools' }, 'normal'), 'providers'), + /Invalid catalog page/ + ); + assert.throws( + () => + nextPage( + { + nextTool: { + name: 'firecrawl_scrape', + arguments: { level: 'providers', offset: 2 }, + }, + }, + 'providers' + ), + /Invalid catalog navigation/ + ); + assert.throws( + () => + nextPage( + { + nextTool: { + name: 'firecrawl_find_tools', + arguments: { level: 'tools', offset: 2 }, + }, + }, + 'providers' + ), + /Invalid catalog pagination/ + ); + assert.throws( + () => + nextPage( + { + nextTool: { + name: 'firecrawl_find_tools', + arguments: { level: 'providers', offset: -1 }, + }, + }, + 'providers' + ), + /Invalid catalog pagination/ + ); + const textOnly = previewCatalog({ level: 'categories' }, 'normal'); + delete textOnly.structuredContent; + assert.equal(catalogPage(textOnly, 'categories').items[0].id, 'shopping'); +}); + +test('composer context preserves exact IDs, selected contracts and prices for mixed selection', () => { + const provider = { + id: 'amazon-com', + name: 'Amazon US', + description: 'Products', + }; + const args = { + level: 'tools', + providers: [provider.id], + capabilities: ['products/offer'], + expand: ['options', 'response'], + }; + const row = catalogPage(previewCatalog(args, 'normal'), 'tools').items[0]; + const capability = capabilityFrom(row, provider.id); + assert.throws( + () => capabilityFrom(row, 'other-provider'), + /Invalid capability identity/ + ); + const all = { id: 'allbirds-com', name: 'Allbirds', description: 'Shoes' }; + const { content, context } = buildSelectionContext([ + { provider, capabilities: [capability], scope: 'tools' }, + { + provider: all, + capabilities: [ + { + provider: all.id, + capability: 'products/offer', + name: 'Offer', + description: 'Sample', + creditsCost: 5, + }, + ], + scope: 'provider', + }, + ]); + assert.equal(content.length, 2); + assert.deepEqual( + content.map((item) => item._meta['openai/title']), + ['Amazon US', 'Allbirds'] + ); + assert.ok(content[0].text.includes(JSON.stringify(context.providers[0]))); + assert.ok(!content[0].text.includes(JSON.stringify(context.providers[1]))); + assert.ok(!content[1].text.includes(JSON.stringify(context.providers[0]))); + assert.equal( + context.providers[0].capabilities[0].capability, + 'products/offer' + ); + assert.deepEqual(context.providers[0].capabilities[0].requiresOneOf, [ + ['asin', 'url'], + ]); + assert.equal(context.providers[0].capabilities[0].options[0].name, 'asin'); + assert.equal(context.providers[0].capabilities[0].creditsCost, 5); + assert.deepEqual(context.providers[0].contractLookup.arguments.capabilities, [ + 'products/offer', + ]); + assert.equal( + context.providers[1].contractLookup.arguments.capabilities, + undefined + ); + assert.match(content[0].text, /selection is guidance/); + assert.match(content[0].text, /enable\/connect Firecrawl/); + assert.deepEqual(buildSelectionContext([]).content, []); + assert.throws( + () => + buildSelectionContext([ + { provider, capabilities: [], scope: 'provider' }, + ]), + /Select providers with available tools/ + ); + assert.throws( + () => + buildSelectionContext([ + { + provider, + capabilities: [{ ...capability, description: 'x'.repeat(120000) }], + scope: 'tools', + }, + ]), + /This selection is too large/ + ); +}); + +test('preview discovery defaults match selector-derived levels and required inputs are declared', () => { + for (const [args, level] of [ + [{}, 'categories'], + [{ categories: ['shopping'] }, 'providers'], + [{ providers: ['amazon-com'] }, 'tools'], + ]) { + assert.ok(catalogPage(previewCatalog(args, 'normal'), level).items.length); + } + const row = catalogPage( + previewCatalog( + { providers: ['amazon-com'], expand: ['options'] }, + 'normal' + ), + 'tools' + ).items[0]; + assert.deepEqual( + row.options.map((option) => option.name), + ['asin', 'url'] + ); +}); diff --git a/tests/usage-maintenance.test.mjs b/tests/usage-maintenance.test.mjs new file mode 100644 index 00000000..d44fb628 --- /dev/null +++ b/tests/usage-maintenance.test.mjs @@ -0,0 +1,71 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtemp, readFile, readdir, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { build } from 'esbuild'; +import { + snapshotProviders, + writeLogoSnapshot, +} from '../scripts/logo-snapshot.mjs'; + +test('malformed logo snapshots do not break fallback and writes preserve the last valid snapshot on failure', async (t) => { + for (const value of [{}, { providers: null }, { providers: [] }, null]) + assert.deepEqual(snapshotProviders(value), {}); + const previous = { + 'allbirds-com': { + src: 'data:image/png;base64,eA==', + website: 'https://allbirds.com/', + }, + }; + assert.equal(snapshotProviders({ providers: previous }), previous); + const root = await mkdtemp(join(tmpdir(), 'firecrawl-logo-write-')); + t.after(() => rm(root, { recursive: true, force: true })); + const output = pathToFileURL(join(root, 'logos.json')); + await writeLogoSnapshot(output, { providers: previous }); + const before = await readFile(output, 'utf8'); + const circular = {}; + circular.self = circular; + await assert.rejects(writeLogoSnapshot(output, circular), /circular/i); + assert.equal(await readFile(output, 'utf8'), before); + assert.deepEqual(await readdir(root), ['logos.json']); + await writeLogoSnapshot(output, { providers: {} }); + assert.deepEqual(JSON.parse(await readFile(output, 'utf8')), { + providers: {}, + }); +}); + +test('provider logos accept embedded ICO artwork and reject malformed external inputs', async () => { + const bundle = await build({ + entryPoints: [fileURLToPath(new URL('../web/provider-logo.ts', import.meta.url))], + bundle: true, + write: false, + format: 'esm', + platform: 'node', + }); + const { providerLogoSrc } = await import( + 'data:text/javascript;base64,' + + Buffer.from(bundle.outputFiles[0].text).toString('base64') + ); + for (const mime of [ + 'image/x-icon', + 'image/vnd.microsoft.icon', + 'image/png', + ]) { + const src = 'data:' + mime + ';base64,eA=='; + assert.equal( + providerLogoSrc({ id: 'new-provider', logoDataUri: src }), + src + ); + } + for (const src of [ + 'https://provider.example/logo.png', + 'data:text/html;base64,eA==', + 'data:image/png;base64,