diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 000000000..40dd1ca77 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "docs-scalekit-com" + } +} diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml new file mode 100644 index 000000000..f5f7d7b70 --- /dev/null +++ b/.github/workflows/firebase-hosting-pull-request.yml @@ -0,0 +1,36 @@ +# This file was auto-generated by the Firebase CLI +# https://github.com/firebase/firebase-tools + +name: Deploy to Firebase Hosting on PR +on: + pull_request: + types: [opened, synchronize, reopened, labeled] +permissions: + checks: write + contents: read + pull-requests: write +jobs: + build_and_preview: + if: | + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.draft == false && + contains(github.event.pull_request.labels.*.name, 'firebase-preview') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - run: npm install -g pnpm + - uses: actions/cache@v4 + with: + path: ~/.local/share/pnpm/store + key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-pnpm- + - run: pnpm install + - run: pnpm build + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_DOCS_SCALEKIT_COM }} + projectId: docs-scalekit-com diff --git a/.gitignore b/.gitignore index 4cddc814b..91a9f669d 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,6 @@ skills-lock.json .windsurf/ .tmp/ +.firebase/ +.superpowers/ +docs/superpowers/ diff --git a/astro.config.mjs b/astro.config.mjs index a182b3dcb..b909c0a08 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -305,7 +305,7 @@ export default defineConfig({ }, }), d2({ - skipGeneration: !!process.env['NETLIFY'], + skipGeneration: !!process.env['NETLIFY'] || !!process.env['GITHUB_ACTIONS'], theme: { default: '1', // Light theme (Neutral default) dark: '1', @@ -360,5 +360,5 @@ export default defineConfig({ }, }, }, - adapter: netlify(), + adapter: netlify({ imageCDN: !!process.env.NETLIFY }), }) diff --git a/docs/superpowers/specs/2026-03-26-website-redesign-design.md b/docs/superpowers/specs/2026-03-26-website-redesign-design.md new file mode 100644 index 000000000..8442df9a1 --- /dev/null +++ b/docs/superpowers/specs/2026-03-26-website-redesign-design.md @@ -0,0 +1,319 @@ +# Website redesign — design spec + +**Date:** 2026-03-26 +**Status:** Draft — pending leadership alignment + +--- + +## The core idea + +Scalekit solves two fundamentally different authentication problems. Most auth tools treat them as one. The redesign makes this distinction the organizing principle of the entire web presence. + +> "Auth used to be one problem. AI made it two." + +**Inbound auth** — users, agents, and M2M clients authenticate _into_ your app, MCP server, or API. +**Outbound auth** — your AI agents authenticate _out_ to external services like Gmail, Slack, and Salesforce. + +These require different products, different pricing models, and different documentation. The site reflects that. + +--- + +## Domain strategy + +| Domain | Audience | Purpose | +| ------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| `scalekit.com` | Human developers | Replaces current docs.scalekit.com. Developer-first homepage + full documentation. No marketing fluff. | +| `docs.scalekit.com` | AI coding agents | Machine-readable layer over the same content. Designed for Cursor, Claude Code, Copilot, and any agent building with Scalekit. | + +`scalekit.com` is the primary destination. `docs.scalekit.com` is a programmatic API over the docs — not a parallel site. + +--- + +## Two product lines + +### Auth for Agents (primary) + +**Positioning:** Outbound authentication for AI workflows. + +Your AI agents connect to external services — Gmail, Slack, Salesforce, Notion, and 100+ more — without managing OAuth flows, storing tokens, or handling refresh logic. + +**Products:** Agent Auth +**Pricing metric:** Connected accounts + tool calls +**Primary color:** Purple (`#6366f1`) +**Target developer:** Building AI agents, autonomous workflows, or agentic B2B SaaS + +### Auth for Apps (secondary) + +**Positioning:** Inbound authentication for B2B applications. + +Everything users and agents need to authenticate into your app — login, organizations, SSO, SCIM, MCP server security, and API auth. + +**Products:** Full Stack Auth, Modular SSO, Modular SCIM, MCP Auth, API/M2M Auth +**Pricing metric:** MAUs, MAOs, SSO connections +**Primary color:** Green (`#10b981`) +**Target developer:** Building B2B SaaS apps, MCP servers, or APIs + +**Why agents-primary?** The agent auth market is the growth vector. Most competing auth tools don't solve outbound agent auth at all. Making it primary is a clear positioning statement. + +--- + +## Information hierarchy + +### scalekit.com + +``` +/ Homepage (two-door entry, agents-primary) +/for-agents/ Auth for Agents documentation root + quickstart 10-minute Gmail agent walkthrough + overview What Agent Auth is and how it works + connected-accounts Let users connect their accounts + token-vault How token storage and refresh works + tool-calling Using tokens in agent tool calls + frameworks/ LangChain, OpenAI, Anthropic, Vercel AI, Google ADK, Mastra + providers/ 100+ OAuth provider integration guides +/for-apps/ Auth for Apps documentation root + quickstart Full stack auth in minutes + overview Products and when to use each + full-stack-auth/ Login, session, orgs, RBAC + modular-sso/ SAML/OIDC with enterprise IdPs + modular-scim/ User provisioning from Okta, Azure AD + mcp-auth/ Securing your MCP server + api-auth/ M2M tokens and API keys +/sdks/ Shared across both product lines + node / python / go / java / expo +/apis/ REST API reference (Scalar-powered) +/pricing/ Tabbed pricing — Agent Auth | App Auth +/changelog/ Engineering blog-style release notes +``` + +### docs.scalekit.com + +``` +/llms.txt Product overview optimized for LLM consumption +/llms-full.txt Full context dump — all docs in one file +/sitemap.xml Machine-readable URL index +/openapi/spec.json OpenAPI spec for programmatic access +/mcp MCP server (SSE endpoint) + Tools: + search(query) Semantic search across all docs + get_page(path) Fetch any doc page as clean markdown + list_products() List all products with descriptions + get_code_example(p, lang) Get a code example by product + language +/context/ Structured context files per topic + for-agents.md Full agent auth context for LLM consumption + for-apps.md Full app auth context for LLM consumption + sdks.md SDK reference and naming conventions + errors.md Error codes and troubleshooting +/search REST search API — GET /search?q= +/pages/ Clean markdown mirror of scalekit.com + for-agents/** No sidebars, no JS, no images — pure content + for-apps/** + sdks/** +``` + +The `llms.txt` encodes SDK variable naming conventions (critical for correct code generation): + +- Node.js: `scalekit` +- Python: `scalekit_client` +- Go: `scalekitClient` +- Java: `scalekitClient` + +--- + +## Homepage design + +### Navigation + +``` +⬡ Scalekit | For Agents ↓ | For Apps ↓ | SDKs & APIs | Pricing | Changelog | Sign in | [Start free →] +``` + +- "For Agents" appears first — primary product line +- "For Apps" is secondary but always present +- No "Enterprise" in nav — enterprise is a pricing tier, not a product +- Maximum 6 nav items excluding auth CTAs + +### Hero section + +**Eyebrow:** `Authentication infrastructure for AI-era applications` + +**Headline:** + +``` +Auth used to be one problem. +AI made it two. +``` + +**Body copy:** + +> Your app needs to authenticate the people and agents connecting to it. Your AI agents need to authenticate to the external services they connect to. These are fundamentally different problems. Scalekit solves both. + +**Diagram:** The Inbound/Outbound architecture diagram sits directly below the copy — it explains the entire product in 10 seconds without words. + +### Two doors (below diagram) + +**Door 1 — Agent Auth (full-width primary card, purple border)** + +- Label: `Outbound · Primary product` +- Headline: "Connect your agents to anything" +- Body: Token vault, delegated OAuth, auto-refresh. 100+ providers. Works with every major AI framework. +- Framework tags: LangChain · OpenAI · Anthropic · Vercel AI · Google ADK · Mastra +- Code snippet (Node.js): `scalekit.getToken({ provider: 'google', userId, scopes: ['gmail.send'] })` +- Primary CTA: "Quickstart: Agent Auth →" +- Secondary CTA: "View 100+ providers" + +**Door 2 — App Auth (slim secondary card, green border)** + +- Label: `Inbound · For your app` +- Headline: "Authenticate your app" +- Body: Full-stack login, organizations, RBAC, SSO, SCIM, MCP server security. +- CTA: "Quickstart: For Apps →" (right-aligned) + +### Below-fold sections (in scroll order) + +1. **Pricing** — tabbed (Agent Auth | App Auth), transparent numbers +2. **Security & Compliance** — four cards: certifications, data residency, reliability, token security +3. **Enterprise** — feature checklist (SSO, SCIM, admin portal, custom domain, audit logs, SLA, dedicated support) +4. **Developer Resources** — SDKs, REST API, AI coding assistant MCP callout with copy-paste config snippet +5. **Footer** — product / developer / company columns + compliance badge strip + +--- + +## Docs experience + +### Shared doc shell + +Both `/for-agents/` and `/for-apps/` use the same layout: + +- **Left sidebar:** Product switcher at top (toggles between For Agents ↔ For Apps), then product-specific nav tree +- **Main content:** Breadcrumb, H1, intro paragraph, content +- **Right column:** Table of contents + persistent "For AI assistants → docs.scalekit.com" widget with MCP config snippet + +### For Agents sidebar structure + +``` +Get started + Overview · Quickstart · How it works +Core concepts + Connected accounts · Token vault · Delegated OAuth · Token refresh +Frameworks + LangChain · OpenAI Agents SDK · Anthropic · Vercel AI · Google ADK · Mastra +Providers + Google (Gmail, Drive, Calendar) · Slack · Salesforce · HubSpot · Notion · +95 more +───────────────────────────── +Also using Scalekit for: → Authenticate your app +``` + +### For Apps sidebar structure + +``` +Get started + Overview · Quickstart +Full Stack Auth + Login & session · Organizations · RBAC & permissions · Social login · Magic links +Modular add-ons + SSO (SAML / OIDC) · SCIM provisioning · MCP Auth · API & M2M Auth +───────────────────────────── +Also building agents? → Connect your agents +``` + +### Cross-sell bridge + +Every sidebar has a persistent cross-sell prompt at the bottom pointing developers to the other product line. Developers building AI-powered B2B SaaS need both — this is the bridge. + +### Code examples + +All SDK code examples use the 4-language tab pattern: + +- Node.js (default) · Python · Go · Java +- Variable names follow CLAUDE.md NON-NEGOTIABLE conventions + +--- + +## Pricing design + +Single pricing section on the homepage with a tab toggle: + +**Tab 1 — Agent Auth** (purple) +| Tier | Price | Includes | +|------|-------|---------| +| Starter | $0 | 10 connected accounts · 5k tool calls/mo · 5 providers | +| Growth | Custom | 500 connected accounts · 100k tool calls/mo · 100+ providers · then per-unit | +| Enterprise | Custom | Unlimited · dedicated vault · custom residency · SLA | + +Overages: per connected account + per tool call + +**Tab 2 — App Auth** (green) +| Tier | Price | Includes | +|------|-------|---------| +| Starter | $0 | 1k MAUs · 3 MAOs · 0 SSO connections | +| Growth | Custom | 10k MAUs · 50 MAOs · 3 SSO connections · then per-unit | +| Enterprise | Custom | Unlimited MAUs/MAOs/SSO · custom domain · audit logs · SLA | + +Overages: per MAU + per MAO + per SSO connection + +**Metric definitions** appear below each table — these terms are not industry-standard and must be explained clearly. + +--- + +## docs.scalekit.com — agent-readable surface + +This is not a website. It is a programmatic API layer over the same content that powers `scalekit.com`. + +**Who uses it:** Cursor, Claude Code, GitHub Copilot, and any AI coding agent a developer uses to build with Scalekit. Humans never need to visit it directly. + +**Key surfaces:** + +`llms.txt` — the entry point. Product overview, SDK naming conventions, MCP server URL, and a page index. This is what LLMs read when they need context on Scalekit. + +`/mcp` — MCP server with four tools: `search`, `get_page`, `list_products`, `get_code_example`. Developers add this to their coding agent config with a single JSON snippet: + +```json +{ + "mcpServers": { + "scalekit-docs": { + "url": "https://docs.scalekit.com/mcp", + "type": "sse" + } + } +} +``` + +`/context/for-agents.md` and `/context/for-apps.md` — full product context in clean markdown. Designed to be dropped into an LLM context window directly. + +`/pages/**` — clean markdown mirror of all doc pages. No sidebars, no JavaScript, no images. Pure content for agent consumption. + +--- + +## Visual language + +| Element | Value | +| ----------------- | ----------------------------------------- | +| Background | `#0a0a0f` (near black) | +| Surface | `#0d0d14` / `#080810` | +| Border | `#1e293b` | +| Text primary | `#f1f5f9` | +| Text secondary | `#64748b` | +| Agent Auth accent | `#6366f1` (purple) | +| App Auth accent | `#10b981` (green) | +| Font | System UI / monospace for code and labels | +| Theme | Dark by default — signals developer-first | + +--- + +## What this is not + +- Not a marketing site with a blog, case studies, or testimonials +- Not a "request a demo" funnel — self-serve is primary +- Not a unified pricing model — each product line has its own unit economics +- Not a rebrand — this is a restructure of what already exists + +--- + +## Open questions for leadership alignment + +1. **Pricing numbers** — direction approved, specific tier pricing TBD +2. **Domain migration** — timeline for deprecating scalekit.com as marketing site +3. **docs.scalekit.com build** — MCP server and llms.txt are new infrastructure; who owns this? +4. **"For Agents" vs "For Apps" naming** — these are working titles; does leadership want to finalize these as the canonical product line names? +5. **Self-serve vs sales-led** — design assumes self-serve is primary CTA; confirm this holds for enterprise tier diff --git a/firebase.json b/firebase.json new file mode 100644 index 000000000..5a89d68a5 --- /dev/null +++ b/firebase.json @@ -0,0 +1,6 @@ +{ + "hosting": { + "public": "dist", + "ignore": ["firebase.json", "**/.*", "**/node_modules/**"] + } +} diff --git a/package.json b/package.json index 1e7e0dc2b..a097746e7 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "sync-agent-connectors": "node scripts/sync-agent-connectors.js" }, "dependencies": { + "@ai-sdk/vue": "^1.2.12", "@astrojs/mdx": "^4.3.14", "@astrojs/netlify": "^6.6.4", "@astrojs/react": "^5.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26d29d6a0..7d3002084 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,9 @@ importers: .: dependencies: + '@ai-sdk/vue': + specifier: ^1.2.12 + version: 1.2.12(vue@3.5.31(typescript@5.9.3))(zod@4.3.6) '@astrojs/mdx': specifier: ^4.3.14 version: 4.3.14(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3)) @@ -170,7 +173,7 @@ importers: version: 0.2.5(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3)) '@tailwindcss/vite': specifier: ^4.2.2 - version: 4.2.2(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.2.2(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)) '@types/react': specifier: ^19.2.14 version: 19.2.14 @@ -233,10 +236,10 @@ importers: version: 0.12.0(@astrojs/starlight@0.37.7(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3))) starlight-page-actions: specifier: ^0.5.0 - version: 0.5.0(@astrojs/starlight@0.37.7(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3)))(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3))(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)) + version: 0.5.0(@astrojs/starlight@0.37.7(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3)))(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3))(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)) starlight-plugin-icons: specifier: ^1.1.6 - version: 1.1.6(@astrojs/starlight@0.37.7(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3)))(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)(unocss@66.4.2(postcss@8.5.8)(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)))(zod@3.25.76) + version: 1.1.6(@astrojs/starlight@0.37.7(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3)))(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)(unocss@66.4.2(postcss@8.5.8)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)))(zod@4.3.6) starlight-showcases: specifier: ^0.3.2 version: 0.3.2(@astrojs/starlight@0.37.7(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3))) @@ -267,7 +270,7 @@ importers: version: 0.4.14(typescript@5.9.3) '@vitejs/plugin-vue': specifier: ^6.0.5 - version: 6.0.5(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.31(typescript@5.9.3)) + version: 6.0.5(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.31(typescript@5.9.3)) cross-env: specifier: ^10.1.0 version: 10.1.0 @@ -310,16 +313,41 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider-utils@2.2.8': + resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + '@ai-sdk/provider-utils@4.0.5': resolution: {integrity: sha512-Ow/X/SEkeExTTc1x+nYLB9ZHK2WUId8+9TlkamAx7Tl9vxU+cKzWx2dwjgMHeCN6twrgwkLrrtqckQeO4mxgVA==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider@1.1.3': + resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==} + engines: {node: '>=18'} + '@ai-sdk/provider@3.0.2': resolution: {integrity: sha512-HrEmNt/BH/hkQ7zpi2o6N3k1ZR1QTb7z85WYhYygiTxOQuaml4CMtHCWRbric5WPU+RNsYI7r1EpyVQMKO1pYw==} engines: {node: '>=18'} + '@ai-sdk/ui-utils@1.2.11': + resolution: {integrity: sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + + '@ai-sdk/vue@1.2.12': + resolution: {integrity: sha512-uJJ4w6vlj3mmWzjwg+1dqKtyQSVmavO//189eh3D6bUC/G17OWQdV47b67FaOiNkdlDIxormmbUOjlYDQv0TtA==} + engines: {node: '>=18'} + peerDependencies: + vue: ^3.3.4 + peerDependenciesMeta: + vue: + optional: true + '@ai-sdk/vue@3.0.33': resolution: {integrity: sha512-czM9Js3a7f+Eo35gjEYEeJYUoPvMg5Dfi4bOLyDBghLqn0gaVg8yTmTaSuHCg+3K/+1xPjyXd4+2XcQIohWWiQ==} engines: {node: '>=18'} @@ -1395,89 +1423,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -2013,36 +2057,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.6': resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.6': resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.6': resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.6': resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.6': resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-wasm@2.5.6': resolution: {integrity: sha512-byAiBZ1t3tXQvc8dMD/eoyE7lTXYorhn+6uVW5AC+JGI1KtJC/LvDche5cfUE+qiefH+Ybq0bUCJU0aB1cSHUA==} @@ -2183,66 +2233,79 @@ packages: resolution: {integrity: sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.0': resolution: {integrity: sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.0': resolution: {integrity: sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.0': resolution: {integrity: sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.0': resolution: {integrity: sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.0': resolution: {integrity: sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.0': resolution: {integrity: sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.0': resolution: {integrity: sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.0': resolution: {integrity: sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.0': resolution: {integrity: sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.0': resolution: {integrity: sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.0': resolution: {integrity: sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.0': resolution: {integrity: sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.60.0': resolution: {integrity: sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==} @@ -2489,24 +2552,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.2': resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.2': resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.2': resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.2': resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} @@ -4759,24 +4826,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -5908,6 +5979,9 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -7005,6 +7079,13 @@ snapshots: '@vercel/oidc': 3.1.0 zod: 4.3.6 + '@ai-sdk/provider-utils@2.2.8(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 1.1.3 + nanoid: 3.3.11 + secure-json-parse: 2.7.0 + zod: 4.3.6 + '@ai-sdk/provider-utils@4.0.5(zod@4.3.6)': dependencies: '@ai-sdk/provider': 3.0.2 @@ -7012,10 +7093,31 @@ snapshots: eventsource-parser: 3.0.6 zod: 4.3.6 + '@ai-sdk/provider@1.1.3': + dependencies: + json-schema: 0.4.0 + '@ai-sdk/provider@3.0.2': dependencies: json-schema: 0.4.0 + '@ai-sdk/ui-utils@1.2.11(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@4.3.6) + zod: 4.3.6 + zod-to-json-schema: 3.25.2(zod@4.3.6) + + '@ai-sdk/vue@1.2.12(vue@3.5.31(typescript@5.9.3))(zod@4.3.6)': + dependencies: + '@ai-sdk/provider-utils': 2.2.8(zod@4.3.6) + '@ai-sdk/ui-utils': 1.2.11(zod@4.3.6) + swrv: 1.2.0(vue@3.5.31(typescript@5.9.3)) + optionalDependencies: + vue: 3.5.31(typescript@5.9.3) + transitivePeerDependencies: + - zod + '@ai-sdk/vue@3.0.33(vue@3.5.31(typescript@5.9.3))(zod@4.3.6)': dependencies: '@ai-sdk/provider-utils': 4.0.5(zod@4.3.6) @@ -9816,12 +9918,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 - '@tailwindcss/vite@4.2.2(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3))': + '@tailwindcss/vite@4.2.2(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3))': dependencies: '@tailwindcss/node': 4.2.2 '@tailwindcss/oxide': 4.2.2 tailwindcss: 4.2.2 - vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3) + vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3) '@tanstack/virtual-core@3.13.23': {} @@ -9985,13 +10087,13 @@ snapshots: unhead: 2.1.12 vue: 3.5.31(typescript@5.9.3) - '@unocss/astro@66.4.2(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3))': + '@unocss/astro@66.4.2(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3))': dependencies: '@unocss/core': 66.4.2 '@unocss/reset': 66.4.2 - '@unocss/vite': 66.4.2(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)) + '@unocss/vite': 66.4.2(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)) optionalDependencies: - vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3) + vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3) '@unocss/cli@66.4.2': dependencies: @@ -10120,7 +10222,7 @@ snapshots: dependencies: '@unocss/core': 66.4.2 - '@unocss/vite@66.4.2(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3))': + '@unocss/vite@66.4.2(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3))': dependencies: '@ampproject/remapping': 2.3.0 '@unocss/config': 66.4.2 @@ -10131,7 +10233,7 @@ snapshots: pathe: 2.0.3 tinyglobby: 0.2.15 unplugin-utils: 0.2.5 - vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3) + vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3) '@usesapient/agent-tracker@0.1.1': {} @@ -10203,10 +10305,10 @@ snapshots: vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3) vue: 3.5.31(typescript@5.9.3) - '@vitejs/plugin-vue@6.0.5(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.31(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.5(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.31(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.2 - vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3) + vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3) vue: 3.5.31(typescript@5.9.3) '@vue/babel-helper-vue-transform-on@1.5.0': {} @@ -14043,6 +14145,8 @@ snapshots: scheduler@0.27.0: {} + secure-json-parse@2.7.0: {} + semver@6.3.1: {} semver@7.7.4: {} @@ -14301,16 +14405,16 @@ snapshots: dependencies: '@astrojs/starlight': 0.37.7(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3)) - starlight-page-actions@0.5.0(@astrojs/starlight@0.37.7(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3)))(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3))(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)): + starlight-page-actions@0.5.0(@astrojs/starlight@0.37.7(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3)))(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3))(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)): dependencies: '@astrojs/starlight': 0.37.7(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3)) astro: 5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3) - vite-plugin-static-copy: 3.4.0(patch_hash=af70d2811d381cdea319ad374155ad8bf276cf04bf80834dd64bc0800a7cd4a3)(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)) - vite-plugin-virtual: 0.5.0(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)) + vite-plugin-static-copy: 3.4.0(patch_hash=af70d2811d381cdea319ad374155ad8bf276cf04bf80834dd64bc0800a7cd4a3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)) + vite-plugin-virtual: 0.5.0(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)) transitivePeerDependencies: - vite - starlight-plugin-icons@1.1.6(@astrojs/starlight@0.37.7(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3)))(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)(unocss@66.4.2(postcss@8.5.8)(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)))(zod@3.25.76): + starlight-plugin-icons@1.1.6(@astrojs/starlight@0.37.7(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3)))(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)(unocss@66.4.2(postcss@8.5.8)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)))(zod@4.3.6): dependencies: '@astrojs/starlight': 0.37.7(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3)) astro: 5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3) @@ -14321,8 +14425,8 @@ snapshots: rehype: 13.0.2 typescript: 5.9.3 unist-util-visit: 5.1.0 - unocss: 66.4.2(postcss@8.5.8)(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)) - zod: 3.25.76 + unocss: 66.4.2(postcss@8.5.8)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)) + zod: 4.3.6 starlight-showcases@0.3.2(@astrojs/starlight@0.37.7(astro@5.18.1(@netlify/blobs@10.7.4)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(terser@5.46.1)(typescript@5.9.3)(yaml@2.8.3))): dependencies: @@ -14798,9 +14902,9 @@ snapshots: dependencies: normalize-path: 2.1.1 - unocss@66.4.2(postcss@8.5.8)(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)): + unocss@66.4.2(postcss@8.5.8)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)): dependencies: - '@unocss/astro': 66.4.2(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)) + '@unocss/astro': 66.4.2(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)) '@unocss/cli': 66.4.2 '@unocss/core': 66.4.2 '@unocss/postcss': 66.4.2(postcss@8.5.8) @@ -14818,9 +14922,9 @@ snapshots: '@unocss/transformer-compile-class': 66.4.2 '@unocss/transformer-directives': 66.4.2 '@unocss/transformer-variant-group': 66.4.2 - '@unocss/vite': 66.4.2(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)) + '@unocss/vite': 66.4.2(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)) optionalDependencies: - vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3) + vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3) transitivePeerDependencies: - postcss - supports-color @@ -14930,17 +15034,17 @@ snapshots: dependencies: monaco-editor: 0.54.0 - vite-plugin-static-copy@3.4.0(patch_hash=af70d2811d381cdea319ad374155ad8bf276cf04bf80834dd64bc0800a7cd4a3)(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)): + vite-plugin-static-copy@3.4.0(patch_hash=af70d2811d381cdea319ad374155ad8bf276cf04bf80834dd64bc0800a7cd4a3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)): dependencies: chokidar: 3.6.0 p-map: 7.0.4 picocolors: 1.1.1 tinyglobby: 0.2.15 - vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3) + vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3) - vite-plugin-virtual@0.5.0(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)): + vite-plugin-virtual@0.5.0(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3)): dependencies: - vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3) + vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3) vite-plugin-vue-devtools@7.7.9(rollup@4.60.0)(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.31(typescript@5.9.3)): dependencies: @@ -15212,6 +15316,10 @@ snapshots: dependencies: zod: 3.25.76 + zod-to-json-schema@3.25.2(zod@4.3.6): + dependencies: + zod: 4.3.6 + zod-to-ts@1.2.0(typescript@5.9.3)(zod@3.25.76): dependencies: typescript: 5.9.3 diff --git a/project-docs/2026-03-30-docs-chatbot-flywheel-design.md b/project-docs/2026-03-30-docs-chatbot-flywheel-design.md new file mode 100644 index 000000000..c63f74b1f --- /dev/null +++ b/project-docs/2026-03-30-docs-chatbot-flywheel-design.md @@ -0,0 +1,326 @@ +# Docs Chatbot Flywheel — Design Spec + +**Date:** 2026-03-30 +**Status:** Approved +**Scope:** Embeddable docs chatbot + Pylon feedback loop + Slack bot + +--- + +## Goal + +Build an ever-evolving developer documentation system where every unanswered customer question improves the docs for the next customer. Three components work together as a flywheel: a docs chatbot, a Pylon issue → PR drafting loop, and a Slack bot. + +--- + +## Decisions made + +| Question | Decision | +| -------------------- | --------------------------------------------------------------- | +| Chatbot engine | Custom-built with Claude API (not SaaS) | +| Docs update workflow | Agent drafts PR, human reviews and merges | +| Slack integration | @mention only (any channel) | +| Pylon issue creation | User-initiated with confirmation | +| Pylon → docs trigger | On issue close AND on substantive team comment | +| Doc search approach | Direct llms.txt custom sets (no mcp.scalekit.com, no vector DB) | +| Agent runtime | agentboard (scalekit-inc/agentboard) for all agents | + +--- + +## Architecture overview + +Three input surfaces, two agents, one shared flywheel. + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Docs widget │ │ Webflow / app │ │ Slack @mention │ │ Pylon webhook │ +│ docs.scalekit.com│ │ script tag │ │ any channel │ │ close / comment │ +└────────┬────────┘ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ + └──────────────────────────┬───────────────┘ │ + ↓ ↓ + ┌──────────────────┐ ┌──────────────────────┐ + │ Agent 1 │ │ Agent 2 │ + │ Chatbot │ │ Gap Analyzer │ + │ (AgentRunner) │ │ + PR Drafter │ + │ │ │ (AgentRunner) │ + │ tools: │ │ │ + │ - search_docs │ │ tools: │ + │ - create_pylon_ │ │ - read_pylon_thread │ + │ issue* │ │ - search_docs │ + └────────┬─────────┘ │ - create_github_pr │ + │ └──────────┬───────────┘ + ┌────────────┼────────────┐ │ + ↓ ↓ ↓ ↓ + llms.txt Pylon MCP Slack Block Kit GitHub MCP + custom sets (issue) (confirm button) (PR → human review) + +* requiresConfirmation: true +``` + +--- + +## Component 1: Chatbot widget + +### What it does + +A floating chat widget embedded on any web surface. Users ask questions in natural language. The agent answers grounded strictly in docs content. If it can't answer confidently, it offers to create a Pylon support issue. + +### Widget delivery + +A self-contained JS file served from a CDN. No framework dependency. Embeds the same way on any surface: + +```html + + + + + +``` + +The Astro/Starlight layout override includes the script tag. The React components from agentboard (`AgentTerminal`, `use-agent`, `MessageStream`) are bundled into this CDN file. + +### User identity + +Two modes: + +- **Anonymous**: No identity set. When the user initiates Pylon issue creation, the widget asks for an email inline (optional — user can skip; issue still created). +- **Identified**: App calls `ScalekitChat.identify()` after its own auth. User context flows into `AgentContext` and is automatically included in any Pylon issue created. + +### Answer flow + +1. Widget POSTs `{ question, conversation_history }` to `/api/chat` on the backend. +2. Backend builds `AgentContext` from the request (anonymous or identified). +3. `AgentRunner` calls `search_docs(query)`: + - Classify query into product area (FSA / SSO / Agent Auth / MCP / SCIM / M2M / etc.) + - Load matching custom set from generated `llms.txt` files (defined in `llms.config.ts`) + - Return relevant content to Claude as context +4. Claude evaluates with system prompt (see below). Two outcomes: + - **Confident answer**: Return answer with source page citations. Log question + answer. + - **Not confident**: Return "I don't have a good answer for this. Want me to create a support issue?" User confirms → `create_pylon_issue` fires (with full conversation + user context). +5. Thumbs-down on a confident answer also triggers the Pylon issue offer. + +### System prompt (chatbot) + +``` +You are the Scalekit docs assistant. Answer questions using only the content +returned by the search_docs tool. Always cite the source page path in your answer. + +If the search_docs results don't contain a clear answer to the question, +say so explicitly — do not guess or invent information. Offer to create a +support issue so a human from the team can follow up. + +Keep answers concise. Link directly to the relevant docs page. +``` + +### Agentboard configuration + +```ts +const tools = new ToolRegistry() +tools.register(searchDocsTool) // llms.txt custom sets +tools.register(createPylonIssueTool) // requiresConfirmation: true + +const chatbotRunner = new AgentRunner({ + anthropic, + systemPrompt: CHATBOT_SYSTEM_PROMPT, + tools, + sessionStore: new InMemorySessionStore(), + model: 'claude-haiku-4-5-20251001', // fast for synchronous chatbot + maxTokens: 1024, +}) +``` + +--- + +## Component 2: search_docs tool (llms.txt approach) + +### What it does + +Classifies the user's question into a product area, loads the matching custom set from the statically generated `llms.txt` files, and returns the content as context for Claude. + +### Topic routing + +Uses the routing logic already documented in `src/configs/llms.config.ts`: + +| Topic | Custom set | +| ------------------------------------------- | ------------------------ | +| FSA, users, orgs, sessions, RBAC | Full Stack Auth Complete | +| Agent Auth, OAuth vault, connectors | Agent Authentication | +| MCP, OAuth 2.1, Dynamic Client Registration | MCP Authentication | +| SSO, SAML, OIDC, Intra | Enterprise SSO & SCIM | +| SCIM, directory, user sync | Enterprise SSO & SCIM | +| M2M, client credentials, API keys | Machine-to-Machine Auth | +| SDK methods, endpoints, webhooks | API & SDK Reference | +| Getting started, quickstarts | Quickstart Collection | + +### No external dependencies + +This tool reads from files generated at build time by `starlight-llms-txt`. No vector DB, no embedding pipeline, no external API call. Content updates automatically when a docs PR is merged and the site rebuilds. + +--- + +## Component 3: Pylon feedback loop + +### Triggers + +A webhook listener on the backend fires the gap analyzer agent on two events: + +- **Issue closed** — full thread available, resolution known +- **Substantive team comment** — team member comment longer than ~100 characters (filters out "+1", emoji reactions) + +### Gap analyzer agent + +Built on `AgentRunner` with three tools: + +```ts +tools.register(readPylonThreadTool) // Pylon MCP — fetch issue + all comments +tools.register(searchDocsTool) // same llms.txt tool as chatbot +tools.register(createGithubPrTool) // GitHub MCP — open PR with MDX content +``` + +### Gap analysis system prompt + +``` +You are a documentation gap analyzer for Scalekit. + +Given a support thread (issue + comments) and the current documentation for +the relevant product area, determine: + +1. DOCS GAP — the feature exists but docs are missing or unclear +2. PRODUCT GAP — the feature doesn't exist yet (tag for PM, no PR) +3. ALREADY COVERED — docs answer this question (no action needed) + +If DOCS GAP: use create_github_pr to open a PR. Write the new or updated +MDX content following Scalekit's CLAUDE.md conventions (sentence-case headings, +4-language SDK examples, correct frontmatter). The PR description must link +back to the Pylon issue and explain what was missing. + +Be conservative — only draft a PR if you're confident content is genuinely +missing or misleading. +``` + +### Three outcomes + +| Outcome | Action | +| --------------- | ----------------------------------------------------- | +| Docs gap | Draft PR with new/updated MDX → team reviews → merges | +| Product gap | Tag Pylon issue `product-gap` — no PR, PM sees it | +| Already covered | Tag Pylon issue `docs-ok` — no action | + +### PR format + +- Branch: `docs/gap-pylon-{issueId}` +- Title: `docs: {brief description of what was clarified}` +- Body: why the PR exists, what was missing, link to Pylon issue +- Files: one or more MDX files in `src/content/docs/` +- Follows all CLAUDE.md conventions (frontmatter, sentence case, 90% language rule) + +### Agentboard configuration + +```ts +const gapRunner = new AgentRunner({ + anthropic, + systemPrompt: GAP_ANALYSIS_SYSTEM_PROMPT, + tools: gapTools, + sessionStore: new InMemorySessionStore(), + model: 'claude-sonnet-4-6', // quality matters here; lower volume than chatbot + maxTokens: 4096, +}) +``` + +--- + +## Component 4: Slack bot + +### What it does + +A Slack Bolt (Node.js) app that listens for `app_mention` events in any channel. Forwards to the same chatbot `AgentRunner`. Streams responses back to the thread. Maps agentboard's `confirm` SSE event to Slack Block Kit buttons. + +### Identity resolution + +```ts +// On app_mention event +const slackUser = await app.client.users.info({ user: event.user }) +const ctx: AgentContext = { + userId: slackUser.user.profile.email, + orgId: slackUser.user.enterprise_id ?? slackUser.user.team_id, + isAdmin: isInternalTeamMember(slackUser.user.profile.email), +} +``` + +### Response streaming + +1. Post initial "thinking…" message to thread. +2. Update message as `token` SSE events arrive. +3. On `confirm` event: replace message with Block Kit button message (Yes / No). +4. On button click: Slack interaction payload → `runner.confirm()`. + +### isAdmin gate + +Community members: `isAdmin: false` — chatbot + issue creation only. +Team members: `isAdmin: true` — same tools today, but unlocks admin-only tools in future (org config lookup, etc.) without restructuring the agent. + +--- + +## The flywheel + +``` +User asks unanswerable question + → Pylon issue created (with conversation + user context) + → Team member answers in thread + → Gap analyzer detects docs gap + → PR drafted with new MDX content + → Human reviews and merges + → Docs updated → llms.txt regenerated on deploy + → Chatbot answers correctly next time + → Fewer support issues over time +``` + +Every merged docs PR reduces future support load. The system improves continuously without manual curation. + +--- + +## Technology stack + +| Layer | Technology | +| ------------------ | ----------------------------------------------- | +| Agent runtime | agentboard (scalekit-inc/agentboard) | +| Backend framework | Express (agentboard/express adapter) | +| Chatbot model | claude-haiku-4-5-20251001 | +| Gap analysis model | claude-sonnet-4-6 | +| Doc content source | llms.txt custom sets (starlight-llms-txt) | +| Widget | agentboard React components, CDN-bundled | +| Slack | Slack Bolt SDK (Node.js) | +| Support | Pylon MCP | +| Code | GitHub MCP | +| Session store | InMemorySessionStore (MVP) → Redis (production) | + +--- + +## What is NOT in scope + +- Vector search / embeddings (future upgrade path if precision issues arise) +- Auto-merging PRs (human review always required) +- mcp.scalekit.com search_docs (not needed; llms.txt approach used instead) +- Analytics dashboard for chatbot usage (future) +- Multi-language response support (future) + +--- + +## Deployment topology + +All three backend components (chatbot API, Pylon webhook listener, Slack Bolt app) run as a **single Node.js service** for MVP — one Express server with three route groups. They share one `AgentRunner` instance for the chatbot and one for the gap analyzer. Splitting into separate services is a future option if scaling requires it. + +--- + +## Open questions (non-blocking) + +1. **CDN hosting**: Where does `chatbot.js` get hosted? (scalekit CDN, npm + jsDelivr, or self-hosted on Firebase?) +2. **Session persistence**: InMemorySessionStore works for MVP single-instance. If the backend scales horizontally, migrate to Redis. +3. **Webhook security**: Pylon webhook endpoint needs signature verification to prevent spoofing. +4. **Rate limiting**: Chatbot `/api/chat` endpoint needs rate limiting per IP for anonymous users. diff --git a/project-docs/plans/2026-03-30-chatbot-api-plan-a.md b/project-docs/plans/2026-03-30-chatbot-api-plan-a.md new file mode 100644 index 000000000..dbb405be5 --- /dev/null +++ b/project-docs/plans/2026-03-30-chatbot-api-plan-a.md @@ -0,0 +1,1337 @@ +# Chatbot API Service + Widget Integration — Implementation Plan (Plan A) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a Node.js chatbot API service using agentboard that answers docs questions grounded in llms.txt custom sets, offers to create Pylon issues when it can't answer, and integrates as a widget into docs.scalekit.com. + +**Architecture:** An Express service at `services/chatbot-api/` uses agentboard's `AgentRunner` configured with two tools — `search_docs` (classifies query → fetches matching llms.txt custom set from the public docs URL) and `create_pylon_issue` (creates a Pylon issue with conversation context, requires user confirmation). The widget is injected into the Starlight docs site via the existing `Head.astro` override using agentboard's `AgentTerminal` React component as a placeholder — swap for `ChatBubble` variant once agentboard's UI variants are complete. + +**Tech Stack:** Node.js 20+, TypeScript, agentboard (`github:scalekit-inc/agentboard`), Express 5, Vitest, Anthropic SDK (via agentboard), Pylon REST API. + +**Depends on:** agentboard repo being installable from GitHub. Run `gh repo view scalekit-inc/agentboard` to verify it is public and has a build before starting. + +**Plan B (Pylon feedback loop) and Plan C (Slack bot) follow this plan.** + +--- + +## File map + +``` +services/chatbot-api/ + src/ + index.ts ← Express app entry + server start + agent.ts ← AgentRunner + ToolRegistry setup (the configured agent) + tools/ + search-docs.ts ← search_docs tool: classifies query, fetches llms.txt set + create-pylon-issue.ts ← create_pylon_issue tool (requiresConfirmation: true) + lib/ + classify-query.ts ← maps query text → topic slug + fetch-custom-set.ts ← fetches llms.txt file by topic slug from docs URL + tests/ + lib/ + classify-query.test.ts + fetch-custom-set.test.ts + tools/ + search-docs.test.ts + create-pylon-issue.test.ts + agent.test.ts ← integration: AgentRunner responds to a query + package.json + tsconfig.json + vitest.config.ts + .env.example + +src/components/overrides/ + Head.astro ← MODIFY: inject chatbot widget script tag +src/components/chatbot/ + ChatbotWidget.astro ← NEW: Astro component that mounts the React widget +``` + +--- + +## Task 1: Bootstrap the chatbot-api service + +**Files:** + +- Create: `services/chatbot-api/package.json` +- Create: `services/chatbot-api/tsconfig.json` +- Create: `services/chatbot-api/vitest.config.ts` +- Create: `services/chatbot-api/.env.example` +- Create: `services/chatbot-api/src/index.ts` + +- [ ] **Step 1: Create the service directory and package.json** + +```bash +mkdir -p services/chatbot-api/src/tools services/chatbot-api/src/lib services/chatbot-api/tests/lib services/chatbot-api/tests/tools +``` + +Create `services/chatbot-api/package.json`: + +```json +{ + "name": "chatbot-api", + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "agentboard": "github:scalekit-inc/agentboard", + "express": "^5.0.0", + "dotenv": "^16.0.0" + }, + "devDependencies": { + "@types/express": "^5.0.0", + "@types/node": "^20.0.0", + "tsx": "^4.0.0", + "typescript": "^5.0.0", + "vitest": "^2.0.0", + "msw": "^2.0.0" + } +} +``` + +- [ ] **Step 2: Create tsconfig.json** + +Create `services/chatbot-api/tsconfig.json`: + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +- [ ] **Step 3: Create vitest.config.ts** + +Create `services/chatbot-api/vitest.config.ts`: + +```ts +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'node', + globals: true, + }, +}) +``` + +- [ ] **Step 4: Create .env.example** + +Create `services/chatbot-api/.env.example`: + +``` +ANTHROPIC_API_KEY=sk-ant-... +PYLON_API_TOKEN=pylon_... +DOCS_BASE_URL=https://docs.scalekit.com +PORT=3001 +``` + +Copy to `.env` and fill in values: + +```bash +cp services/chatbot-api/.env.example services/chatbot-api/.env +``` + +- [ ] **Step 5: Create skeleton Express app** + +Create `services/chatbot-api/src/index.ts`: + +```ts +import 'dotenv/config' +import express from 'express' + +const app = express() +app.use(express.json()) + +app.get('/health', (_req, res) => { + res.json({ status: 'ok' }) +}) + +const port = process.env.PORT ?? 3001 +app.listen(port, () => { + console.log(`chatbot-api listening on http://localhost:${port}`) +}) + +export { app } +``` + +- [ ] **Step 6: Install dependencies and verify server starts** + +```bash +cd services/chatbot-api && npm install +npm run dev +``` + +Expected output: + +``` +chatbot-api listening on http://localhost:3001 +``` + +Verify: + +```bash +curl http://localhost:3001/health +``` + +Expected: `{"status":"ok"}` + +- [ ] **Step 7: Commit** + +```bash +git add services/chatbot-api/ +git commit -m "feat(chatbot-api): bootstrap express service skeleton" +``` + +--- + +## Task 2: Query topic classifier + +**Files:** + +- Create: `services/chatbot-api/src/lib/classify-query.ts` +- Create: `services/chatbot-api/tests/lib/classify-query.test.ts` + +The classifier maps a query string to one of the topic slugs that correspond to llms.txt custom sets. + +- [ ] **Step 1: Write failing tests** + +Create `services/chatbot-api/tests/lib/classify-query.test.ts`: + +```ts +import { describe, it, expect } from 'vitest' +import { classifyQuery } from '../../src/lib/classify-query.js' + +describe('classifyQuery', () => { + it('classifies FSA questions', () => { + expect(classifyQuery('How do I manage user sessions in FSA?')).toBe('fsa') + expect(classifyQuery('How does RBAC work?')).toBe('fsa') + expect(classifyQuery('How do I add users to an org?')).toBe('fsa') + }) + + it('classifies SSO questions', () => { + expect(classifyQuery('How do I set up SAML SSO?')).toBe('sso') + expect(classifyQuery('Configure OIDC with Okta')).toBe('sso') + expect(classifyQuery('single sign-on setup')).toBe('sso') + }) + + it('classifies SCIM questions', () => { + expect(classifyQuery('How does SCIM provisioning work?')).toBe('scim') + expect(classifyQuery('Sync users from directory')).toBe('scim') + }) + + it('classifies Agent Auth questions', () => { + expect(classifyQuery('How do AI agents authenticate?')).toBe('agent-auth') + expect(classifyQuery('OAuth vault for tool calling')).toBe('agent-auth') + }) + + it('classifies MCP questions', () => { + expect(classifyQuery('How do I add auth to my MCP server?')).toBe('mcp') + expect(classifyQuery('Dynamic Client Registration')).toBe('mcp') + }) + + it('classifies M2M questions', () => { + expect(classifyQuery('Service to service authentication')).toBe('m2m') + expect(classifyQuery('client credentials flow')).toBe('m2m') + expect(classifyQuery('API key authentication')).toBe('m2m') + }) + + it('classifies SDK/API reference questions', () => { + expect(classifyQuery('What does the getSession() method return?')).toBe('sdk') + expect(classifyQuery('Webhook payload format')).toBe('sdk') + }) + + it('falls back to quickstart for unknown topics', () => { + expect(classifyQuery('How do I get started with Scalekit?')).toBe('quickstart') + expect(classifyQuery('What is Scalekit?')).toBe('quickstart') + }) + + it('is case-insensitive', () => { + expect(classifyQuery('HOW DO I SET UP SAML SSO')).toBe('sso') + }) +}) +``` + +- [ ] **Step 2: Run tests — verify they fail** + +```bash +cd services/chatbot-api && npm test tests/lib/classify-query.test.ts +``` + +Expected: FAIL — `Cannot find module '../../src/lib/classify-query.js'` + +- [ ] **Step 3: Implement classifyQuery** + +Create `services/chatbot-api/src/lib/classify-query.ts`: + +```ts +export type TopicSlug = 'fsa' | 'sso' | 'scim' | 'agent-auth' | 'mcp' | 'm2m' | 'sdk' | 'quickstart' + +const TOPIC_PATTERNS: Array<[TopicSlug, RegExp]> = [ + [ + 'fsa', + /\bfsa\b|full[\s-]stack\s*auth|session|rbac|role.based|\buser.*org\b|\borg.*user\b|login\s*flow|sign[\s-]?in\s*flow/i, + ], + [ + 'sso', + /\bsso\b|saml|oidc(?!\s*vault)|single\s*sign[\s-]on|identity\s*provider|enterprise\s*login/i, + ], + ['scim', /\bscim\b|directory\s*sync|user\s*sync|provisioning|deprovisioning|group\s*sync/i], + [ + 'agent-auth', + /agent\s*auth|ai\s*agent|oauth\s*vault|tool\s*call|mcp\s*connector|agent\s*connector/i, + ], + [ + 'mcp', + /\bmcp\b|model\s*context\s*protocol|dynamic\s*client\s*registration|mcp\s*server|mcp\s*auth/i, + ], + [ + 'm2m', + /m2m|machine[\s-]to[\s-]machine|client\s*credentials|api\s*key|service[\s-]to[\s-]service|service\s*account/i, + ], + [ + 'sdk', + /\bsdk\b|endpoint|webhook|api\s*reference|method\s*return|\bgetSession\b|\bcreateUser\b/i, + ], +] + +export function classifyQuery(query: string): TopicSlug { + for (const [slug, pattern] of TOPIC_PATTERNS) { + if (pattern.test(query)) return slug + } + return 'quickstart' +} +``` + +- [ ] **Step 4: Run tests — verify they pass** + +```bash +cd services/chatbot-api && npm test tests/lib/classify-query.test.ts +``` + +Expected: All tests PASS. + +- [ ] **Step 5: Commit** + +```bash +git add services/chatbot-api/src/lib/classify-query.ts services/chatbot-api/tests/lib/classify-query.test.ts +git commit -m "feat(chatbot-api): implement query topic classifier" +``` + +--- + +## Task 3: llms.txt custom set fetcher + +**Files:** + +- Create: `services/chatbot-api/src/lib/fetch-custom-set.ts` +- Create: `services/chatbot-api/tests/lib/fetch-custom-set.test.ts` + +Fetches the right llms.txt file from the public docs URL based on topic slug. The custom sets are served at `{DOCS_BASE_URL}/llms-{slug}.txt` by `starlight-llms-txt`. + +**Before implementing:** verify the actual URLs by running: + +```bash +curl -I https://docs.scalekit.com/llms-fsa.txt +curl -I https://docs.scalekit.com/llms-agent-authentication.txt +``` + +If the URLs use different slugs (e.g. label-based like `llms-full-stack-auth-complete.txt`), update the `SLUG_TO_PATH` map in this task accordingly. + +- [ ] **Step 1: Write failing tests** + +Create `services/chatbot-api/tests/lib/fetch-custom-set.test.ts`: + +```ts +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { fetchCustomSet } from '../../src/lib/fetch-custom-set.js' +import type { TopicSlug } from '../../src/lib/classify-query.js' + +const MOCK_DOCS_CONTENT = '# FSA Docs\n\nThis is the full stack auth documentation.' + +describe('fetchCustomSet', () => { + beforeEach(() => { + vi.stubGlobal('fetch', vi.fn()) + process.env.DOCS_BASE_URL = 'https://docs.scalekit.com' + }) + + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('fetches the correct URL for fsa topic', async () => { + ;(fetch as ReturnType).mockResolvedValueOnce({ + ok: true, + text: async () => MOCK_DOCS_CONTENT, + }) + + const result = await fetchCustomSet('fsa') + + expect(fetch).toHaveBeenCalledWith(expect.stringContaining('/llms-'), expect.any(Object)) + expect(result).toBe(MOCK_DOCS_CONTENT) + }) + + it('falls back to llms-small.txt when custom set fetch fails', async () => { + ;(fetch as ReturnType) + .mockResolvedValueOnce({ ok: false, status: 404 }) + .mockResolvedValueOnce({ ok: true, text: async () => '# Full Docs\n\nAll content.' }) + + const result = await fetchCustomSet('fsa') + expect(result).toBe('# Full Docs\n\nAll content.') + }) + + it('throws when both custom set and fallback fail', async () => { + ;(fetch as ReturnType) + .mockResolvedValueOnce({ ok: false, status: 404 }) + .mockResolvedValueOnce({ ok: false, status: 500 }) + + await expect(fetchCustomSet('fsa')).rejects.toThrow('Failed to fetch docs content') + }) + + it('fetches all topic slugs without throwing', async () => { + const slugs: TopicSlug[] = [ + 'fsa', + 'sso', + 'scim', + 'agent-auth', + 'mcp', + 'm2m', + 'sdk', + 'quickstart', + ] + for (const slug of slugs) { + ;(fetch as ReturnType).mockResolvedValueOnce({ + ok: true, + text: async () => `# ${slug} docs`, + }) + const result = await fetchCustomSet(slug) + expect(result).toContain(slug) + } + }) +}) +``` + +- [ ] **Step 2: Run tests — verify they fail** + +```bash +cd services/chatbot-api && npm test tests/lib/fetch-custom-set.test.ts +``` + +Expected: FAIL — `Cannot find module '../../src/lib/fetch-custom-set.js'` + +- [ ] **Step 3: Implement fetchCustomSet** + +Create `services/chatbot-api/src/lib/fetch-custom-set.ts`: + +```ts +import type { TopicSlug } from './classify-query.js' + +// Maps topic slugs to the llms.txt path suffix served by starlight-llms-txt. +// Verify these paths exist by running: +// curl -I https://docs.scalekit.com/llms-{path}.txt +// Update this map if the plugin uses different naming conventions. +const SLUG_TO_PATH: Record = { + fsa: 'full-stack-auth-complete', + sso: 'enterprise-sso-and-scim', + scim: 'enterprise-sso-and-scim', + 'agent-auth': 'agent-authentication', + mcp: 'mcp-authentication', + m2m: 'machine-to-machine-auth', + sdk: 'api-and-sdk-reference', + quickstart: 'quickstart-collection', +} + +export async function fetchCustomSet(topic: TopicSlug): Promise { + const baseUrl = process.env.DOCS_BASE_URL ?? 'https://docs.scalekit.com' + const path = SLUG_TO_PATH[topic] + const customSetUrl = `${baseUrl}/llms-${path}.txt` + + const customRes = await fetch(customSetUrl, { + headers: { 'User-Agent': 'scalekit-chatbot/1.0' }, + }) + + if (customRes.ok) { + return customRes.text() + } + + // Fall back to llms-small.txt which contains all content in a smaller footprint + const fallbackUrl = `${baseUrl}/llms-small.txt` + const fallbackRes = await fetch(fallbackUrl, { + headers: { 'User-Agent': 'scalekit-chatbot/1.0' }, + }) + + if (fallbackRes.ok) { + return fallbackRes.text() + } + + throw new Error( + `Failed to fetch docs content: custom set ${customSetUrl} returned ${customRes.status}, ` + + `fallback ${fallbackUrl} returned ${fallbackRes.status}`, + ) +} +``` + +- [ ] **Step 4: Run tests — verify they pass** + +```bash +cd services/chatbot-api && npm test tests/lib/fetch-custom-set.test.ts +``` + +Expected: All tests PASS. + +- [ ] **Step 5: Commit** + +```bash +git add services/chatbot-api/src/lib/fetch-custom-set.ts services/chatbot-api/tests/lib/fetch-custom-set.test.ts +git commit -m "feat(chatbot-api): implement llms.txt custom set fetcher with fallback" +``` + +--- + +## Task 4: search_docs tool + +**Files:** + +- Create: `services/chatbot-api/src/tools/search-docs.ts` +- Create: `services/chatbot-api/tests/tools/search-docs.test.ts` + +Combines `classifyQuery` + `fetchCustomSet` into a `RegistryTool` that agentboard's `ToolRegistry` can register. + +- [ ] **Step 1: Write failing tests** + +Create `services/chatbot-api/tests/tools/search-docs.test.ts`: + +```ts +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { searchDocsTool } from '../../src/tools/search-docs.js' + +const MOCK_FSA_CONTENT = '# Full Stack Auth\n\nScalekit FSA documentation content here.' + +describe('searchDocsTool', () => { + beforeEach(() => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + text: async () => MOCK_FSA_CONTENT, + }), + ) + process.env.DOCS_BASE_URL = 'https://docs.scalekit.com' + }) + + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('has correct tool definition for Claude', () => { + expect(searchDocsTool.definition.name).toBe('search_docs') + expect(searchDocsTool.definition.description).toContain('search') + expect(searchDocsTool.definition.input_schema.properties).toHaveProperty('query') + }) + + it('returns docs content for a query', async () => { + const result = await searchDocsTool.execute( + { query: 'How do I set up RBAC?' }, + { userId: 'anon', orgId: '', isAdmin: false }, + ) + expect(result).toContain('Full Stack Auth') + }) + + it('accepts optional topic override', async () => { + const result = await searchDocsTool.execute( + { query: 'How do I set up RBAC?', topic: 'sso' }, + { userId: 'anon', orgId: '', isAdmin: false }, + ) + expect(result).toBeDefined() + // Verify the sso path was fetched + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('enterprise-sso'), + expect.any(Object), + ) + }) + + it('does not require confirmation', () => { + expect(searchDocsTool.requiresConfirmation).toBeFalsy() + }) +}) +``` + +- [ ] **Step 2: Run tests — verify they fail** + +```bash +cd services/chatbot-api && npm test tests/tools/search-docs.test.ts +``` + +Expected: FAIL — `Cannot find module '../../src/tools/search-docs.js'` + +- [ ] **Step 3: Implement search_docs tool** + +Create `services/chatbot-api/src/tools/search-docs.ts`: + +```ts +import type { RegistryTool } from 'agentboard' +import { classifyQuery, type TopicSlug } from '../lib/classify-query.js' +import { fetchCustomSet } from '../lib/fetch-custom-set.js' + +export const searchDocsTool: RegistryTool = { + definition: { + name: 'search_docs', + description: + 'Search Scalekit documentation to answer questions. ' + + 'Always call this tool before answering any question about Scalekit products ' + + '(FSA, SSO, SCIM, Agent Auth, MCP, M2M, SDK). ' + + 'Returns the relevant documentation content as context.', + input_schema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'The user question to search for in the docs', + }, + topic: { + type: 'string', + enum: ['fsa', 'sso', 'scim', 'agent-auth', 'mcp', 'm2m', 'sdk', 'quickstart'], + description: + 'Optional topic override. If omitted, the topic is auto-detected from the query.', + }, + }, + required: ['query'], + }, + }, + execute: async (args) => { + const { query, topic } = args as { query: string; topic?: TopicSlug } + const resolvedTopic = topic ?? classifyQuery(query) + const content = await fetchCustomSet(resolvedTopic) + return content + }, + requiresConfirmation: false, +} +``` + +- [ ] **Step 4: Run tests — verify they pass** + +```bash +cd services/chatbot-api && npm test tests/tools/search-docs.test.ts +``` + +Expected: All tests PASS. + +- [ ] **Step 5: Commit** + +```bash +git add services/chatbot-api/src/tools/search-docs.ts services/chatbot-api/tests/tools/search-docs.test.ts +git commit -m "feat(chatbot-api): implement search_docs tool using llms.txt custom sets" +``` + +--- + +## Task 5: create_pylon_issue tool + +**Files:** + +- Create: `services/chatbot-api/src/tools/create-pylon-issue.ts` +- Create: `services/chatbot-api/tests/tools/create-pylon-issue.test.ts` + +Creates a Pylon support issue with full conversation context and user identity. `requiresConfirmation: true` — agentboard pauses and asks the user to confirm before executing. + +**Before implementing:** Verify your Pylon API base URL and auth format by checking the Pylon dashboard or docs. The tool uses `https://api.usepylon.com` with a Bearer token. If your Pylon instance uses a different URL, set `PYLON_API_URL` in `.env`. + +- [ ] **Step 1: Write failing tests** + +Create `services/chatbot-api/tests/tools/create-pylon-issue.test.ts`: + +```ts +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { createPylonIssueTool } from '../../src/tools/create-pylon-issue.js' +import type { AgentContext } from 'agentboard' + +const MOCK_ISSUE_RESPONSE = { + id: 'issue_abc123', + title: 'Docs gap: passkeys + SAML SSO', + status: 'open', +} + +describe('createPylonIssueTool', () => { + beforeEach(() => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + json: async () => MOCK_ISSUE_RESPONSE, + }), + ) + process.env.PYLON_API_TOKEN = 'test-pylon-token' + process.env.PYLON_API_URL = 'https://api.usepylon.com' + }) + + afterEach(() => { + vi.unstubAllGlobals() + delete process.env.PYLON_API_TOKEN + delete process.env.PYLON_API_URL + }) + + it('has correct tool definition', () => { + expect(createPylonIssueTool.definition.name).toBe('create_pylon_issue') + expect(createPylonIssueTool.definition.input_schema.properties).toHaveProperty('question') + expect(createPylonIssueTool.definition.input_schema.properties).toHaveProperty( + 'conversation_summary', + ) + }) + + it('requires confirmation before executing', () => { + expect(createPylonIssueTool.requiresConfirmation).toBe(true) + }) + + it('creates an issue with user context when user is identified', async () => { + const ctx: AgentContext = { + userId: 'jane@acme.com', + orgId: 'org_acme', + isAdmin: false, + } + + const result = (await createPylonIssueTool.execute( + { + question: 'Can passkeys and SAML SSO coexist for the same org?', + conversation_summary: + 'User asked about passkeys + SAML, no confident answer found in docs.', + }, + ctx, + )) as { issueId: string; message: string } + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('usepylon.com'), + expect.objectContaining({ + method: 'POST', + headers: expect.objectContaining({ + Authorization: 'Bearer test-pylon-token', + }), + }), + ) + expect(result.issueId).toBe('issue_abc123') + expect(result.message).toContain('issue_abc123') + }) + + it('creates an issue for anonymous users without user context', async () => { + const ctx: AgentContext = { + userId: '', + orgId: '', + isAdmin: false, + } + + await createPylonIssueTool.execute( + { question: 'How does M2M auth work?', conversation_summary: 'No answer found.' }, + ctx, + ) + + expect(fetch).toHaveBeenCalled() + }) + + it('throws with a clear message when PYLON_API_TOKEN is missing', async () => { + delete process.env.PYLON_API_TOKEN + const ctx: AgentContext = { userId: '', orgId: '', isAdmin: false } + + await expect( + createPylonIssueTool.execute({ question: 'test', conversation_summary: 'test' }, ctx), + ).rejects.toThrow('PYLON_API_TOKEN') + }) + + it('throws when Pylon API returns an error', async () => { + ;(fetch as ReturnType).mockResolvedValueOnce({ + ok: false, + status: 401, + text: async () => 'Unauthorized', + }) + const ctx: AgentContext = { userId: '', orgId: '', isAdmin: false } + + await expect( + createPylonIssueTool.execute({ question: 'test', conversation_summary: 'test' }, ctx), + ).rejects.toThrow('Pylon API error 401') + }) +}) +``` + +- [ ] **Step 2: Run tests — verify they fail** + +```bash +cd services/chatbot-api && npm test tests/tools/create-pylon-issue.test.ts +``` + +Expected: FAIL — `Cannot find module '../../src/tools/create-pylon-issue.js'` + +- [ ] **Step 3: Implement create_pylon_issue tool** + +Create `services/chatbot-api/src/tools/create-pylon-issue.ts`: + +```ts +import type { RegistryTool, AgentContext } from 'agentboard' + +interface PylonIssueArgs { + question: string + conversation_summary: string + user_email?: string // provided by widget when anonymous user opts in +} + +export const createPylonIssueTool: RegistryTool = { + definition: { + name: 'create_pylon_issue', + description: + 'Create a support issue in Pylon when the documentation does not contain a confident answer. ' + + 'Only call this tool after the user has explicitly confirmed they want to create an issue. ' + + 'Include the original question and a summary of what was searched.', + input_schema: { + type: 'object', + properties: { + question: { + type: 'string', + description: "The user's original question that could not be answered from docs.", + }, + conversation_summary: { + type: 'string', + description: 'Brief summary of the conversation and what docs were searched.', + }, + user_email: { + type: 'string', + description: 'Optional email address provided by the user for follow-up.', + }, + }, + required: ['question', 'conversation_summary'], + }, + }, + execute: async (args, ctx: AgentContext) => { + const { question, conversation_summary, user_email } = args as PylonIssueArgs + + const token = process.env.PYLON_API_TOKEN + if (!token) throw new Error('PYLON_API_TOKEN environment variable is not set') + + const apiUrl = process.env.PYLON_API_URL ?? 'https://api.usepylon.com' + + const title = `Docs gap: ${question.slice(0, 80)}${question.length > 80 ? '…' : ''}` + + const body: Record = { + title, + body: [ + `**Question:** ${question}`, + '', + `**Context:** ${conversation_summary}`, + '', + `**Source:** docs chatbot`, + ctx.userId ? `**User:** ${ctx.userId}` : '', + ctx.orgId ? `**Org:** ${ctx.orgId}` : '', + user_email ? `**Contact:** ${user_email}` : '', + ] + .filter(Boolean) + .join('\n'), + tags: ['docs-gap'], + } + + if (ctx.userId) body.requester_email = ctx.userId + + const res = await fetch(`${apiUrl}/issues`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }) + + if (!res.ok) { + const text = await res.text().catch(() => '') + throw new Error(`Pylon API error ${res.status}: ${text}`) + } + + const issue = (await res.json()) as { id: string } + return { + issueId: issue.id, + message: `Support issue created (${issue.id}). Our team will follow up${ctx.userId ? ` with ${ctx.userId}` : ''}.`, + } + }, + requiresConfirmation: true, +} +``` + +- [ ] **Step 4: Run tests — verify they pass** + +```bash +cd services/chatbot-api && npm test tests/tools/create-pylon-issue.test.ts +``` + +Expected: All tests PASS. + +- [ ] **Step 5: Commit** + +```bash +git add services/chatbot-api/src/tools/create-pylon-issue.ts services/chatbot-api/tests/tools/create-pylon-issue.test.ts +git commit -m "feat(chatbot-api): implement create_pylon_issue tool with confirmation" +``` + +--- + +## Task 6: Configure AgentRunner and wire Express routes + +**Files:** + +- Create: `services/chatbot-api/src/agent.ts` +- Modify: `services/chatbot-api/src/index.ts` +- Create: `services/chatbot-api/tests/agent.test.ts` + +- [ ] **Step 1: Write failing integration test** + +Create `services/chatbot-api/tests/agent.test.ts`: + +```ts +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import Anthropic from '@anthropic-ai/sdk' + +// Mock the Anthropic SDK to avoid real API calls in tests +vi.mock('@anthropic-ai/sdk', () => { + return { + default: vi.fn().mockImplementation(() => ({ + messages: { + create: vi.fn().mockResolvedValue({ + id: 'msg_test', + content: [{ type: 'text', text: 'Based on the docs, here is how FSA sessions work...' }], + stop_reason: 'end_turn', + usage: { input_tokens: 100, output_tokens: 50 }, + }), + }, + })), + } +}) + +vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + text: async () => '# FSA Docs\n\nSession management works by...', + }), +) + +describe('createAgent', () => { + afterEach(() => { + vi.clearAllMocks() + }) + + it('creates an agent with search_docs and create_pylon_issue tools', async () => { + process.env.ANTHROPIC_API_KEY = 'test-key' + const { createAgent } = await import('../src/agent.js') + const { runner, tools } = createAgent() + + expect(runner).toBeDefined() + expect(tools.size).toBe(2) + }) + + it('registers search_docs without confirmation', async () => { + process.env.ANTHROPIC_API_KEY = 'test-key' + const { createAgent } = await import('../src/agent.js') + const { tools } = createAgent() + + expect(tools.requiresConfirmation('search_docs')).toBe(false) + expect(tools.requiresConfirmation('create_pylon_issue')).toBe(true) + }) +}) +``` + +- [ ] **Step 2: Run test — verify it fails** + +```bash +cd services/chatbot-api && npm test tests/agent.test.ts +``` + +Expected: FAIL — `Cannot find module '../src/agent.js'` + +- [ ] **Step 3: Implement agent.ts** + +Create `services/chatbot-api/src/agent.ts`: + +```ts +import Anthropic from '@anthropic-ai/sdk' +import { AgentRunner, ToolRegistry, InMemorySessionStore } from 'agentboard' +import { searchDocsTool } from './tools/search-docs.js' +import { createPylonIssueTool } from './tools/create-pylon-issue.js' + +const SYSTEM_PROMPT = `You are the Scalekit docs assistant. + +When answering questions: +1. ALWAYS call search_docs first before answering any product question. +2. Answer ONLY using content returned by search_docs. Do not use prior knowledge. +3. Always cite the source by mentioning the relevant docs section. +4. Keep answers concise and link to the relevant docs page when possible. + +If search_docs does not contain a clear answer to the question: +- Say explicitly: "I don't have a confident answer for this in the docs." +- Offer to create a support issue: "Would you like me to create a support issue so our team can follow up?" +- If the user confirms, call create_pylon_issue with the original question and a summary. + +Never guess or invent information about Scalekit products.` + +export function createAgent() { + const anthropic = new Anthropic({ + apiKey: process.env.ANTHROPIC_API_KEY, + }) + + const tools = new ToolRegistry() + tools.register(searchDocsTool) + tools.register(createPylonIssueTool) + + const runner = new AgentRunner({ + anthropic, + systemPrompt: SYSTEM_PROMPT, + tools, + sessionStore: new InMemorySessionStore(), + model: 'claude-haiku-4-5-20251001', + maxTokens: 1024, + }) + + return { runner, tools } +} +``` + +- [ ] **Step 4: Wire the Express routes using agentboard's Express adapter** + +Update `services/chatbot-api/src/index.ts`: + +```ts +import 'dotenv/config' +import express from 'express' +import { createAgentRouter } from 'agentboard/express' +import { createAgent } from './agent.js' + +const app = express() +app.use(express.json()) + +// CORS for the docs site and any embed origin +app.use((_req, res, next) => { + res.setHeader('Access-Control-Allow-Origin', '*') + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, x-user-id, x-org-id, x-is-admin') + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') + next() +}) + +app.options('*', (_req, res) => res.sendStatus(200)) + +app.get('/health', (_req, res) => { + res.json({ status: 'ok' }) +}) + +const { runner, tools } = createAgent() + +// Mount the agentboard Express router at /api/chat +// It handles POST /api/chat (send message) and POST /api/chat/confirm (tool confirmation) +app.use( + '/api/chat', + createAgentRouter({ + runner, + context: (req) => ({ + userId: (req.headers['x-user-id'] as string) ?? '', + orgId: (req.headers['x-org-id'] as string) ?? '', + isAdmin: req.headers['x-is-admin'] === 'true', + }), + }), +) + +const port = process.env.PORT ?? 3001 +app.listen(port, () => { + console.log(`chatbot-api listening on http://localhost:${port}`) +}) + +export { app } +``` + +- [ ] **Step 5: Run all tests** + +```bash +cd services/chatbot-api && npm test +``` + +Expected: All tests PASS. + +- [ ] **Step 6: Smoke test the running server** + +```bash +cd services/chatbot-api && npm run dev +``` + +In another terminal: + +```bash +curl -X POST http://localhost:3001/api/chat \ + -H "Content-Type: application/json" \ + -d '{"message": "How does RBAC work in FSA?", "session_id": "test-123"}' \ + --no-buffer +``` + +Expected: SSE stream of `data: {"type":"token","data":{"text":"..."}}` events, ending with `data: {"type":"done",...}`. + +- [ ] **Step 7: Commit** + +```bash +git add services/chatbot-api/src/agent.ts services/chatbot-api/src/index.ts services/chatbot-api/tests/agent.test.ts +git commit -m "feat(chatbot-api): wire AgentRunner with Express routes" +``` + +--- + +## Task 7: Integrate widget into Starlight docs site + +**Files:** + +- Create: `src/components/chatbot/ChatbotWidget.astro` +- Modify: `src/components/overrides/Head.astro` + +This task embeds the agentboard widget into the docs site. It uses `AgentTerminal` as a placeholder. When agentboard's `ChatBubble` variant is complete (built in the parallel agentboard session), replace `AgentTerminal` with `AgentChatBubble` — no other changes needed. + +- [ ] **Step 1: Create the ChatbotWidget Astro component** + +Create `src/components/chatbot/ChatbotWidget.astro`: + +```astro +--- +// Chatbot widget component. +// Currently uses AgentTerminal as placeholder. +// Replace with AgentChatBubble once agentboard UI variants are complete. +const chatApiUrl = import.meta.env.PUBLIC_CHATBOT_API_URL ?? 'http://localhost:3001' +--- + + + + + + + + +``` + +- [ ] **Step 2: Add PUBLIC_CHATBOT_API_URL to the docs site env** + +Add to the root `.env` of the docs site (create if it doesn't exist): + +``` +PUBLIC_CHATBOT_API_URL=http://localhost:3001 +``` + +For production, this will be the deployed chatbot-api URL. + +- [ ] **Step 3: Inject the widget into Head.astro** + +Modify `src/components/overrides/Head.astro`: + +```astro +--- +import Default from '@astrojs/starlight/components/Head.astro' +import ChatbotWidget from '../chatbot/ChatbotWidget.astro' +--- + + + + + + +``` + +- [ ] **Step 4: Verify the widget renders in local dev** + +```bash +# Terminal 1 — start chatbot API +cd services/chatbot-api && npm run dev + +# Terminal 2 — start docs site +pnpm dev +``` + +Open `http://localhost:4321` in a browser. Verify: + +- A purple 💬 button appears in the bottom-right corner +- Clicking it opens a chat panel +- Typing a question produces a streamed response +- An unanswerable question produces the "create support issue?" offer + +- [ ] **Step 5: Commit** + +```bash +git add src/components/chatbot/ChatbotWidget.astro src/components/overrides/Head.astro +git commit -m "feat(docs): integrate chatbot widget into Starlight layout" +``` + +--- + +## Task 8: Final wiring check and env documentation + +**Files:** + +- Create: `services/chatbot-api/README.md` + +- [ ] **Step 1: Run the full test suite** + +```bash +cd services/chatbot-api && npm test +``` + +Expected: All tests PASS with no skipped tests. + +- [ ] **Step 2: Verify the confirmation flow end-to-end** + +With the chatbot API running and docs site open: + +1. Ask: "How do I set up SCIM provisioning?" — should get a docs-grounded answer. +2. Ask: "Does Scalekit support biometric authentication on iOS?" — should get the "I don't have a confident answer" response with Yes/No buttons. +3. Click "Yes, create issue" — should trigger the `confirm` flow and create a Pylon issue (check your Pylon dashboard). + +- [ ] **Step 3: Write the service README** + +Create `services/chatbot-api/README.md`: + +````markdown +# chatbot-api + +Node.js Express service powering the Scalekit docs chatbot. + +## Setup + +\```bash +npm install +cp .env.example .env + +# Fill in ANTHROPIC_API_KEY and PYLON_API_TOKEN in .env + +\``` + +## Run + +\```bash +npm run dev # development with hot reload +npm start # production +\``` + +## Environment variables + +| Variable | Required | Description | +| ------------------- | -------- | ------------------------------------------------------------------- | +| `ANTHROPIC_API_KEY` | Yes | Anthropic API key | +| `PYLON_API_TOKEN` | Yes | Pylon API Bearer token | +| `DOCS_BASE_URL` | No | Docs URL for llms.txt fetching (default: https://docs.scalekit.com) | +| `PYLON_API_URL` | No | Pylon API base URL (default: https://api.usepylon.com) | +| `PORT` | No | Port to listen on (default: 3001) | + +## Endpoints + +- `GET /health` — health check +- `POST /api/chat` — send a message (SSE stream) +- `POST /api/chat/confirm` — confirm a pending tool action + +## Passing user context + +Set headers on requests from identified surfaces: + +\``` +x-user-id: jane@acme.com +x-org-id: org_acme +x-is-admin: false +\``` + +## Swapping the widget UI + +The docs site widget (`src/components/chatbot/ChatbotWidget.astro`) currently uses +`AgentTerminal`. Once agentboard's `ChatBubble` variant is complete, replace the +import and component in that file — no changes to this service needed. +```` + +- [ ] **Step 4: Final commit** + +```bash +git add services/chatbot-api/README.md +git commit -m "docs(chatbot-api): add README with setup and env documentation" +``` + +--- + +## Self-review notes + +**Spec coverage check:** + +- ✅ Custom-built with Claude API (agentboard + Anthropic SDK) +- ✅ search_docs tool using llms.txt custom sets (Tasks 2–4) +- ✅ create_pylon_issue with requiresConfirmation (Task 5) +- ✅ AgentRunner configured with Haiku model (Task 6) +- ✅ Anonymous + identified user modes (Task 6 headers, Task 7 ScalekitChat.identify) +- ✅ Widget integrated into Starlight Head.astro (Task 7) +- ✅ Thumbs-down triggering issue offer: handled by system prompt instruction to Claude + confirmation tool flow +- ⚠️ **llms.txt URL paths**: The `SLUG_TO_PATH` map in Task 3 uses assumed paths. Implementer must verify against the live docs site before deployment (step noted in Task 3). +- ⚠️ **Pylon API endpoint format**: Task 5 uses `POST /issues` on `api.usepylon.com`. Implementer must verify against Pylon's actual API docs. + +**Out of scope for Plan A** (covered in Plans B and C): + +- Pylon webhook listener and gap analyzer +- PR drafter agent +- Slack bot diff --git a/public/redesign/inbound-outbound.png b/public/redesign/inbound-outbound.png new file mode 100644 index 000000000..cc81952a3 Binary files /dev/null and b/public/redesign/inbound-outbound.png differ diff --git a/public/redesign/index.html b/public/redesign/index.html new file mode 100644 index 000000000..0cda875ae --- /dev/null +++ b/public/redesign/index.html @@ -0,0 +1,1752 @@ + + + + + + Scalekit website redesign — reference + + + + + + + +
+ +

Homepage

+

+ A developer-first homepage that replaces both the current marketing site and docs landing + page. Two-door entry: Auth for Agents (primary, purple) and Auth for Apps (secondary, + green). +

+ +
+ +
+
+
+
+
+
+
scalekit.com
+
+ + + + + +
+
Authentication infrastructure for AI-era applications
+

Auth used to be one problem.
AI made it two.

+

+ Your app needs to authenticate the people and agents connecting to it. Your AI + agents need to authenticate to the external services they connect to. These are + fundamentally different problems. Scalekit solves both. +

+ + +
+ Inbound and Outbound auth architecture diagram +
+ + +
+ +
+
Outbound · Primary product
+
Connect your agents
+
+ Token vault, delegated OAuth, auto-refresh. 100+ providers. Works with every major + AI framework. Your agents connect to anything without managing credentials. +
+
+ LangChain  ·  OpenAI  ·  Anthropic  ·  Vercel AI +  ·  Google ADK  ·  Mastra +
+
+ // Give your agent a Gmail tool
+ const + token = + await + scalekit
+   .getToken({ provider: + 'google',
+     userId, scopes: ['gmail.send'] }); +
+ Quickstart: Agent Auth → +   View 100+ providers +
+ + +
+
Inbound · For your app
+
Authenticate your app
+
+ Full-stack login, organizations, RBAC, SSO, SCIM, MCP server security. Everything + users and agents need to authenticate into your application. +
+
+ // Node.js — add login in 3 lines
+ const + url = + scalekit.getAuthorizationUrl({
+   redirectUri: process.env.REDIRECT_URI
+ });
+ res.redirect(url); +
+ Quickstart: For Apps → +
+
+
+ + + +
+
Already have an auth system? Add what you need:
+
+
+
Modular SSO
+
Add SAML/OIDC without replacing your stack
+
+
+
Modular SCIM
+
Plug-in user provisioning from Okta, Azure
+
+
+
MCP Auth
+
Secure your MCP server with OAuth 2.1
+
+
+
API Auth
+
M2M tokens and scoped API key management
+
+
+
+ SOC 2 Type II + ISO 27001 + GDPR + 99.99% uptime + US + EU data residency + → View pricing +
+
+
+ +
+ + +
+ +

Site structure

+

+ Two domains, two audiences, one source of truth. + scalekit.com is for human developers. docs.scalekit.com is a + machine-readable API layer for coding agents. +

+ +
+
+
scalekit.com — human developers
+
+
/   Homepage
+
+ Two-door hero (inbound / outbound)
+ Pricing · Security · Enterprise · Footer +
+ +
+ /for-agents/  ← Auth for Agents +
+
+ quickstart · overview · how it works
+ connected-accounts · token-vault
+ tool-calling · delegated-oauth
+ frameworks/  langchain · openai · anthropic · vercel · adk · mastra
+ providers/  google · slack · salesforce · +95 more +
+ +
+ /for-apps/  ← Auth for Apps +
+
+ quickstart · overview
+ full-stack-auth/  login · session · orgs · RBAC
+ modular-sso/ · modular-scim/
+ mcp-auth/ · api-auth/ +
+ +
+ /sdks/  shared across both +
+
node · python · go · java · expo
+ +
/apis/   /pricing/   /changelog/
+
+
+ +
+
docs.scalekit.com — coding agents
+
+
/   Agent entry point
+
+ llms.txt  ← product overview for LLMs
+ llms-full.txt  ← full context dump
+ sitemap.xml  ← machine-readable +
+ +
+ /mcp/  MCP server (SSE) +
+
+ search(query) tool
+ get_page(path) tool
+ list_products() tool
+ get_code_example(product, lang) tool +
+ +
+ /context/  structured context files +
+
+ for-agents.md · for-apps.md
+ sdks.md · errors.md +
+ +
+ /pages/  clean markdown mirror +
+
+ No sidebars · No JS · No images
+ Pure content for agent consumption
+ for-agents/** · for-apps/** · sdks/** +
+ +
+ /search  REST semantic search API +
+
GET /search?q=<query>
+ +
+ /openapi/  spec.json · spec.yaml +
+
+
+
+ +
+

+ Key insight: + docs.scalekit.com is not a parallel docs site — it's a machine-readable API layer over the + same content. Coding agents (Cursor, Claude Code, Copilot) hit docs.scalekit.com. Humans + hit scalekit.com. Same source of truth, two rendering targets. +

+
+
+ + +
+ +

Docs experience

+

+ Both product lines share the same doc shell. A product switcher in the sidebar lets + developers move between the two. Each sidebar has a persistent cross-sell prompt at the + bottom. +

+ +
+ + + +
+ + +
+
+
+
+
+
+
+
scalekit.com/for-agents/
+
+
+
+ +
+ 🤖 + For Agents + +
+ + +
+ +
+
For Agents › Get started › Overview
+
Agent Auth
+

+ Connect your AI agents to external services — Gmail, Slack, Salesforce, and 100+ more + — without managing OAuth flows, storing tokens, or handling refresh logic. +

+ +
+
What you'll build
+
+ 1. User connects their Google account via your app
+ 2. Scalekit stores the token securely in the vault
+ 3. Your agent calls scalekit.getToken() at task time
+ 4. Token is returned, refreshed automatically if expired +
+
+ +
+ + + + +
+
+ // Retrieve a token for a user's Google account
+ const + token = + await + scalekit.getToken({
+   provider: 'google',
+   userId: 'user_123',
+   scopes: ['https://www.googleapis.com/auth/gmail.send']
+ });

+ // token.accessToken is ready to use +
+
+ +
+
On this page
+
Overview
+
How it works
+
Prerequisites
+
Quickstart
+
Next steps
+
+
For AI assistants
+
docs.scalekit.com/mcp
+
+
+
+
+ + +
+
+
+
+
+
+
+
scalekit.com/for-apps/
+
+
+
+ +
+ 🏗️ + For Apps + +
+ + +
+ +
+
For Apps › Get started › Overview
+
Auth for Apps
+

+ Everything your B2B application needs for authentication — login, sessions, + organizations, RBAC, SSO, SCIM, and MCP server security — in one SDK. +

+ +
+
Products in this section
+
+ Full Stack Auth — complete login, + session, orgs, RBAC
+ Modular SSO — add SAML/OIDC to any + existing auth system
+ Modular SCIM — user provisioning from + Okta, Azure AD
+ MCP Auth — OAuth 2.1 security for your + MCP server
+ API Auth — M2M tokens and scoped API + keys +
+
+ +
+ + + + +
+
+ // Add login to your app
+ const + url = + scalekit.getAuthorizationUrl({
+   redirectUri: process.env.REDIRECT_URI
+ });
+ res.redirect(url); +
+
+ +
+
On this page
+
Overview
+
Products
+
When to use each
+
Quickstart
+
Next steps
+
+
For AI assistants
+
docs.scalekit.com/mcp
+
+
+
+
+ + +
+
+
+
+
+
+
+
+ docs.scalekit.com — machine-readable layer +
+
+
+

Agent entry points

+

+ This domain is not for humans. It's a programmatic API layer for coding agents (Cursor, + Claude Code, Copilot). Humans never need to visit it directly. +

+ +
/llms.txt
+
Product overview + SDK naming conventions + page index
+
/llms-full.txt
+
Full context dump — all docs in one file
+
+ +
+ /mcp + — SSE endpoint +
+
+ search(query)  — semantic search across all docs +
+
+ get_page(path)  — fetch any doc page as clean markdown +
+
+ list_products()  — list all products with descriptions +
+
+ get_code_example(product, lang)  — get code by product + language +
+
+ +
+ Add to your coding agent config: +
+
+ { "mcpServers": { "scalekit-docs": { "url": "https://docs.scalekit.com/mcp", "type": + "sse" } } } +
+ +
/context/for-agents.md
+
+ Full agent auth context — drop directly into an LLM context window +
+
/context/for-apps.md
+
Full app auth context for LLM consumption
+
+ +
/pages/**
+
+ Clean markdown mirror of all doc pages. No sidebars, no JS, no images. Pure content. +
+
+ +
/search?q=<query>
+
+ REST semantic search API — returns ranked doc excerpts + URLs +
+
+
+
+ + +
+ +

Pricing

+

+ Two independent pricing models — one per product line — displayed as tabs on a single + pricing section. Each model has its own unit economics. +

+ +
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
TierPriceIncludes
Starter$010 connected accounts · 5k tool calls/mo · 5 providers
GrowthCustom + 500 connected accounts · 100k tool calls/mo · 100+ providers · then per-unit +
EnterpriseCustomUnlimited · dedicated vault · custom residency · SLA
+
+ Overages: per connected account + per tool + call

+ Connected account — a single user's OAuth + connection to one external service (e.g. one user's Gmail)
+ Tool call — one call to getToken() by your + agent, regardless of whether the token was cached or refreshed +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
TierPriceIncludes
Starter$01k MAUs · 3 MAOs · 0 SSO connections
GrowthCustom10k MAUs · 50 MAOs · 3 SSO connections · then per-unit
EnterpriseCustomUnlimited MAUs/MAOs/SSO · custom domain · audit logs · SLA
+
+ Overages: per MAU + per MAO + per SSO + connection

+ MAU — monthly active user who authenticates into + your app
+ MAO — monthly active organization (a + tenant/workspace in your app)
+ SSO connection — one configured enterprise IdP + (Okta, Azure AD, etc.) for one organization +
+
+
+
+ + +
+ +

Design tokens

+

+ Color palette and visual language. Dark by default — signals developer-first. Purple for + Agent Auth. Green for App Auth. +

+ +
+
+
+
+
Background
+
#0a0a0f
+
+
+
+
+
+
Surface
+
#0d0d14
+
+
+
+
+
+
Border
+
#1e293b
+
+
+
+
+
+
Text primary
+
#f1f5f9
+
+
+
+
+
+
Text secondary
+
#64748b
+
+
+
+
+
+
Agent Auth accent
+
#6366f1 (purple)
+
+
+
+
+
+
App Auth accent
+
#10b981 (green)
+
+
+
+
+ Aa +
+
+
Typography
+
System UI / monospace code
+
+
+
+ +
+ SDK variable naming (non-negotiable): +   Node.js: scalekit   Python: scalekit_client   Go: + scalekitClient   Java: scalekitClient +
+
+ + + + + + + diff --git a/render.yaml b/render.yaml new file mode 100644 index 000000000..471dcd93a --- /dev/null +++ b/render.yaml @@ -0,0 +1,19 @@ +services: + - type: web + name: scalekit-chatbot-api + runtime: node + rootDir: services/chatbot-api + buildCommand: npm install && npm run build + startCommand: npm start + healthCheckPath: /health + envVars: + - key: ANTHROPIC_API_KEY + sync: false + - key: PYLON_API_TOKEN + sync: false + - key: PYLON_API_URL + value: https://api.usepylon.com + - key: PORT + value: 3001 + - key: NODE_VERSION + value: 20 diff --git a/services/chatbot-api/.env.example b/services/chatbot-api/.env.example new file mode 100644 index 000000000..2a4588573 --- /dev/null +++ b/services/chatbot-api/.env.example @@ -0,0 +1,4 @@ +ANTHROPIC_API_KEY=sk-ant-... +PYLON_API_TOKEN=pylon_... +DOCS_BASE_URL=https://docs.scalekit.com +PORT=3001 diff --git a/services/chatbot-api/README.md b/services/chatbot-api/README.md new file mode 100644 index 000000000..de56f92c1 --- /dev/null +++ b/services/chatbot-api/README.md @@ -0,0 +1,48 @@ +# chatbot-api + +Node.js Express service powering the Scalekit docs chatbot. Answers questions grounded in the docs llms.txt custom sets, and creates Pylon support issues for questions it can't answer. + +## Setup + +```bash +npm install +cp .env.example .env +# Fill in ANTHROPIC_API_KEY and PYLON_API_TOKEN in .env +``` + +## Run + +```bash +npm run dev # development with hot reload +npm start # production +``` + +## Test + +```bash +npm test +``` + +## Environment variables + +| Variable | Required | Description | +| ------------------- | -------- | --------------------------------------------------------------------- | +| `ANTHROPIC_API_KEY` | Yes | Anthropic API key | +| `PYLON_API_TOKEN` | Yes | Pylon API Bearer token | +| `DOCS_BASE_URL` | No | Docs URL for llms.txt fetching (default: `https://docs.scalekit.com`) | +| `PYLON_API_URL` | No | Pylon API base URL (default: `https://api.usepylon.com`) | +| `PORT` | No | Port to listen on (default: `3001`) | + +## Endpoints + +- `GET /health` — health check +- `POST /api/chat` — send a message, returns SSE stream +- `POST /api/chat/confirm` — confirm or cancel a pending tool action (e.g. create Pylon issue) + +## How it works + +1. User sends a question to `POST /api/chat` +2. The agent calls `search_docs`, which classifies the query topic and fetches the matching llms.txt custom set from docs.scalekit.com +3. Claude answers using only the fetched docs content +4. If the question can't be answered, the agent offers to create a Pylon issue +5. If the user confirms, `create_pylon_issue` fires (requires explicit confirmation via `POST /api/chat/confirm`) diff --git a/services/chatbot-api/package-lock.json b/services/chatbot-api/package-lock.json new file mode 100644 index 000000000..adac9afff --- /dev/null +++ b/services/chatbot-api/package-lock.json @@ -0,0 +1,1772 @@ +{ + "name": "chatbot-api", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "chatbot-api", + "version": "0.1.0", + "dependencies": { + "@ai-sdk/anthropic": "^1.0.0", + "ai": "^4.0.0", + "dotenv": "^16.0.0", + "express": "^5.0.0", + "zod": "^3.0.0" + }, + "devDependencies": { + "@types/express": "^5.0.0", + "@types/node": "^20.0.0", + "tsx": "^4.0.0", + "typescript": "^5.0.0" + } + }, + "node_modules/@ai-sdk/anthropic": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-1.2.12.tgz", + "integrity": "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz", + "integrity": "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.8.tgz", + "integrity": "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "nanoid": "^3.3.8", + "secure-json-parse": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.23.8" + } + }, + "node_modules/@ai-sdk/react": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-1.2.12.tgz", + "integrity": "sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider-utils": "2.2.8", + "@ai-sdk/ui-utils": "1.2.11", + "swr": "^2.2.5", + "throttleit": "2.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@ai-sdk/ui-utils": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-1.2.11.tgz", + "integrity": "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.23.8" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/diff-match-patch": { + "version": "1.0.36", + "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz", + "integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==", + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ai": { + "version": "4.3.19", + "resolved": "https://registry.npmjs.org/ai/-/ai-4.3.19.tgz", + "integrity": "sha512-dIE2bfNpqHN3r6IINp9znguYdhIOheKW2LDigAMrgt/upT3B8eBGPSCblENvaZGoq+hxaN9fSMzjWpbqloP+7Q==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8", + "@ai-sdk/react": "1.2.12", + "@ai-sdk/ui-utils": "1.2.11", + "@opentelemetry/api": "1.9.0", + "jsondiffpatch": "0.6.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "license": "Apache-2.0" + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/jsondiffpatch": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz", + "integrity": "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==", + "license": "MIT", + "dependencies": { + "@types/diff-match-patch": "^1.0.36", + "chalk": "^5.3.0", + "diff-match-patch": "^1.0.5" + }, + "bin": { + "jsondiffpatch": "bin/jsondiffpatch.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", + "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "license": "BSD-3-Clause" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/swr": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz", + "integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/throttleit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", + "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/services/chatbot-api/package.json b/services/chatbot-api/package.json new file mode 100644 index 000000000..5e6a57f29 --- /dev/null +++ b/services/chatbot-api/package.json @@ -0,0 +1,23 @@ +{ + "name": "chatbot-api", + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js" + }, + "dependencies": { + "@ai-sdk/anthropic": "^1.0.0", + "ai": "^4.0.0", + "dotenv": "^16.0.0", + "express": "^5.0.0", + "zod": "^3.0.0" + }, + "devDependencies": { + "@types/express": "^5.0.0", + "@types/node": "^20.0.0", + "tsx": "^4.0.0", + "typescript": "^5.0.0" + } +} diff --git a/services/chatbot-api/src/agent.ts b/services/chatbot-api/src/agent.ts new file mode 100644 index 000000000..87b35d439 --- /dev/null +++ b/services/chatbot-api/src/agent.ts @@ -0,0 +1,23 @@ +export const SYSTEM_PROMPT = `You are the Scalekit docs assistant. Be brief and direct — give concise answers, not full documentation. + +When answering questions: +1. ALWAYS call search_docs first before answering any product question. +2. Answer ONLY using content returned by search_docs. Do not use prior knowledge. +3. Be concise: 2–4 sentences that directly address the question. No walls of text. +4. Always end your response with a sources section. Format it exactly like this (one source per line): + +--- +**Sources:** +[Page title](url) +[Page title](url) + +Use the URLs returned by search_docs. Only include sources relevant to your answer. 2–4 sources maximum. + +If search_docs does not contain a clear answer to the question: +- Say explicitly: "I don't have a confident answer for this in the docs." +- Offer to create a support issue: "Would you like me to create a support issue so our team can follow up?" +- If the user confirms, call create_pylon_issue with the original question and a summary. + +Never guess or invent information about Scalekit products. +Never reproduce large sections of documentation verbatim — snippet and link, don't dump. +Never invent or assume API endpoints, SDK method names, parameter names, or return types. If the exact method signature or endpoint is not present in the search results, say so explicitly and direct the user to the API reference.` diff --git a/services/chatbot-api/src/index.ts b/services/chatbot-api/src/index.ts new file mode 100644 index 000000000..4f80db087 --- /dev/null +++ b/services/chatbot-api/src/index.ts @@ -0,0 +1,50 @@ +import 'dotenv/config' +import express from 'express' +import { streamText } from 'ai' +import { anthropic } from '@ai-sdk/anthropic' +import { searchDocsTool } from './tools/search-docs.js' +import { createPylonIssueTool } from './tools/create-pylon-issue.js' +import { SYSTEM_PROMPT } from './agent.js' + +const app = express() +app.use(express.json()) + +app.use((req, res, next) => { + res.setHeader('Access-Control-Allow-Origin', '*') + res.setHeader('Access-Control-Allow-Headers', 'Content-Type') + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') + if (req.method === 'OPTIONS') { + res.sendStatus(200) + return + } + next() +}) + +app.get('/health', (_req, res) => { + res.json({ status: 'ok' }) +}) + +app.post('/api/chat', async (req, res) => { + const { messages, userId = '', orgId = '' } = req.body + + const result = streamText({ + model: anthropic('claude-haiku-4-5-20251001'), + system: SYSTEM_PROMPT, + messages, + maxTokens: 1024, + maxSteps: 5, + tools: { + search_docs: searchDocsTool, + create_pylon_issue: createPylonIssueTool({ userId, orgId }), + }, + }) + + result.pipeDataStreamToResponse(res) +}) + +const port = process.env.PORT ?? 3001 +app.listen(port, () => { + console.log(`chatbot-api listening on http://localhost:${port}`) +}) + +export { app } diff --git a/services/chatbot-api/src/lib/search-algolia.ts b/services/chatbot-api/src/lib/search-algolia.ts new file mode 100644 index 000000000..5fd40aa8e --- /dev/null +++ b/services/chatbot-api/src/lib/search-algolia.ts @@ -0,0 +1,56 @@ +const ALGOLIA_APP_ID = process.env.ALGOLIA_APP_ID ?? '7554BDRAJD' +const ALGOLIA_API_KEY = process.env.ALGOLIA_API_KEY ?? 'b2fecf525a556f05d46ef2389ad7e4b6' +const ALGOLIA_INDEX = process.env.ALGOLIA_INDEX ?? 'scalekit-starlight-crawler' + +interface AlgoliaHit { + objectID: string + url: string + hierarchy: { + lvl0?: string + lvl1?: string + lvl2?: string + lvl3?: string + } + content?: string +} + +interface AlgoliaResponse { + hits: AlgoliaHit[] +} + +export interface DocSnippet { + title: string + url: string + snippet: string +} + +export async function searchAlgolia(query: string, hitsPerPage = 6): Promise { + const url = `https://${ALGOLIA_APP_ID}-dsn.algolia.net/1/indexes/${ALGOLIA_INDEX}/query` + + const res = await fetch(url, { + method: 'POST', + headers: { + 'X-Algolia-API-Key': ALGOLIA_API_KEY, + 'X-Algolia-Application-Id': ALGOLIA_APP_ID, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query, + hitsPerPage, + attributesToRetrieve: ['url', 'hierarchy', 'content'], + attributesToHighlight: [], + }), + }) + + if (!res.ok) { + throw new Error(`Algolia search failed: ${res.status}`) + } + + const data = (await res.json()) as AlgoliaResponse + + return data.hits.map((hit) => ({ + title: hit.hierarchy.lvl2 ?? hit.hierarchy.lvl1 ?? hit.hierarchy.lvl0 ?? 'Scalekit Docs', + url: hit.url, + snippet: hit.content ?? '', + })) +} diff --git a/services/chatbot-api/src/tools/create-pylon-issue.ts b/services/chatbot-api/src/tools/create-pylon-issue.ts new file mode 100644 index 000000000..2efd634af --- /dev/null +++ b/services/chatbot-api/src/tools/create-pylon-issue.ts @@ -0,0 +1,74 @@ +import { tool } from 'ai' +import { z } from 'zod' + +interface Context { + userId: string + orgId: string +} + +export function createPylonIssueTool(ctx: Context) { + return tool({ + description: + 'Create a support issue in Pylon when the documentation does not contain a confident answer. ' + + 'Only call this tool after the user has explicitly confirmed they want to create an issue. ' + + 'Include the original question and a summary of what was searched.', + parameters: z.object({ + question: z + .string() + .describe("The user's original question that could not be answered from docs."), + conversation_summary: z + .string() + .describe('Brief summary of the conversation and what docs were searched.'), + user_email: z + .string() + .optional() + .describe('Optional email address provided by the user for follow-up.'), + }), + execute: async ({ question, conversation_summary, user_email }) => { + const token = process.env.PYLON_API_TOKEN + if (!token) throw new Error('PYLON_API_TOKEN environment variable is not set') + + const apiUrl = process.env.PYLON_API_URL ?? 'https://api.usepylon.com' + const title = `Docs gap: ${question.slice(0, 80)}${question.length > 80 ? '…' : ''}` + + const body: Record = { + title, + body: [ + `**Question:** ${question}`, + '', + `**Context:** ${conversation_summary}`, + '', + '**Source:** docs chatbot', + ctx.userId ? `**User:** ${ctx.userId}` : '', + ctx.orgId ? `**Org:** ${ctx.orgId}` : '', + user_email ? `**Contact:** ${user_email}` : '', + ] + .filter(Boolean) + .join('\n'), + tags: ['docs-gap'], + } + + if (ctx.userId) body.requester_email = ctx.userId + + const res = await fetch(`${apiUrl}/issues`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }) + + if (!res.ok) { + const text = await res.text().catch(() => '') + throw new Error(`Pylon API error ${res.status}: ${text}`) + } + + const issue = (await res.json()) as { id: string } + return { + issueId: issue.id, + message: `Support issue created (${issue.id}). Our team will follow up${ctx.userId ? ` with ${ctx.userId}` : ''}.`, + } + }, + }) +} diff --git a/services/chatbot-api/src/tools/search-docs.ts b/services/chatbot-api/src/tools/search-docs.ts new file mode 100644 index 000000000..414f7f926 --- /dev/null +++ b/services/chatbot-api/src/tools/search-docs.ts @@ -0,0 +1,19 @@ +import { tool } from 'ai' +import { z } from 'zod' +import { searchAlgolia } from '../lib/search-algolia.js' + +export const searchDocsTool = tool({ + description: + 'Search Scalekit documentation to answer questions. ' + + 'Always call this tool before answering any question about Scalekit products ' + + '(FSA, SSO, SCIM, Agent Auth, MCP, M2M, SDK). ' + + 'Returns relevant snippets with source URLs.', + parameters: z.object({ + query: z.string().describe('The user question to search for in the docs'), + }), + execute: async ({ query }) => { + const snippets = await searchAlgolia(query) + if (snippets.length === 0) return 'No relevant documentation found for this query.' + return snippets.map((s) => `## ${s.title}\nURL: ${s.url}\n${s.snippet}`).join('\n\n---\n\n') + }, +}) diff --git a/services/chatbot-api/tests/agent.test.ts b/services/chatbot-api/tests/agent.test.ts new file mode 100644 index 000000000..6038d1880 --- /dev/null +++ b/services/chatbot-api/tests/agent.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect, vi, afterEach } from 'vitest' + +vi.mock('@anthropic-ai/sdk', () => { + return { + default: vi.fn().mockImplementation(() => ({ + messages: { + create: vi.fn().mockResolvedValue({ + id: 'msg_test', + content: [{ type: 'text', text: 'Based on the docs, here is how FSA sessions work...' }], + stop_reason: 'end_turn', + usage: { input_tokens: 100, output_tokens: 50 }, + }), + }, + })), + } +}) + +vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + text: async () => '# FSA Docs\n\nSession management works by...', + }), +) + +describe('createAgent', () => { + afterEach(() => { + vi.clearAllMocks() + }) + + it('creates an agent with search_docs and create_pylon_issue tools', async () => { + process.env.ANTHROPIC_API_KEY = 'test-key' + const { createAgent } = await import('../src/agent.js') + const { runner, tools } = createAgent() + + expect(runner).toBeDefined() + expect(tools.size).toBe(2) + }) + + it('registers search_docs without confirmation and create_pylon_issue with confirmation', async () => { + process.env.ANTHROPIC_API_KEY = 'test-key' + const { createAgent } = await import('../src/agent.js') + const { tools } = createAgent() + + expect(tools.requiresConfirmation('search_docs')).toBe(false) + expect(tools.requiresConfirmation('create_pylon_issue')).toBe(true) + }) +}) diff --git a/services/chatbot-api/tests/tools/create-pylon-issue.test.ts b/services/chatbot-api/tests/tools/create-pylon-issue.test.ts new file mode 100644 index 000000000..431d1f2fd --- /dev/null +++ b/services/chatbot-api/tests/tools/create-pylon-issue.test.ts @@ -0,0 +1,107 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { createPylonIssueTool } from '../../src/tools/create-pylon-issue.js' +import type { AgentContext } from '@scalekit/agentkit' + +const MOCK_ISSUE_RESPONSE = { + id: 'issue_abc123', + title: 'Docs gap: passkeys + SAML SSO', + status: 'open', +} + +describe('createPylonIssueTool', () => { + beforeEach(() => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + json: async () => MOCK_ISSUE_RESPONSE, + }), + ) + process.env.PYLON_API_TOKEN = 'test-pylon-token' + process.env.PYLON_API_URL = 'https://api.usepylon.com' + }) + + afterEach(() => { + vi.unstubAllGlobals() + delete process.env.PYLON_API_TOKEN + delete process.env.PYLON_API_URL + }) + + it('has correct tool definition', () => { + expect(createPylonIssueTool.definition.name).toBe('create_pylon_issue') + expect(createPylonIssueTool.definition.input_schema.properties).toHaveProperty('question') + expect(createPylonIssueTool.definition.input_schema.properties).toHaveProperty( + 'conversation_summary', + ) + }) + + it('requires confirmation before executing', () => { + expect(createPylonIssueTool.requiresConfirmation).toBe(true) + }) + + it('creates an issue with user context when user is identified', async () => { + const ctx: AgentContext = { + userId: 'jane@acme.com', + orgId: 'org_acme', + isAdmin: false, + } + + const result = (await createPylonIssueTool.execute( + { + question: 'Can passkeys and SAML SSO coexist for the same org?', + conversation_summary: + 'User asked about passkeys + SAML, no confident answer found in docs.', + }, + ctx, + )) as { issueId: string; message: string } + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('usepylon.com'), + expect.objectContaining({ + method: 'POST', + headers: expect.objectContaining({ + Authorization: 'Bearer test-pylon-token', + }), + }), + ) + expect(result.issueId).toBe('issue_abc123') + expect(result.message).toContain('issue_abc123') + }) + + it('creates an issue for anonymous users without user context', async () => { + const ctx: AgentContext = { + userId: '', + orgId: '', + isAdmin: false, + } + + await createPylonIssueTool.execute( + { question: 'How does M2M auth work?', conversation_summary: 'No answer found.' }, + ctx, + ) + + expect(fetch).toHaveBeenCalled() + }) + + it('throws with a clear message when PYLON_API_TOKEN is missing', async () => { + delete process.env.PYLON_API_TOKEN + const ctx: AgentContext = { userId: '', orgId: '', isAdmin: false } + + await expect( + createPylonIssueTool.execute({ question: 'test', conversation_summary: 'test' }, ctx), + ).rejects.toThrow('PYLON_API_TOKEN') + }) + + it('throws when Pylon API returns an error', async () => { + ;(fetch as ReturnType).mockResolvedValueOnce({ + ok: false, + status: 401, + text: async () => 'Unauthorized', + }) + const ctx: AgentContext = { userId: '', orgId: '', isAdmin: false } + + await expect( + createPylonIssueTool.execute({ question: 'test', conversation_summary: 'test' }, ctx), + ).rejects.toThrow('Pylon API error 401') + }) +}) diff --git a/services/chatbot-api/tests/tools/search-docs.test.ts b/services/chatbot-api/tests/tools/search-docs.test.ts new file mode 100644 index 000000000..c481bc775 --- /dev/null +++ b/services/chatbot-api/tests/tools/search-docs.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { searchDocsTool } from '../../src/tools/search-docs.js' + +const MOCK_FSA_CONTENT = '# Full Stack Auth\n\nScalekit FSA documentation content here.' + +describe('searchDocsTool', () => { + beforeEach(() => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + text: async () => MOCK_FSA_CONTENT, + }), + ) + process.env.DOCS_BASE_URL = 'https://docs.scalekit.com' + }) + + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('has correct tool definition for Claude', () => { + expect(searchDocsTool.definition.name).toBe('search_docs') + expect(searchDocsTool.definition.description).toContain('search') + expect(searchDocsTool.definition.input_schema.properties).toHaveProperty('query') + }) + + it('returns docs content for a query', async () => { + const result = await searchDocsTool.execute( + { query: 'How do I set up RBAC?' }, + { userId: 'anon', orgId: '', isAdmin: false }, + ) + expect(result).toContain('Full Stack Auth') + }) + + it('accepts optional topic override', async () => { + const result = await searchDocsTool.execute( + { query: 'How do I set up RBAC?', topic: 'sso' }, + { userId: 'anon', orgId: '', isAdmin: false }, + ) + expect(result).toBeDefined() + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('enterprise-sso'), + expect.any(Object), + ) + }) + + it('does not require confirmation', () => { + expect(searchDocsTool.requiresConfirmation).toBeFalsy() + }) +}) diff --git a/services/chatbot-api/tsconfig.json b/services/chatbot-api/tsconfig.json new file mode 100644 index 000000000..0a639d80c --- /dev/null +++ b/services/chatbot-api/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/services/chatbot-api/vitest.config.ts b/services/chatbot-api/vitest.config.ts new file mode 100644 index 000000000..f1881c183 --- /dev/null +++ b/services/chatbot-api/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'node', + globals: true, + }, +}) diff --git a/src/components/chatbot/ChatbotWidget.vue b/src/components/chatbot/ChatbotWidget.vue new file mode 100644 index 000000000..6661f99b9 --- /dev/null +++ b/src/components/chatbot/ChatbotWidget.vue @@ -0,0 +1,660 @@ + + + + + diff --git a/src/components/chatbot/renderMarkdown.ts b/src/components/chatbot/renderMarkdown.ts new file mode 100644 index 000000000..b51db0f87 --- /dev/null +++ b/src/components/chatbot/renderMarkdown.ts @@ -0,0 +1,112 @@ +function escHtml(s: string): string { + return s.replace(/&/g, '&').replace(//g, '>') +} + +function inlineMd(text: string): string { + return escHtml(text) + .replace(/`([^`]+)`/g, '$1') + .replace(/\*\*([^*\n]+)\*\*/g, '$1') + .replace(/\*([^*\n]+)\*/g, '$1') + .replace( + /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, + '$1', + ) +} + +export function renderMarkdown(text: string): string { + const lines = text.split('\n') + let html = '' + let i = 0 + let inSources = false + + while (i < lines.length) { + const line = lines[i] + + // Horizontal rule — check if next non-empty line is **Sources:** + if (/^[-*_]{3,}$/.test(line.trim())) { + let j = i + 1 + while (j < lines.length && lines[j].trim() === '') j++ + if (j < lines.length && /^\*\*Sources:\*\*/.test(lines[j])) { + html += '
Sources' + inSources = true + i = j + 1 + continue + } + html += '
' + i++ + continue + } + + // Inside sources section + if (inSources) { + if (line.trim() === '') { + i++ + continue + } + const linkMatch = line.match(/^\[([^\]]+)\]\((https?:\/\/[^)]+)\)/) + if (linkMatch) { + html += `${escHtml(linkMatch[1])}` + } else { + html += `${inlineMd(line)}` + } + i++ + continue + } + + // Fenced code block + if (line.startsWith('```')) { + let code = '' + i++ + while (i < lines.length && !lines[i].startsWith('```')) { + code += escHtml(lines[i]) + '\n' + i++ + } + html += `
${code.trimEnd()}
` + i++ + continue + } + + // Headings + const hm = line.match(/^(#{1,3}) (.+)/) + if (hm) { + html += `${inlineMd(hm[2])}` + i++ + continue + } + + // Unordered list + if (/^[-*+] /.test(line)) { + html += '
    ' + while (i < lines.length && /^[-*+] /.test(lines[i])) { + html += `
  • ${inlineMd(lines[i].replace(/^[-*+] /, ''))}
  • ` + i++ + } + html += '
' + continue + } + + // Ordered list + if (/^\d+\. /.test(line)) { + html += '
    ' + while (i < lines.length && /^\d+\. /.test(lines[i])) { + html += `
  1. ${inlineMd(lines[i].replace(/^\d+\. /, ''))}
  2. ` + i++ + } + html += '
' + continue + } + + // Blank line + if (line.trim() === '') { + i++ + continue + } + + // Paragraph + html += `

${inlineMd(line)}

` + i++ + } + + if (inSources) html += '
' + return html +} diff --git a/src/components/overrides/Head.astro b/src/components/overrides/Head.astro index 4f694bba7..3431955e7 100644 --- a/src/components/overrides/Head.astro +++ b/src/components/overrides/Head.astro @@ -1,7 +1,10 @@ --- import Default from '@astrojs/starlight/components/Head.astro' +import ChatbotWidget from '../chatbot/ChatbotWidget.vue' --- + + diff --git a/src/components/templates/coding-agents/_mcp-auth-claude-code.mdx b/src/components/templates/coding-agents/_mcp-auth-claude-code.mdx index d759a6d85..80109fbde 100644 --- a/src/components/templates/coding-agents/_mcp-auth-claude-code.mdx +++ b/src/components/templates/coding-agents/_mcp-auth-claude-code.mdx @@ -1,6 +1,7 @@ import { Steps, Aside, Tabs, TabItem } from '@astrojs/starlight/components' import { Image } from 'astro:assets' import enableClaudePluginGif from '@/assets/docs/ai-assisted-mcp-quickstart/2.gif' +import skillActivationImg from '@/assets/docs/ai-assisted-mcp-quickstart/skill-activation.png' @@ -79,7 +80,7 @@ import enableClaudePluginGif from '@/assets/docs/ai-assisted-mcp-quickstart/2.gi When you submit this prompt, Claude Code loads the MCP authentication skill from the marketplace -> analyzes your existing MCP server structure -> generates authentication middleware with token validation -> creates the OAuth discovery endpoint -> configures environment variable handling. Claude Code activating MCP authentication skill diff --git a/src/pages/redesign.astro b/src/pages/redesign.astro new file mode 100644 index 000000000..3993f6d87 --- /dev/null +++ b/src/pages/redesign.astro @@ -0,0 +1,7 @@ +--- +// Local dev only — imports and renders the static public HTML directly. +// Firebase hosting serves public/redesign/index.html as a static file. +import htmlContent from '../../public/redesign/index.html?raw' +--- + +