diff --git a/.env.example b/.env.example index 391cceb..ed42cf3 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,13 @@ LIBSQL_URL=http://libsql:8080 BETTER_AUTH_SECRET=CHANGEME BETTER_AUTH_URL=http://localhost:3001 +# === Envelope encryption for per-tenant integration keys === +# 32 bytes, base64 — generate with: openssl rand -base64 32 +# Used by runtime/src/lib/crypto-envelope.ts to wrap per-row DEKs that +# encrypt project_integrations.encrypted_key. Rotating this key requires +# re-wrapping every row's DEK; losing it makes stored keys unrecoverable. +APP_MASTER_KEY=CHANGEME + # === LLM / AI (OpenRouter) === # Get your key at https://openrouter.ai/settings/keys OPENROUTER_API_KEY=CHANGEME @@ -27,9 +34,11 @@ OPENROUTER_API_KEY=CHANGEME # === Integrations === # === Integrations: Linear === -# Get key at: https://linear.app/agentic-engineering-agency/settings/api -# Dashboard: https://linear.app/agentic-engineering-agency/team/TRI/all +# Get key at: your Linear workspace settings → API +# Team ID: go to your team dashboard → copy the team UUID from the URL +# Dashboard: https://linear.app/your-workspace/team/YOUR_TEAM_KEY/all LINEAR_API_KEY=CHANGEME +LINEAR_TEAM_ID=645a639b-39e2-4abe-8ded-3346d2f79f9f # Default: TRI team from agentic-engineering-agency GITHUB_TOKEN=CHANGEME # GitHub PAT for PR comments (repo scope) # === Integrations: Resend (Email) === @@ -46,9 +55,9 @@ SLACK_CHANNEL_ID=CHANGEME SLACK_SIGNING_SECRET=CHANGEME # Team member Slack user IDs (for @mentions in notifications) # Find IDs: click a user profile in Slack → "..." → "Copy member ID" -SLACK_USER_CHENKO= SLACK_USER_FERNANDO= SLACK_USER_KOKI= +SLACK_USER_CHENKO= SLACK_USER_LALO= # === Integrations: Jira === @@ -56,55 +65,15 @@ JIRA_BASE_URL=https://your-domain.atlassian.net JIRA_EMAIL=your-email@example.com JIRA_API_TOKEN=CHANGEME -# === Cloudflare Tunnel === -# Token for the triage-hackathon tunnel (routes langfuse.agenticengineering.lat → langfuse-web:3000) -# Get with: cloudflared tunnel token triage-hackathon -CLOUDFLARE_TUNNEL_TOKEN=CHANGEME - -# === Langfuse Core === -# Generate with: openssl rand -hex 32 -ENCRYPTION_KEY=CHANGEME -SALT=CHANGEME -NEXTAUTH_SECRET=CHANGEME -NEXTAUTH_URL=https://langfuse.agenticengineering.lat -# External URL via Cloudflare Tunnel — used by OpenRouter broadcast and Mastra LangfuseExporter -LANGFUSE_BASEURL=https://langfuse.agenticengineering.lat -LANGFUSE_PUBLIC_KEY=CHANGEME -LANGFUSE_SECRET_KEY=CHANGEME -TELEMETRY_ENABLED=true -LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=false -# URL shown in the frontend "Observability" link in the sidebar. -# Production (via Cloudflare Tunnel): https://langfuse.agenticengineering.lat -# Demo / local dev: http://localhost:3000 -# Note: exposed to the browser at build time (VITE_* prefix). -VITE_OBSERVABILITY_URL=http://localhost:3000 - -# === Langfuse Infrastructure === -# Generate with: openssl rand -hex 32 -CLICKHOUSE_PASSWORD=CHANGEME -REDIS_AUTH=CHANGEME -MINIO_ROOT_USER=minio -MINIO_ROOT_PASSWORD=CHANGEME -LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=CHANGEME -LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=CHANGEME -LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=CHANGEME -LANGFUSE_S3_BATCH_EXPORT_ENABLED=false - -# === Langfuse Init (Headless Setup — auto-creates org/project/user) === -LANGFUSE_INIT_ORG_ID=triage-org -LANGFUSE_INIT_ORG_NAME=Triage SRE -LANGFUSE_INIT_PROJECT_ID=triage-project -LANGFUSE_INIT_PROJECT_NAME=Triage Agent -LANGFUSE_INIT_PROJECT_PUBLIC_KEY=CHANGEME -LANGFUSE_INIT_PROJECT_SECRET_KEY=CHANGEME -LANGFUSE_INIT_USER_EMAIL=admin@agenticengineering.lat -LANGFUSE_INIT_USER_NAME=Admin -LANGFUSE_INIT_USER_PASSWORD=CHANGEME - -# === Postgres (Langfuse backend DB) === -POSTGRES_USER=postgres -POSTGRES_PASSWORD=CHANGEME -POSTGRES_DB=langfuse +# === Webhook testing (optional, dev-only) === +# scripts/simulate-webhook.sh signs its payloads with HMAC-SHA256. By default +# it reads the secret from the webhook_secrets table (populated by POST +# /api/linear/webhook/setup once the webhook is registered with Linear). +# If you don't have a registered webhook yet — e.g. you're testing locally +# without ngrok — uncomment and set this to any non-empty string and export +# it only in the shell that runs simulate-webhook.sh. Do NOT set this in the +# running runtime's env: the server always reads from the DB. +# WEBHOOK_SECRET=dev-only-not-a-real-secret # === Frontend === # Valid values: static (production, serves built SPA) or dev (proxies to Vite HMR) diff --git a/.mcp.json b/.mcp.json index b3d81b9..a7f8b9e 100644 --- a/.mcp.json +++ b/.mcp.json @@ -8,6 +8,10 @@ "@mastra/mcp-docs-server@latest" ], "env": {} + }, + "linear": { + "command": "npx", + "args": ["-y", "mcp-remote", "https://mcp.linear.app/mcp"] } } } \ No newline at end of file diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..396c422 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,305 @@ +# Multi-Project Triage Architecture + +## Overview + +The Triage application has been refactored from a single-project model to a **multi-project architecture** where each project is an independent entity with its own: +- Repository configuration (URL, branch) +- Integration credentials (Linear token, GitHub token, Slack webhook) +- Wiki/RAG data (documents, embeddings, chunks) +- Project-scoped workflows and data + +This enables parallel, isolated execution of triage workflows across multiple codebases without credential or data leakage. + +## Data Model + +### Core Entity: Project + +Projects are the root entity in the system, stored in the `projects` table with: + +```sql +CREATE TABLE projects ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + repo_url TEXT NOT NULL, + repo_default_branch TEXT DEFAULT 'main', + + -- Integration credentials (per-project) + linear_token TEXT, + linear_team_id TEXT, + linear_webhook_id TEXT, + linear_webhook_url TEXT, + + github_token TEXT, + github_repo_owner TEXT, + github_repo_name TEXT, + + slack_enabled BOOLEAN DEFAULT 0, + slack_webhook_url TEXT, + slack_channel_id TEXT, + + -- Wiki/RAG status + wiki_status TEXT DEFAULT 'idle', + wiki_error TEXT, + documents_count INTEGER DEFAULT 0, + chunks_count INTEGER DEFAULT 0, + + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); +``` + +### Related Tables + +- **wiki_documents**: Repository documents for RAG, scoped to `project_id` +- **wiki_chunks**: Document chunks with embeddings, linked via `document_id → wiki_documents(id)` +- **conversations, linear_issues, local_tickets**: All have `project_id` foreign key + +**Key constraint**: All nested entities cascade-delete when a project is deleted. + +## Architecture Layers + +### 1. Backend API Routes + +**Location**: `runtime/src/lib/*.ts` + +#### A. Project Management Routes (`project-routes.ts`) +Implements CRUD operations on projects: +- `GET /projects` — list all projects +- `POST /projects` — create project + trigger wiki generation +- `GET /projects/:id` — get project details with stats +- `PATCH /projects/:id` — update name, repo URL, default branch +- `DELETE /projects/:id` — cascade delete project and all related data + +#### B. Integration Configuration Routes (`integration-routes.ts`) +Per-project integration setup and testing: +- `POST /projects/:projectId/settings/linear/test` — validate Linear token, save +- `POST /projects/:projectId/settings/linear/webhook` — register webhook with Linear API +- `POST /projects/:projectId/settings/github/test` — validate GitHub PAT +- `POST /projects/:projectId/settings/slack/test` — send test message to webhook +- `GET /projects/:projectId/settings/integrations` — fetch all integration status + +#### C. Project-Scoped Routes (`scoped-routes.ts`) +Per-project data queries using project's own credentials: +- `GET /projects/:projectId/linear/issues` — issues using project's Linear token +- `GET /projects/:projectId/linear/cycle` — active cycle for project's Linear team +- `GET /projects/:projectId/linear/members` — team members for project +- `POST /projects/:projectId/wiki/generate` — trigger wiki generation for project +- `GET /projects/:projectId/wiki/status` — wiki generation status for project + +**Implementation pattern**: +``` +Extract projectId → Fetch project row → Validate required integration (e.g., linear_token) +→ Use project's credentials → Filter data to project_id → Return +``` + +#### D. Middleware (`project-middleware.ts`) +Validates `projectId` parameter before route handlers: +- Extracts `projectId` from URL parameter +- Verifies project exists in database +- Attaches projectId to Hono context for use in handlers +- Returns 404 if project not found + +### 2. Frontend Routes & Components + +**Location**: `frontend/src/routes/*.tsx` + +#### Project Management (`projects.lazy.tsx`) +- List all projects +- Create new project (specify name, repo URL, branch) +- View project details with wiki generation status +- Edit project metadata +- Delete project + +**UI Flow**: +1. User creates project via form +2. Backend creates DB record and triggers wiki generation in background +3. Frontend polls `/api/projects/:id` to show progress +4. Wiki status updates in real-time + +#### Project Selector Component (`project-selector.tsx`) +- Dropdown showing all projects +- Stores current `projectId` in localStorage +- Emits custom event `triage:project-change` when selection changes +- Other components listen to this event and auto-refetch project-scoped data + +#### Integration Settings (`project-settings.lazy.tsx`) +Per-project integration configuration page accessible at `/project-settings`: + +**Three sections**: + +**Linear Integration** +- Input field for API token +- Test & Save button → calls `POST /api/projects/:projectId/settings/linear/test` +- On success: shows "Connected as ()" +- Webhook registration section (only if token configured): + - Pre-filled webhook URL: `{origin}/api/webhooks/linear` + - Optional Team ID input + - Register Webhook button → calls `POST /api/projects/:projectId/settings/linear/webhook` + +**GitHub Integration** +- Input fields: Owner, Repo, Personal Access Token +- Test & Save → `POST /api/projects/:projectId/settings/github/test` +- Shows authenticated user on success + +**Slack Integration** +- Webhook URL input (masked) +- Optional Channel ID input +- Test & Save → `POST /api/projects/:projectId/settings/slack/test` +- Backend sends real test message to webhook + +**UI Features**: +- Status indicator per integration (green checkmark "Configured" or red X "Not configured") +- Masked input fields for sensitive values +- Loading states and error messages +- Query invalidation to refresh status after save +- Integrations are project-scoped via `useCurrentProjectId()` hook + +## Data Flow: Creating a Project & Generating Wiki + +``` +1. User POST /projects { name, repositoryUrl, branch } + ↓ +2. Backend creates project row with status='pending' + ↓ +3. generateWiki(projectId, repoUrl, branch) spawned in background + ↓ +4. Wiki pipeline clones repo to /data/repos/{projectId} + ↓ +5. Documents extracted, vectorized, stored in wiki_documents + wiki_chunks + ↓ +6. Project status updated to 'ready' or 'error' + ↓ +7. Frontend polls GET /projects/:projectId/wiki/status + ↓ +8. Displays progress and final result +``` + +## Data Flow: Using a Project's Integrations + +``` +1. User navigates to /project-settings + ↓ +2. Frontend fetches GET /projects/:projectId/settings/integrations + ↓ +3. Shows configuration status (configured boolean, non-sensitive metadata) + ↓ +4. User enters Linear token and clicks Test & Save + ↓ +5. POST /api/projects/:projectId/settings/linear/test { token } + ↓ +6. Backend validates token via LinearClient.viewer + ↓ +7. Saves token to projects.linear_token (encrypted in production) + ↓ +8. Frontend invalidates query and refreshes status + ↓ +9. User registers webhook via POST /projects/:projectId/settings/linear/webhook + ↓ +10. Backend creates webhook via LinearClient.createWebhook() + ↓ +11. Webhook ID and URL stored in DB + ↓ +12. When issue moves to Done, webhook sends update to /api/webhooks/linear + ↓ +13. Endpoint resumes suspended workflow run for that project +``` + +## Data Isolation Guarantees + +**Project A and Project B cannot see each other's data**: + +1. **Credential isolation**: Each project stores its own Linear token, GitHub token, Slack webhook + - A Linear query by Project A uses Project A's token and team ID + - Project B's token/team is completely separate + +2. **Document isolation**: Wiki documents and chunks are filtered by `WHERE project_id = ?` + - Queries never leak documents from other projects + +3. **Conversation isolation**: Each conversation is tied to a projectId + - Conversations from Project A don't appear in Project B's history + +4. **Workflow isolation**: Workflows are triggered per-project + - The triage workflow runs independently for each project's issues + - Suspended runs are matched to specific projects + +## Security Considerations + +### Token Storage +- Linear, GitHub, and Slack tokens stored in plaintext in `projects` table +- **Production**: Should be encrypted at rest (e.g., via encryption middleware or encrypted columns) +- **Never** exposed in API responses (only `configured: boolean` status) +- Slack webhook URLs masked as `****` in GET responses + +### Webhook Validation +- Linear webhook URL pre-validated before saving +- Slack webhook tested by sending actual message +- GitHub token tested via API call +- All external HTTP calls wrapped in try-catch with meaningful errors + +### Future: Per-Project Authorization +- Middleware currently validates project existence only +- **TODO**: Add auth check to verify user owns/can access project +- Prevents unauthorized users from accessing other users' projects + +## File Organization + +``` +runtime/ + src/ + lib/ + project-routes.ts # GET/POST/PATCH/DELETE /projects + integration-routes.ts # POST /projects/:projectId/settings/* + scoped-routes.ts # GET/POST /projects/:projectId/linear/* /wiki/* + project-middleware.ts # projectId validation middleware + wiki-rag.ts # Wiki generation pipeline + project-selector.ts # Frontend hook + context + mastra/ + index.ts # Imports all route arrays and spreads into apiRoutes + +frontend/ + src/ + routes/ + projects.lazy.tsx # Project CRUD UI + project-settings.lazy.tsx # Integration settings UI + __root.tsx # Sidebar + ProjectSelector integration + components/ + project-selector.tsx # Dropdown + custom event system +``` + +## Testing + +Comprehensive test suite in `runtime/src/lib/project-routes.test.ts`: +- Mock in-memory database (Map-backed) +- Mock LinearClient, GitHub fetch, Slack fetch +- Test CRUD operations on projects +- Test integration token validation and saving +- Test scoped queries (data isolation) +- Test error cases (missing tokens, invalid projects, etc.) + +Run with: `npm test` or `vitest` + +## Deployment Considerations + +### Docker Volumes +- `/data/repos/{projectId}` — persistent repository storage + - Prevents re-cloning repos on every container restart + - Supports incremental updates via `git pull` + +### Environment Variables +- `LINEAR_API_KEY` — optional global Linear key (legacy, for backward compatibility) +- `LIBSQL_URL` — database connection string +- `OPENROUTER_API_KEY` — LLM provider + +### Database Initialization +- Run `runtime/init-db.mjs` on first deployment +- Creates all tables with proper foreign keys and indexes +- Indexes on `project_id` for fast filtering in scoped queries + +## Future Improvements + +1. **Encryption**: Encrypt credential columns at rest +2. **Rate limiting**: Implement exponential backoff for Linear API calls +3. **Idempotency**: Check if wiki generation already in progress before re-triggering +4. **Per-project auth**: Verify user owns project before returning data +5. **Webhook signature validation**: Verify Linear webhook requests are authentic +6. **Audit logging**: Track who created/modified each project and integration diff --git a/Caddyfile b/Caddyfile index 4c36795..7dd98bb 100644 --- a/Caddyfile +++ b/Caddyfile @@ -35,7 +35,31 @@ # --- Backend API routes (all proxied to runtime:4111) --- - # Mastra built-in API (/api/agents/*, /api/workflows/*, etc.) + # registerApiRoute routes are served WITHOUT the /api prefix by Mastra. + # Strip /api from the path for these specific route groups before proxying. + + # Observability API + @obs_api path /api/observability/* + handle @obs_api { + uri replace /api/observability /observability + reverse_proxy runtime:4111 + } + + # Projects API (project-routes.ts uses registerApiRoute('/projects/...')) + @projects_api_prefixed path /api/projects /api/projects/* + handle @projects_api_prefixed { + uri replace /api/projects /projects + reverse_proxy runtime:4111 + } + + # Integrations API + @integrations_api path /api/integrations/* + handle @integrations_api { + uri replace /api/integrations /integrations + reverse_proxy runtime:4111 + } + + # Mastra built-in API + inline routes (/api/agents/*, /api/workflows/*, /api/linear/*, /api/memory/*, etc.) handle /api/* { reverse_proxy runtime:4111 { flush_interval -1 @@ -65,6 +89,13 @@ } } + # Memory threads API (conversation history) + handle /memory/threads/* { + reverse_proxy runtime:4111 { + flush_interval -1 + } + } + # Project management API — only proxy XHR/fetch (not browser navigation) @projects_api { path /projects /projects/* @@ -81,6 +112,6 @@ # Frontend: static or dev mode handle { - import {$FRONTEND_MODE:static}-frontend + import static-frontend } } diff --git a/HANDOFF.md b/HANDOFF.md new file mode 100644 index 0000000..fa1d0d5 --- /dev/null +++ b/HANDOFF.md @@ -0,0 +1,411 @@ +# Triage Infrastructure Handoff Guide + +## Quick Start + +```bash +# Start all containers +docker compose up -d + +# Check health +docker compose ps + +# View logs +docker compose logs -f runtime +docker compose logs -f frontend +``` + +## Container Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ USER BROWSER (laptop) │ +│ http://localhost:3001 │ +└────────────────────────────┬────────────────────────────────┘ + │ (SSH tunnel) + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ DOCKER COMPOSE HOST │ +│ (Remote server: agent@100.89.94.39) │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ FRONTEND (Caddy on :3001) │ │ +│ │ - Serves built React SPA from /srv │ │ +│ │ - Reverse proxies /api/* to runtime:4111 │ │ +│ │ - Reverse proxies /chat, /auth/*, /webhooks/* │ │ +│ │ - Static file server with SPA fallback │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ ↓ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ RUNTIME (Node.js on :4111) │ │ +│ │ - Mastra agent orchestration │ │ +│ │ - Chat endpoint (/chat) │ │ +│ │ - API endpoints (/api/*) │ │ +│ │ - Webhook handlers (/webhooks/linear) │ │ +│ │ - Connects to: libsql, langfuse, linear, slack │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ ↓ ↓ ↓ │ +│ ┌─────────┐ ┌──────────────┐ ┌────────────────┐ │ +│ │ LibSQL │ │ Langfuse │ │ Linear API │ │ +│ │ :8080 │ │ Web :3000 │ │ (external) │ │ +│ │ :5001 │ │ Worker │ │ │ │ +│ │(database) │ :3030 │ │ Slack API │ │ +│ └─────────┘ │ │ │ (external) │ │ +│ │ Redis, Click- │ │ │ │ +│ │ House, Minio │ │ GitHub API │ │ +│ │ Postgres │ │ (external) │ │ +│ └──────────────┘ └────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Key Containers + +### Frontend (Caddy) +- **Port**: 3001 (localhost via Caddyfile) +- **Image**: `triage-dev-frontend` (built from Dockerfile.frontend) +- **Build**: `npm ci && tsc -b && vite build` → outputs to /srv +- **Mode**: Static file serving (FRONTEND_MODE=static in .env) +- **Critical Config**: Caddyfile (reverse proxy rules + security headers) +- **Health Check**: `wget --spider http://127.0.0.1:3001` + +### Runtime (Node.js + Mastra) +- **Port**: 4111 (localhost) +- **Image**: `triage-dev-runtime` (built from Dockerfile.runtime) +- **Language**: TypeScript/Node +- **Database**: Connects to LibSQL on http://libsql:8080 +- **Key Routes**: + - `POST /chat` — Main chat endpoint + - `GET /api/*` — API endpoints + - `POST /webhooks/linear` — Linear webhook handler + - `GET /health` — Health check +- **Health Check**: `wget --spider http://127.0.0.1:4111/health` +- **Env File**: Reads from .env (API keys, tokens, URLs) + +### LibSQL (Database) +- **Port**: 8080 (HTTP), 5001 (gRPC) +- **Image**: `ghcr.io/tursodatabase/libsql-server:v0.24.32` +- **Data Volume**: `libsql_data:/var/lib/sqld` +- **Purpose**: Stores chat history, threads, team members, projects +- **Health Check**: TCP check on port 8080 +- **No auth required** (internal only) + +### Langfuse (Observability) +- **Web**: http://localhost:3000 +- **Worker**: http://localhost:3030 +- **Postgres Backend**: :5432 +- **ClickHouse**: :8123, :9000 +- **Redis**: :6379 +- **MinIO (S3)**: :9090 (console), :9091 (API) +- **Purpose**: Logs all LLM calls, tool usage, reasoning steps +- **Credentials**: See .env (LANGFUSE_INIT_*) +- **External Access**: Via Cloudflare Tunnel to langfuse.agenticengineering.lat + +## Environment Variables (.env) + +**⚠️ CRITICAL: Never commit .env — always use .env.example as template** + +### App Configuration +``` +PORT=4111 # Runtime port +NODE_ENV=production # Always production in Docker +LIBSQL_URL=http://libsql:8080 # Database connection +BETTER_AUTH_SECRET= # Auth encryption (generate: openssl rand -hex 32) +BETTER_AUTH_URL=http://localhost:3001 # Frontend URL for auth callbacks +``` + +### LLM / AI +``` +OPENROUTER_API_KEY=sk-or-v1-... # OpenRouter API key (for Claude, models) +``` + +### External Integrations (API Keys) +``` +LINEAR_API_KEY=lin_api_... # Linear workspace API token +LINEAR_TEAM_ID=645a639b-39e2-4abe-... # Triage team UUID +RESEND_API_KEY=re_... # Email service (for notifications) +RESEND_FROM_EMAIL=triage@... # Verified sender email +GITHUB_TOKEN=github_pat_... # GitHub PAT (for PR comments) +SLACK_BOT_TOKEN=xoxb-... # Slack bot token +SLACK_CHANNEL_ID=D0ARNRLHLVB # Default Slack channel/DM +SLACK_SIGNING_SECRET=... # Webhook verification +``` + +### Slack User IDs (for @mentions) +``` +SLACK_USER_FERNANDO=U0ARKPZ7Z7V +SLACK_USER_KOKI=U0AS531HKBK +SLACK_USER_CHENKO=U0ARS63RS2E +SLACK_USER_LALO=U0ARQ466E22 +``` + +### Langfuse Core +``` +ENCRYPTION_KEY= # Generate: openssl rand -hex 32 +SALT= # Generate: openssl rand -hex 32 +NEXTAUTH_SECRET= # Generate: openssl rand -hex 32 +NEXTAUTH_URL=http://localhost:3000 +LANGFUSE_BASEURL=https://langfuse.agenticengineering.lat # External URL via tunnel +LANGFUSE_PUBLIC_KEY=pk_... +LANGFUSE_SECRET_KEY=sk_... +``` + +### Langfuse Infrastructure +``` +CLICKHOUSE_PASSWORD= +REDIS_AUTH= +MINIO_ROOT_USER=minio +MINIO_ROOT_PASSWORD= +LANGFUSE_S3_*_SECRET_ACCESS_KEY= +``` + +### Postgres (Langfuse) +``` +POSTGRES_USER=postgres +POSTGRES_PASSWORD= +POSTGRES_DB=langfuse +``` + +### Frontend +``` +FRONTEND_MODE=static # Options: static (production) or dev (Vite HMR) + # ⚠️ MUST be static for SSH tunnel access +``` + +### Caddy +``` +CADDY_PORT=3001 +RUNTIME_HOST=runtime +RUNTIME_PORT=4111 +``` + +### Cloudflare Tunnel +``` +CLOUDFLARE_TUNNEL_TOKEN=eyJhIjo... # Routes langfuse.agenticengineering.lat to langfuse-web:3000 +``` + +## Common Operations + +### View Logs +```bash +# Runtime (Mastra agent, API, webhooks) +docker compose logs -f runtime + +# Frontend (Caddy reverse proxy) +docker compose logs -f frontend + +# LibSQL (database) +docker compose logs -f libsql + +# All services +docker compose logs -f +``` + +### Restart a Service +```bash +docker compose restart runtime +docker compose restart frontend +docker compose restart libsql +``` + +### Rebuild Frontend (after code changes) +```bash +docker image rm triage-dev-frontend +docker compose up -d frontend +``` + +### Rebuild Runtime (after code changes) +```bash +docker image rm triage-dev-runtime +docker compose up -d runtime +``` + +### Rebuild Everything +```bash +docker compose down +docker image rm triage-dev-frontend triage-dev-runtime +docker compose up -d +``` + +### Check Container Health +```bash +docker compose ps +# STATUS column: "Healthy" or "starting" or "unhealthy" +``` + +### Access Database (LibSQL) +```bash +# From runtime container, curl to http://libsql:8080 +# Or use tursodatabase CLI on host +curl -X POST http://localhost:8080/execute -d "SELECT * FROM conversations" +``` + +### Access Langfuse Web UI +```bash +# Via tunnel: https://langfuse.agenticengineering.lat +# Or locally: http://localhost:3000 +# Login: admin@example.com / Triagefb1a0cc61520d44eXz (from .env) +``` + +## Disk Storage Management ⚠️ + +**Container volumes can grow large. Check regularly:** + +```bash +# See Docker disk usage +docker system df + +# List volumes +docker volume ls + +# Remove unused volumes (frees disk) +docker volume prune + +# See size of specific volume +docker run --rm -v libsql_data:/data busybox du -sh /data +docker run --rm -v langfuse_postgres_data:/data busybox du -sh /data +``` + +**Common Culprits:** +- `libsql_data` — Database file grows with conversation history +- `langfuse_postgres_data` — Observability data (traces, spans, logs) +- `langfuse_clickhouse_data` — Time-series analytics + +**If disk fills up (>85%):** +1. Check which volumes are largest: `docker system df -v` +2. Backup important data if needed +3. Remove old volumes: `docker volume rm ` +4. Clean unused images: `docker image prune -a` +5. Clean build cache: `docker builder prune -a` + +**To prevent issues:** +- Regularly review Langfuse data and clean old traces if needed +- Set up monitoring for disk usage on the host + +## Networking + +All containers are on the `app` network (bridge mode). + +**Container-to-Container Communication:** +- Frontend → Runtime: `http://runtime:4111` +- Runtime → LibSQL: `http://libsql:8080` +- Runtime → Langfuse: `http://langfuse-web:3000` (for trace export) +- External integrations: Via internet (Linear, GitHub, Slack APIs) + +**Host-to-Container (from laptop via SSH tunnel):** +```bash +# User's laptop setup +ssh -L 3001:localhost:3001 agent@100.89.94.39 +# Then: http://localhost:3001 → tunnel → frontend:3001 +``` + +**Caddy Reverse Proxy Rules (Caddyfile):** +``` +:3001 { + /api/* → runtime:4111 (Mastra API) + /chat (POST) → runtime:4111 (Chat endpoint) + /auth/* → runtime:4111 (Authentication) + /health → runtime:4111 (Health check) + /webhooks/* → runtime:4111 (Webhook handlers) + /projects → runtime:4111 (Only for XHR/Accept: application/json) + /* (else) → /srv (Static SPA) +} +``` + +## Troubleshooting + +### Frontend shows blank page +- **Check**: `docker compose logs -f frontend` +- **Likely cause**: FRONTEND_MODE not set to `static`, or Caddy can't reach runtime +- **Fix**: Rebuild: `docker image rm triage-dev-frontend && docker compose up -d frontend` + +### Runtime can't connect to database +- **Check**: `docker compose logs -f runtime` (look for "connection refused") +- **Verify**: `docker compose ps` (is libsql healthy?) +- **Fix**: `docker compose restart libsql && docker compose restart runtime` + +### 404 on /api/projects +- **Expected** if no projects registered yet +- **Check**: Projects UI in Settings > Projects +- **Debug**: `docker compose logs runtime | grep projects` + +### Webhook not triggering from Linear +- **Check**: Webhook URL registered in Linear (https://linear.agenticengineering.lat/api/webhooks/linear) +- **Verify**: Settings > Webhook page shows success status +- **Debug**: `docker compose logs runtime | grep webhook` +- **Network**: Confirm cloudflare tunnel is active: `cloudflared tunnel info triage-hackathon` + +### Langfuse not receiving traces +- **Check**: `docker compose logs -f langfuse-web` +- **Verify**: LANGFUSE_BASEURL in .env points to correct URL +- **Debug**: Check runtime logs for trace export errors +- **Tokens**: Confirm LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY match Langfuse project settings + +### "Container doesn't have a healthcheck" +- **Not critical** — just means `docker ps` won't show health status for that service +- **Verify manually**: `curl http://localhost:PORT/health` or `docker compose logs SERVICE` + +## Important Files + +``` +.env # ⚠️ SECRETS — never commit +.env.example # Template — safe to commit +Caddyfile # Reverse proxy + security headers +docker-compose.yml # Service definitions + networking +Dockerfile.frontend # Frontend build (Node → Caddy) +Dockerfile.runtime # Runtime build (TypeScript → Node) +frontend/src/routes/*.tsx # Frontend routes +runtime/src/**/*.ts # Backend/agent code +``` + +## Quick Reference: Working on Features + +### After modifying frontend code: +```bash +docker image rm triage-dev-frontend +docker compose up -d frontend +# Wait ~30s for build and startup +curl -s http://localhost:3001 | head -20 # Verify it's serving +``` + +### After modifying runtime code: +```bash +docker image rm triage-dev-runtime +docker compose up -d runtime +# Wait ~10s for startup +curl -s http://localhost:4111/health # Verify it's responding +``` + +### After changing .env: +```bash +# Option 1: Restart containers (safest) +docker compose restart + +# Option 2: Recreate with new env (if env vars change) +docker compose down +docker compose up -d +``` + +## Remote Access + +**SSH to server:** +```bash +ssh agent@100.89.94.39 +# Then run: docker compose ps, docker compose logs, etc. +``` + +**Port forwarding to localhost:** +```bash +# From your laptop +ssh -L 3001:localhost:3001 agent@100.89.94.39 +ssh -L 4111:localhost:4111 agent@100.89.94.39 # (optional, for direct runtime access) +ssh -L 3000:localhost:3000 agent@100.89.94.39 # (optional, for Langfuse UI) +``` + +**External URLs (via Cloudflare Tunnel):** +- Frontend: https://triage.agenticengineering.lat (if configured) +- Langfuse: https://langfuse.agenticengineering.lat +- Webhooks: https://triage.agenticengineering.lat/api/webhooks/linear + +--- + +**Last Updated**: 2026-04-10 +**Status**: All containers healthy, frontend fixed with auth guard, testing in progress diff --git a/QUICK_START.md b/QUICK_START.md new file mode 100644 index 0000000..8abe463 --- /dev/null +++ b/QUICK_START.md @@ -0,0 +1,326 @@ +# Quick Start Guide — Development Environment Setup + +## Overview + +This project uses **Docker containers** to run all services locally. A new developer should be able to clone the repo, configure `.env`, and run `docker compose up` to have a fully functional incident triage system. + +### What's Running + +| Service | Purpose | Port | URL | +|---------|---------|------|-----| +| **Frontend** | Web UI (Vite dev server + Caddy) | 3001 | http://localhost:3001 | +| **Runtime** | Mastra orchestration + API | 4111 | http://localhost:4111 | +| **LibSQL** | SQLite database | 8080 | http://libsql:8080 | +| **Langfuse Web** | Observability UI | 3000 | http://localhost:3000 | +| **Langfuse Stack** | Postgres, ClickHouse, Redis, MinIO | (internal) | (internal) | +| **Cloudflare Tunnel** | Public HTTPS access | — | langfuse.agenticengineering.lat | + +## 1. Clone & Setup + +```bash +# Clone the repo +git clone +cd triage-dev + +# Copy .env.example (if you don't have .env already) +cp .env.example .env + +# Edit .env with your credentials (see section 2) +``` + +## 2. Configure .env + +Your `.env` file should have all required credentials. Required variables: + +### App Configuration +```env +PORT=4111 +NODE_ENV=production +LIBSQL_URL=http://libsql:8080 +``` + +### LLM & AI +```env +OPENROUTER_API_KEY=sk-or-... # Get from https://openrouter.ai/keys +``` + +### Linear Integration (for ticket creation) +```env +LINEAR_API_KEY=lin_... # Get from Linear → Settings → API +LINEAR_TEAM_ID=... # Your Linear team UUID (or use default TRI team) +``` + +### Email Notifications +```env +RESEND_API_KEY=re_... # Get from https://resend.com/api-keys +RESEND_FROM_EMAIL=your@email.com +``` + +### GitHub (for PR context) +```env +GITHUB_TOKEN=github_pat_... # Get from GitHub → Settings → Developer settings → Personal access tokens +``` + +### Langfuse Observability +```env +LANGFUSE_PUBLIC_KEY=pk_... # Auto-generated on first container start +LANGFUSE_SECRET_KEY=sk_... # Auto-generated on first container start +LANGFUSE_BASEURL=https://langfuse.agenticengineering.lat +``` + +**Note:** All other Langfuse and infrastructure variables should already be in `.env`. Do not modify them unless instructed. + +## 3. Start Containers + +```bash +# Start all services +docker compose up -d + +# Watch logs as services start (Ctrl+C to exit) +docker logs -f triage-runtime + +# Check health of all services +docker compose ps + +# Wait for health checks to pass (takes ~60s for Langfuse stack) +``` + +### Expected Service Status + +``` +CONTAINER STATUS +triage-frontend Up (healthy) +triage-runtime Up (healthy) +libsql Up (healthy) +langfuse-web Up (healthy) ← Takes ~60s +langfuse-worker Up (healthy) +clickhouse Up (healthy) +redis Up (healthy) +minio Up (healthy) +langfuse-postgres Up (healthy) +cloudflared Up +``` + +If any container fails, check logs: +```bash +docker logs +``` + +## 4. Verify Setup + +### Check Runtime Health +```bash +curl http://localhost:4111/api/config/status +# Expected response: +# {"success":true,"data":{"linearConfigured":true,"openrouterConfigured":true}} +``` + +### Access Web UI +- Frontend: http://localhost:3001 +- Langfuse: http://localhost:3000 + +### Check Database +```bash +curl http://localhost:4111/api/linear/issues +# Should return grouped issues (or empty list if no issues yet) +``` + +## 5. Complete Workflow Test + +### Option A: Via Web UI + +1. Go to http://localhost:3001 +2. Click **Chat** tab +3. Submit an incident report: + ``` + There is a critical bug in the auth module where user sessions are timing out unexpectedly after 10 minutes of inactivity + ``` +4. Watch runtime logs: + ```bash + docker logs -f triage-runtime + ``` +5. Expected flow: + - `[intake]` — Receives incident + - `[triage]` — Analyzes with LLM + - `[dedup]` — Checks for duplicates + - `[ticket]` — Creates Linear ticket + - `[notify]` — Sends notifications + - `[suspend]` — Waits for webhook + +### Option B: Via API + +```bash +curl -X POST http://localhost:4111/api/chat \ + -H "Content-Type: application/json" \ + -d '{ + "messages": [{ + "role": "user", + "content": "There is a critical bug in the auth module where user sessions are timing out unexpectedly after 10 minutes of inactivity" + }] + }' +``` + +### Option C: Direct Workflow Trigger + +```bash +curl -X POST http://localhost:4111/api/workflows/triage-workflow/trigger \ + -H "Content-Type: application/json" \ + -d '{ + "description": "Test incident: auth timeout bug", + "reporterEmail": "test@example.com", + "repository": "org/repo" + }' +``` + +## 6. Monitoring & Debugging + +### View Logs +```bash +# Runtime API logs +docker logs -f triage-runtime + +# Frontend logs +docker logs -f triage-frontend + +# Langfuse logs +docker logs -f langfuse-web + +# All logs at once +docker compose logs -f +``` + +### Check Service Connectivity +```bash +# Is runtime connecting to database? +curl http://localhost:4111/health + +# Can runtime reach Linear? +curl http://localhost:4111/api/linear/members + +# Can runtime reach Langfuse? +# Check logs for initialization messages +docker logs triage-runtime | grep -i langfuse +``` + +### Clear State +```bash +# Restart runtime (clears in-memory caches) +docker restart triage-runtime + +# Reset database +docker exec triage-libsql sqlite3 /data/triage.db "DELETE FROM projects;" + +# Restart everything +docker compose restart +``` + +## 7. Langfuse Observability + +Once containers start, you can access Langfuse at http://localhost:3000. + +**Initial login:** +- Email: `admin@example.com` +- Password: `Triagefb1a0cc61520d44eXz` + +This organization, project, and user are created automatically when the Langfuse containers start (from env vars in `.env`). + +**To capture runtime observability:** +- Ensure `LANGFUSE_PUBLIC_KEY` and `LANGFUSE_SECRET_KEY` are in `.env` +- Runtime will automatically send traces and spans to Langfuse +- View them in the Langfuse dashboard under "Projects" → "My Project" → "Traces" + +## 8. Troubleshooting + +### "No LINEAR_API_KEY configured" Error + +**Solution:** Add `LINEAR_API_KEY` to `.env` and restart runtime: +```bash +docker restart triage-runtime +``` + +### "Webhook not resuming workflow" + +**Steps:** +1. Ensure Cloudflare tunnel is running: `docker logs cloudflared` +2. Register webhook: `curl -X POST http://localhost:4111/api/linear/webhook/setup -d '{"url":"https://langfuse.agenticengineering.lat/api/webhooks/linear"}'` +3. Move ticket to DONE in Linear +4. Check runtime logs for resume signal + +### "Langfuse containers not starting" + +**Debug:** +```bash +docker logs langfuse-postgres +docker logs clickhouse +docker logs redis +docker logs minio +``` + +Common issues: +- PostgreSQL password mismatch: check `POSTGRES_PASSWORD` in `.env` +- ClickHouse memory: ensure Docker has >2GB available +- MinIO permissions: check `/data` volume is writable + +### "Frontend won't compile" + +**Solution:** +```bash +# Rebuild frontend container +docker compose build frontend +docker compose up -d frontend + +# Or manually install dependencies +cd frontend && npm install +``` + +## 9. Next Steps + +Once the complete flow works end-to-end: + +1. **Test with your Linear workspace** — Update `LINEAR_TEAM_ID` to your workspace +2. **Configure Slack notifications** — Add `SLACK_BOT_TOKEN` and `SLACK_CHANNEL_ID` +3. **Setup wiki generation** — Trigger `/api/wiki/generate` for RAG +4. **Monitor in production** — Use Langfuse dashboard for observability +5. **Configure GitHub webhooks** — Link PRs to tickets for full automation + +## 10. Architecture Reference + +**Container dependencies:** +``` +frontend → runtime → libsql + ↓ + langfuse-web ← langfuse-postgres + langfuse-worker ← clickhouse, redis, minio + ↓ + cloudflared (Tunnel) +``` + +**Key directories:** +- `./frontend/` — React UI (Vite) +- `./runtime/src/` — Mastra agents, workflows, tools +- `./docker-compose.yml` — Container orchestration +- `./.env` — Environment configuration +- `./TESTING.md` — Detailed testing guide +- `./CLAUDE.md` — Project instructions + +## 11. Resetting Everything + +If you need a completely fresh start: + +```bash +# Stop and remove all containers, volumes, networks +docker compose down -v + +# Remove built images +docker compose down -v --rmi all + +# Start fresh +docker compose up -d + +# Wait ~2 minutes for Langfuse to initialize +docker compose ps +``` + +--- + +**Need help?** Check `TESTING.md` for detailed testing scenarios, or review the `runtime/src/` directory for implementation details. diff --git a/README.md b/README.md index 8af8ce8..b1e787e 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,32 @@ Open [http://localhost:3000](http://localhost:3000) for the Langfuse observabili See [`.env.example`](./.env.example) for all 58 documented variables. See [`QUICKGUIDE.md`](./QUICKGUIDE.md) for detailed setup and troubleshooting. +## Testing the webhook flow + +After a Linear ticket is created, the triage workflow suspends until Linear POSTs an update — moving the ticket to **Done** resumes it (email to reporter), and **In Review** runs an evidence check. Incoming webhooks are verified with HMAC-SHA256 + a ±60s replay window; unsigned or tampered payloads are rejected 401, and if no secret is registered the endpoint returns 503 (fail-closed). + +Two ways to exercise the flow locally: + +**A) Simulate without Linear** — no public URL required: + +```bash +export WEBHOOK_SECRET=dev-only-not-a-real-secret +./scripts/simulate-webhook.sh done # resumes workflow +./scripts/simulate-webhook.sh in-review # triggers evidence check +``` + +The script signs the payload with your `WEBHOOK_SECRET` before POSTing to `/api/webhooks/linear`. Find a UUID in `workflow_runs` after creating a ticket through chat. + +**B) Real webhook from Linear** — requires a public URL (deploy, or tunnel `:4111` with ngrok / Cloudflare Tunnel): + +```bash +curl -X POST https:///api/linear/webhook/setup \ + -H 'Content-Type: application/json' \ + -d '{"url":"https:///api/webhooks/linear"}' +``` + +The endpoint calls Linear's `createWebhook`, captures the signing secret Linear returns (one-time), and persists it in the `webhook_secrets` table. Subsequent real Linear events verify against that stored secret automatically. + ## Tech Stack | Layer | Technology | Purpose | diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..30653f7 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,292 @@ +# Complete Flow Testing Guide + +## Architecture Update + +The Linear configuration is now **dynamic and workspace-agnostic**. You can test with ANY Linear workspace, not just `agentic-engineering-agency`. + +### What Changed + +| Before | After | +|--------|-------| +| Hardcoded Linear team/state/label UUIDs | `LINEAR_TEAM_ID` from `.env` with fallback | +| System tied to one Linear org | Works with any Linear workspace | +| Can't test without modifying code | Set `LINEAR_TEAM_ID` and go | + +### Configuration Flow + +``` +.env (LINEAR_TEAM_ID, LINEAR_API_KEY) + ↓ +config.ts (validated + exported as LINEAR_CONSTANTS.TEAM_ID) + ↓ +Workflow / Routes (use LINEAR_CONSTANTS.TEAM_ID) + ↓ +Dynamic resolvers (resolveStateId, resolveLabelId) + ↓ +Fallback to hardcoded UUIDs (if API fails or state not found) +``` + +## Testing Complete Flow + +### 1. Prepare Your Linear Workspace + +You have two options: + +#### Option A: Use the existing TRI team (default) +No setup needed. The system defaults to `645a639b-39e2-4abe-8ded-3346d2f79f9f` (TRI team from agentic-engineering-agency). + +#### Option B: Test with your own Linear workspace +1. Create a team in your Linear workspace +2. Copy the team UUID from the URL: + - Go to: `https://linear.app/your-workspace/team/YOUR_TEAM_KEY/all` + - The UUID is in the URL structure (appears in Linear's internal APIs) +3. Set `LINEAR_TEAM_ID` in `.env`: + ``` + LINEAR_TEAM_ID=your-team-uuid-here + ``` + +### 2. Setup .env + +Copy `.env.example` and fill in values: + +```bash +cp .env.example .env +``` + +Required variables: +```env +# LLM (required for workflows) +OPENROUTER_API_KEY=sk-or-... + +# Linear integration (required for ticket creation) +LINEAR_API_KEY=lin_... +LINEAR_TEAM_ID=645a639b-39e2-4abe-8ded-3346d2f79f9f # or your team's UUID + +# Database +LIBSQL_URL=http://libsql:8080 + +# Optional but recommended +RESEND_API_KEY=re_... +SLACK_BOT_TOKEN=xoxb-... +SLACK_CHANNEL_ID=C... +``` + +### 3. Start Containers + +```bash +docker-compose up -d +``` + +Wait for all services to be healthy: +- Frontend: http://localhost:3001 +- Runtime API: http://localhost:4111 +- Langfuse: http://langfuse.agenticengineering.lat + +### 4. Complete Workflow Test (Happy Path) + +#### Step 1: Submit an Incident Report +```bash +curl -X POST http://localhost:4111/api/chat \ + -H "Content-Type: application/json" \ + -d '{ + "messages": [{ + "role": "user", + "content": "There is a critical bug in the auth module where user sessions are timing out unexpectedly after 10 minutes of inactivity" + }] + }' +``` + +Or use the web UI: http://localhost:3001 → Chat tab + +#### Step 2: Watch the Workflow Execute + +Check the runtime logs: +```bash +docker logs -f triage-runtime +``` + +You should see: +1. `[intake]` - Receiving incident +2. `[triage]` - Analyzing with RAG (if wiki exists) +3. `[dedup]` - Checking for duplicates +4. `[ticket]` - Creating Linear ticket +5. `[notify]` - Sending notifications +6. `[suspend]` - Waiting for webhook + +#### Step 3: Monitor Linear + +A new ticket should appear in your Linear team: +- Title: Brief summary of the incident +- Description: Includes root cause, details, suggested files +- Labels: Severity (P0-P4) + BUG category +- State: TRIAGE (waiting for assignment) + +#### Step 4: Assign & Work the Ticket + +In Linear: +1. Click the ticket +2. Assign to yourself +3. Move to IN_PROGRESS +4. Add a comment: "Fixed in PR #123" +5. Create a PR link in the description +6. Move to DONE + +#### Step 5: Webhook Resume & Verification + +When you move the ticket to DONE: +1. Linear sends a webhook to `/api/webhooks/linear` +2. Workflow resumes at `[verify]` step +3. Agents review the PR and comments +4. Final notification sent to reporter + +### 5. Partial Flow Tests + +#### Test Just Ticket Creation (no workflow suspension) + +POST to `/api/workflows/triage-workflow/trigger`: +```bash +curl -X POST http://localhost:4111/api/workflows/triage-workflow/trigger \ + -H "Content-Type: application/json" \ + -d '{ + "description": "Test incident", + "reporterEmail": "test@example.com", + "repository": "your-org/repo" + }' +``` + +Watch logs: +``` +[intake] Receive and validate incident report +[triage] Analyse incident +[dedup] Check for duplicate/similar existing tickets in Linear +[ticket] Create Linear ticket +[notify] Send email + Slack notification to assignee about new ticket +[suspend] Suspend workflow – wait for webhook +``` + +#### Test States/Labels Resolution + +Check if states and labels resolve correctly by looking at created tickets: +- Expected states: TRIAGE, BACKLOG, TODO, IN_PROGRESS, IN_REVIEW, DONE +- Expected labels: CRITICAL, HIGH, MEDIUM, LOW, BUG + +If a state isn't found in your Linear workspace, the system: +1. Logs a warning +2. Falls back to hardcoded UUID +3. Continues (may fail if hardcoded UUID doesn't exist in your workspace) + +### 6. Debugging + +#### No ticket created? + +Check: +1. `LINEAR_API_KEY` is valid +2. `LINEAR_TEAM_ID` exists in your Linear workspace +3. States/labels exist (search for them in Linear) +4. Runtime logs: `docker logs triage-runtime` + +#### States/labels not resolving? + +The system tries: +1. **Cache** (1-hour TTL) +2. **API** (dynamic fetch from Linear) +3. **Fallback** (hardcoded UUIDs) + +If you changed Linear team setup, clear the cache by restarting the runtime: +```bash +docker restart triage-runtime +``` + +#### Webhook not firing? + +1. Linear webhook setup: POST `/api/linear/webhook/setup` +2. Make sure your deployment URL is publicly accessible +3. Check Linear webhook logs: Linear app → Settings → Webhooks + +### 7. What Happens Without LINEAR_API_KEY + +The system gracefully degrades: +- ✅ Chat and incident intake work +- ✅ Triage analysis works (using fallback hardcoded states) +- ❌ Ticket creation fails (Linear API required) +- ❌ Webhook resume fails (Linear API required) + +Set `LINEAR_API_KEY` to enable the full flow. + +## Expected Behavior + +### Happy Path Timeline + +``` +T+0s User submits incident report +T+5s Triage analysis completes (calls LLM + RAG) +T+10s Dedup check completes +T+15s Linear ticket created +T+20s Email notification sent to assignee +T+25s Slack notification sent (if configured) +T+25s Workflow suspended (waiting for webhook) + +[... user works the ticket ...] + +T+1h User moves ticket to DONE +T+1h Linear sends webhook +T+1h+5s Workflow resumes at verify step +T+1h+10s Code review runs (if PR attached) +T+1h+15s Final notification sent to reporter +T+1h+20s Workflow completes +``` + +### State Transitions + +``` +TRIAGE + ↓ (assign + work) +IN_PROGRESS + ↓ (code review feedback) +IN_REVIEW (if issues found) + ↓ (fix issues) +IN_PROGRESS + ↓ (fix merged) +DONE + ↓ (webhook fires) +[verify step runs] +``` + +## Troubleshooting Checklist + +- [ ] `.env` file exists and has required variables +- [ ] `LINEAR_API_KEY` is valid (test with: `curl -H "Authorization: Bearer $LINEAR_API_KEY" https://api.linear.app/graphql`) +- [ ] `LINEAR_TEAM_ID` exists in your workspace (verify in Linear URL) +- [ ] States exist in Linear: TRIAGE, TODO, IN_PROGRESS, IN_REVIEW, DONE (search by name) +- [ ] Labels exist: CRITICAL, HIGH, MEDIUM, LOW, BUG (search by name) +- [ ] `OPENROUTER_API_KEY` is valid +- [ ] `LIBSQL_URL` points to running database +- [ ] Docker containers are running: `docker ps` +- [ ] Runtime is responding: `curl http://localhost:4111/api/config/status` + +## Reset State + +If you want to start fresh: + +```bash +# Clear Linear tickets created during testing +# (manually delete in Linear, or use Linear API) + +# Restart the runtime +docker restart triage-runtime + +# Clear the database +docker exec triage-libsql sqlite3 /data/triage.db "DELETE FROM projects;" + +# Clear the workflow storage +docker restart triage-runtime +``` + +## Next Steps + +Once the complete flow works end-to-end: + +1. **Per-project configuration**: Add project-specific Linear teams/credentials +2. **Wiki generation**: Trigger wiki RAG for your repository +3. **Slack integration**: Configure Slack notifications +4. **GitHub integration**: Enable PR commenting and webhook integration diff --git a/VERIFICATION-GUIDE.md b/VERIFICATION-GUIDE.md new file mode 100644 index 0000000..cac8ac6 --- /dev/null +++ b/VERIFICATION-GUIDE.md @@ -0,0 +1,261 @@ +# Triage Workflow Verification Guide + +## Overview + +This document outlines the complete verification flow for the triage incident workflow. After an incident is reported via the API, it goes through an 8-step workflow that ends with webhook-based verification when the assignee resolves the issue in Linear. + +--- + +## Architecture Summary + +### Current State ✅ + +**Infrastructure:** +- ✅ Wiki generation: Automatic cloning + chunking of public repos into persistent volume +- ✅ Code review agent: Exists but optional (only runs if PR link is present in issue description) +- ✅ Docker containers: All healthy except langfuse-worker (non-critical) +- ✅ Cloudflare tunnel: Exposes runtime to public HTTPS endpoints for webhooks + +**Routes exposed via Cloudflare Tunnel:** +``` +triage.agenticengineering.lat → http://runtime:4111 +slack.agenticengineering.lat → http://runtime:4111 +langfuse.agenticengineering.lat → http://langfuse-web:3000 +``` + +**Workflow Steps (8-step architecture):** +1. **Intake** – Validate incident report, enrich description +2. **Triage** – LLM analysis with wiki RAG fallback, classify severity + root cause +3. **Dedup** – Search existing issues, detect duplicates (0.85 similarity threshold) +4. **Ticket** – Create/update Linear issue with structured data +5. **Notify** – Send email + Slack to assignee about new ticket +6. **Suspend** – Workflow pauses, waiting for Linear webhook +7. **Verify** – When webhook arrives: read comments, optionally run code review +8. **Notify-Resolution** – Send final email + Slack with verdict + activity summary + +--- + +## Verification Checklist (For Ricardo) + +### Phase 1: Incident Submission (No PR, Comment-Only Path) + +**Test Scenario:** Submit simple incident without PR link + +**Expected Behavior:** + +1. **Immediate Response (5-10s)** + - ✅ POST /workflows/triage-workflow/trigger returns successfully + - ✅ Issue created in Linear with triage info + - ✅ Issue gets assigned to a team member + +2. **Email to Assignee (10-20s)** + - ✅ Resend email arrives at assignee's email + - ✅ Email contains: + - Incident title + - Severity (P0-P4) + - Root cause identified by AI + - Linear issue link + - **Instructions:** "Please add comments in Linear when resolved, then move issue to 'Done'" + +3. **Slack Notification (10-20s)** + - ✅ Slack message in configured channel + - ✅ Contains severity emoji (🔴 for P0, 🟠 for P1, etc.) + - ✅ Link to Linear issue + - ✅ Assigned person name + +### Phase 2: Assignee Resolves (Comment + Status Change) + +**Test Steps:** + +1. Open Linear issue +2. **Leave comment** with what was fixed: + ``` + Fixed the null pointer in checkout by adding validation. + Also optimized the database query. + ``` +3. **Move issue to "Done"** status in Linear + +**What triggers webhook:** +- Linear webhook fired (configured at project creation) +- Runtime receives webhook at `https://triage.agenticengineering.lat/api/webhooks/linear` +- Workflow resumes at step 7 (Verify) + +### Phase 3: Verification (Verify Step) ⚠️ THIS IS WHAT CHANGES + +**Current Implementation:** +- Reads all comments from Linear issue +- Builds activity summary: `"[assignee-name] Fixed the null pointer..."` +- **No PR link case** → Uses resolution-reviewer agent with comment context +- Returns verdict: `'resolved'` (always) +- **PR link case** → Runs code-review-agent in parallel, may post GitHub comment + +**Expected Output to Reporter:** + +Email to reporter (original incident submitter): +``` +Issue: [Issue Title] +Verdict: ✅ RESOLVED + +Verification Notes: +The issue was addressed. Assignee reported the fix. + +Activity Report: +[assignee-name] Fixed the null pointer in checkout by adding validation. +Also optimized the database query. + +--- +View issue: https://linear.com/... +``` + +Slack notification (same channel): +``` +✅ Issue Resolved: [Issue Title] + +Verdict: Resolved + +Resolution: The issue was addressed. Assignee reported the fix. + +Activity Report: +[assignee-name] Fixed the null pointer... +``` + +--- + +## What to Watch For (Gotchas) + +### ✅ Working Paths +1. **Comment-only (No PR):** Comments → Email/Slack → Done +2. **With PR + no issues:** Diff reviewed → Comments on GitHub → Email/Slack → Done +3. **With PR + issues found:** Diff reviewed → GitHub comments + Linear moved to IN_REVIEW → Email/Slack says "needs review" + +### ⚠️ Failure Points +- **Issue:** Linear webhook never fires + - **Check:** Linear integration token saved? Webhook registered? Tunnel accessible? + - **Verify:** Check docker logs: `docker-compose logs runtime | grep webhook` + +- **Issue:** Email never arrives + - **Check:** Resend token valid? Reporter email correct? + - **Verify:** Check if email address is on .env `RESEND_FROM_EMAIL` + +- **Issue:** Slack message never arrives + - **Check:** Bot token valid? Channel ID saved to project? + - **Verify:** `GET /projects/:id/settings/integrations` returns `slack.configured: true` + +- **Issue:** Comments not showing in activity summary + - **Check:** Issue was updated with status before webhook fired? + - **Verify:** Check logs: `docker-compose logs runtime | grep "read comments"` + +--- + +## Test Case: Full E2E Flow + +**Setup (1 time):** +```bash +# 1. Create project with public repo +POST /projects +{ + "name": "Test Project", + "repositoryUrl": "https://github.com/user/public-repo", + "branch": "main" +} +# → Wiki generation starts in background + +# 2. Configure Linear integration +POST /projects/:projectId/settings/linear/test +{ "token": "lin_api_..." } + +# 3. Configure assignee & reporter +# (Assume Linear team already set up) +``` + +**Test (repeatable):** +```bash +# Step 1: Submit incident +POST /workflows/triage-workflow/trigger +{ + "reporterName": "Ricardo", + "reporterEmail": "ricardo@example.com", + "title": "Checkout page crashes on mobile", + "description": "Users report 500 error when submitting payment on mobile", + "repository": "myorg/myrepo" +} +# ← Expect: Issue ID returned in ~5s + +# Step 2: Check Linear +# - Issue should exist +# - Should be assigned +# - Email should arrive at assignee + +# Step 3: Assignee action (in Linear UI) +# - Add comment: "Fixed mobile checkout validation" +# - Move to "Done" + +# Step 4: Check final notifications +# - Email should arrive at ricardo@example.com +# - Should say "✅ RESOLVED" +# - Should include assignee's comment +# - Slack notification should also appear +``` + +**Expected timing:** +- Incident creation to Linear issue: 5-10s +- Email/Slack to assignee: 10-20s +- Assignee resolves in Linear: instant (manual) +- Webhook fires: depends on Linear (~1-5s after status change) +- Final email/Slack to reporter: 5-10s after webhook fires +- **Total end-to-end (not including manual): ~30-50 seconds** + +--- + +## Decisions Made + +### ✅ Simplified Verification (Your Preference) +- **Current:** Reads comments, uses resolution-reviewer agent +- **Not doing:** Full code diff review + scope comparison (too complex for now) +- **Code review agent:** Still exists but only runs if PR link present in issue + +### ✅ Wiki Strategy +- **Purpose:** Triage step only (step 2) — provides context for AI to classify severity +- **Not needed for:** Verification step (step 7) — just reads comments +- **Auto-generation:** Happens in background when project created + +### ✅ Slack Channel Selection UI +- **Currently:** Uses hardcoded SLACK_CHANNEL_ID from .env +- **Capability:** Yes, API exists (`conversations.list()`) — could show dropdown +- **Timeline:** Not blocking — MVP uses hardcoded channel, UI selection is optional enhancement + +--- + +## Next Steps (If Issues Found) + +1. **Webhook not firing?** + - Verify Linear webhook registered in dashboard + - Check tunnel is accessible: `curl https://triage.agenticengineering.lat/api/webhooks/linear` + - Logs: `docker-compose logs runtime | grep webhook` + +2. **Comments not reading?** + - Verify Linear API token has `issue:read` scope + - Logs: `docker-compose logs runtime | grep "read comments"` + +3. **Email not arriving?** + - Verify reporter email is correct + - Check Resend dashboard for failures + - Verify .env RESEND_API_KEY is valid + +4. **Performance issues?** + - Wiki generation on large repos (>100MB) may slow triage step + - Monitor: `GET /projects/:id` returns `status: 'pending'` during generation + +--- + +## Files Changed + +- `cloudflare-tunnel.yml`: Added routes for triage + slack webhooks + ```yaml + - hostname: triage.agenticengineering.lat → http://runtime:4111 + - hostname: slack.agenticengineering.lat → http://runtime:4111 + ``` + +--- + +**Ready to verify? Start with Phase 1 test case above.** 🚀 diff --git a/cloudflare-tunnel.yml b/cloudflare-tunnel.yml new file mode 100644 index 0000000..795ca14 --- /dev/null +++ b/cloudflare-tunnel.yml @@ -0,0 +1,10 @@ +tunnel: triage-hackathon +credentials-file: /home/nonroot/.cloudflared/cert.json +ingress: + - hostname: langfuse.agenticengineering.lat + service: http://langfuse-web:3000 + - hostname: triage.agenticengineering.lat + service: http://runtime:4111 + - hostname: slack.agenticengineering.lat + service: http://runtime:4111 + - service: http_status:404 diff --git a/docker-compose.override.yml b/docker-compose.override.yml index f67e6b4..33432a1 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -12,11 +12,13 @@ services: - "127.0.0.1:5173:5173" volumes: - ./frontend:/app - - vite_node_modules:/app/node_modules + - /app/node_modules networks: - app frontend: + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro environment: FRONTEND_MODE: dev @@ -34,6 +36,3 @@ services: volumes: - ./runtime/src:/app/runtime/src - ./runtime/init-db.mjs:/app/runtime/init-db.mjs - -volumes: - vite_node_modules: diff --git a/docker-compose.yml b/docker-compose.yml index 464e1fd..8e1eb03 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,61 +1,3 @@ -x-langfuse-worker-env: &langfuse-worker-env - # Auth.js v5 (NextAuth) vars. Langfuse 3.x uses Auth.js v5 which reads - # AUTH_URL, not the legacy NEXTAUTH_URL. When running behind a reverse - # proxy (our cloudflared tunnel), AUTH_TRUST_HOST=true is mandatory — - # without it Auth.js refuses to trust the forwarded Host header and - # every callback URL falls back to the container's internal hostname, - # which breaks login from the public URL. NEXTAUTH_URL is retained - # for backward compatibility with any code path that still reads it. - AUTH_URL: ${NEXTAUTH_URL} - # AUTH_TRUST_HOST=false is deliberate: our Cloudflare Tunnel ingress - # rewrites the incoming HTTP Host header to the origin service name - # (langfuse-web:3000) before it reaches the container. With TRUST_HOST - # on, Auth.js v5 uses that forwarded Host and builds callback URLs - # pointing at an unreachable internal hostname, which breaks login - # completely. Turning it off forces Auth.js to derive everything - # from AUTH_URL, which is the public tunnel URL. - AUTH_TRUST_HOST: "false" - NEXTAUTH_URL: ${NEXTAUTH_URL} - DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@langfuse-postgres:5432/${POSTGRES_DB:-langfuse} - SALT: ${SALT} - ENCRYPTION_KEY: ${ENCRYPTION_KEY} - TELEMETRY_ENABLED: ${TELEMETRY_ENABLED:-true} - LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: ${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false} - # ClickHouse - CLICKHOUSE_MIGRATION_URL: clickhouse://clickhouse:9000 - CLICKHOUSE_URL: http://clickhouse:8123 - CLICKHOUSE_USER: default - CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD} - CLICKHOUSE_CLUSTER_ENABLED: "false" - # Redis - REDIS_HOST: redis - REDIS_PORT: 6379 - REDIS_AUTH: ${REDIS_AUTH} - # MinIO/S3 - LANGFUSE_S3_EVENT_UPLOAD_BUCKET: langfuse - LANGFUSE_S3_EVENT_UPLOAD_PREFIX: events/ - LANGFUSE_S3_EVENT_UPLOAD_REGION: auto - LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: http://minio:9000 - LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${MINIO_ROOT_USER:-minio} - LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY} - LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: true - LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: langfuse - LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: media/ - LANGFUSE_S3_MEDIA_UPLOAD_REGION: auto - LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: http://minio:9000 - LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: ${MINIO_ROOT_USER:-minio} - LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY} - LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: true - # S3 Batch Export - LANGFUSE_S3_BATCH_EXPORT_ENABLED: ${LANGFUSE_S3_BATCH_EXPORT_ENABLED:-false} - LANGFUSE_S3_BATCH_EXPORT_BUCKET: langfuse - LANGFUSE_S3_BATCH_EXPORT_PREFIX: exports/ - LANGFUSE_S3_BATCH_EXPORT_REGION: auto - LANGFUSE_S3_BATCH_EXPORT_ENDPOINT: http://minio:9000 - LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID: ${MINIO_ROOT_USER:-minio} - LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY: ${LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY} - LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE: true - services: frontend: build: @@ -66,6 +8,8 @@ services: depends_on: runtime: condition: service_healthy + env_file: + - .env healthcheck: test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:3001"] interval: 10s @@ -97,7 +41,6 @@ services: restart: always networks: - app - - langfuse libsql: image: ghcr.io/tursodatabase/libsql-server:v0.24.32 @@ -121,167 +64,6 @@ services: networks: - app - langfuse-web: - image: langfuse/langfuse:3.167.0 - ports: - - "127.0.0.1:3000:3000" - depends_on: - langfuse-postgres: - condition: service_healthy - clickhouse: - condition: service_healthy - redis: - condition: service_healthy - minio: - condition: service_healthy - environment: - <<: *langfuse-worker-env - NEXTAUTH_SECRET: ${NEXTAUTH_SECRET} - LANGFUSE_INIT_ORG_ID: ${LANGFUSE_INIT_ORG_ID} - LANGFUSE_INIT_ORG_NAME: ${LANGFUSE_INIT_ORG_NAME} - LANGFUSE_INIT_PROJECT_ID: ${LANGFUSE_INIT_PROJECT_ID} - LANGFUSE_INIT_PROJECT_NAME: ${LANGFUSE_INIT_PROJECT_NAME} - LANGFUSE_INIT_PROJECT_PUBLIC_KEY: ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY} - LANGFUSE_INIT_PROJECT_SECRET_KEY: ${LANGFUSE_INIT_PROJECT_SECRET_KEY} - LANGFUSE_INIT_USER_EMAIL: ${LANGFUSE_INIT_USER_EMAIL} - LANGFUSE_INIT_USER_NAME: ${LANGFUSE_INIT_USER_NAME} - LANGFUSE_INIT_USER_PASSWORD: ${LANGFUSE_INIT_USER_PASSWORD} - healthcheck: - test: ["CMD-SHELL", "wget -qO- http://$$(hostname):3000/api/public/health || exit 1"] - interval: 15s - timeout: 5s - retries: 10 - start_period: 180s - restart: always - networks: - - langfuse - - langfuse-worker: - image: langfuse/langfuse-worker:3.167.0 - ports: - - "127.0.0.1:3030:3030" - depends_on: - langfuse-postgres: - condition: service_healthy - clickhouse: - condition: service_healthy - redis: - condition: service_healthy - minio: - condition: service_healthy - environment: - <<: *langfuse-worker-env - healthcheck: - test: ["CMD-SHELL", "wget -qO- http://$$(hostname):3030/api/health || exit 1"] - interval: 15s - timeout: 5s - retries: 10 - start_period: 180s - restart: always - networks: - - langfuse - - clickhouse: - image: clickhouse/clickhouse-server:24.8.9.95 - ports: - - "127.0.0.1:8123:8123" - - "127.0.0.1:9000:9000" - environment: - CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD} - healthcheck: - test: ["CMD", "wget", "--spider", "http://127.0.0.1:8123/ping"] - interval: 10s - timeout: 5s - retries: 3 - start_period: 30s - volumes: - - /var/lib/clickhouse - - /var/log/clickhouse-server - restart: always - networks: - - langfuse - - redis: - image: redis:7.4-alpine - ports: - - "127.0.0.1:6379:6379" - command: redis-server --requirepass ${REDIS_AUTH} --maxmemory-policy noeviction - healthcheck: - test: ["CMD", "redis-cli", "-a", "${REDIS_AUTH}", "--no-auth-warning", "ping"] - interval: 10s - timeout: 5s - retries: 3 - volumes: - - /data - restart: always - networks: - - langfuse - - minio: - image: minio/minio:RELEASE.2025-09-07T16-13-09Z - entrypoint: /bin/sh - command: -c "mkdir -p /data/langfuse && minio server --address ':9000' --console-address ':9001' /data" - ports: - - "127.0.0.1:9090:9000" - - "127.0.0.1:9091:9001" - healthcheck: - test: ["CMD", "curl", "-f", "http://127.0.0.1:9000/minio/health/ready"] - interval: 10s - timeout: 5s - retries: 3 - volumes: - - /data - environment: - MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minio} - MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} - restart: always - networks: - - langfuse - - langfuse-postgres: - image: postgres:17.9-alpine - ports: - - "127.0.0.1:5432:5432" - healthcheck: - test: ["CMD", "pg_isready", "-U", "postgres"] - interval: 10s - timeout: 5s - retries: 3 - volumes: - - langfuse_postgres_data:/var/lib/postgresql/data - environment: - POSTGRES_USER: ${POSTGRES_USER:-postgres} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_DB: ${POSTGRES_DB:-langfuse} - TZ: UTC - PGTZ: UTC - restart: always - networks: - - langfuse - - cloudflared: - image: cloudflare/cloudflared:latest - command: tunnel --protocol http2 run - environment: - TUNNEL_TOKEN: ${CLOUDFLARE_TUNNEL_TOKEN} - depends_on: - langfuse-web: - condition: service_healthy - restart: always - networks: - - langfuse - networks: app: driver: bridge - langfuse: - driver: bridge - -volumes: - # Langfuse Postgres data: holds users, orgs, projects, API keys, traces - # metadata. Langfuse headless init only runs on a fresh DB, so keeping - # this volume across restarts avoids reseeding the admin user on every - # up/down cycle (and ensures traces captured during a demo persist for - # replay). Wipe with `docker compose down -v` + `docker volume rm - # triage_langfuse_postgres_data` only when you actually want a reset. - langfuse_postgres_data: diff --git a/docs/HANDOFF-MULTI-TENANT-HARDENING-2026-04-17.md b/docs/HANDOFF-MULTI-TENANT-HARDENING-2026-04-17.md new file mode 100644 index 0000000..4424c7f --- /dev/null +++ b/docs/HANDOFF-MULTI-TENANT-HARDENING-2026-04-17.md @@ -0,0 +1,493 @@ +# Handoff — Multi-tenant hardening & UX redesign + +**Branch:** `fix/ui-cleanup-cycle-panel` — 14 commits pusheados al origin + #5d landed local (pendiente commit). +**Last commit pushed:** `4a350be feat(ui): Slack/Resend/GitHub cards + account-email reporter (#5c)` (2026-04-22). +**Uncommitted at time of write:** #5d slice (backend probe/PUT github + wiki-rag PAT + UI rewrite + tests + handoff update). +**Date:** 2026-04-17 (original), 2026-04-20 (#3 + #4a), 2026-04-21 (#4b + #5a), 2026-04-22 (#5b Linear + #5c Slack/Resend/GitHub + #5d unification) + +--- + +## Objetivo de la próxima sesión + +**Siguiente natural: #6 — onboarding wizard + legacy endpoint cleanup + columna plaintext drops.** + +#1–#5d cerrados: webhook verif, wiki, per-tenant schema, tools + agents per-tenant, 5 integration cards (OpenRouter + Linear + Slack + Resend + GitHub), scoped-routes flip, ownership fix, generic `Picker`, account-email reporter, GitHub/wiki unification con private-repo detection (Pattern C). Todo validado code-side; smoke UI del #5d queda pendiente de usuario. + +Baseline al cierre de #5c: TSC runtime 5 errores estable (3 scoped-routes Cycle casts + 2 sendSlackMessage.execute possibly-undefined — pre-existing). TSC frontend limpio. Tests: 106 failing baseline estable, +12 nuevos passing en #5c (+10 `integration-routes.test.ts` para los 3 providers nuevos, +1 `tenant-keys.test.ts` meta propagation, +1 `resend.test.ts` meta.fromEmail override). 0 regresiones netas. + +Smoke #5c validado end-to-end el 2026-04-22: +- Resend card (PAT + fromEmail manual) → Test & Save → card "From: ..." +- Slack card con token minimal-scope (`chat:write` only) → `auth.test` pasa, `conversations.list` falla con `missing_scope` → UI cae a input manual de channelId con hint sobre scopes. Confirma que el mismo token del `.env` (que sólo tiene chat:write) funciona via manual input. +- GitHub card → picker de repos → Save → card "Repo: owner/name". Funcionó aislado, pero destapa el dual-source-of-truth con `projects.repo_url` — lo resuelve #5d. + +Ver la sección **"Priorización"** más abajo para el detalle de #5d. + +--- + +## Estado actual — qué funciona, qué está probado + +Probado end-to-end esta semana: + +- Chat → displayTriageTool (clasificación + assignee + ciclo + dueDate) +- Create Ticket → workflow crea issue en Linear en el ciclo correcto +- Email al assignee + notificación Slack +- Webhook simulado: issue → "Done" → resume workflow → email de resolución al reporter → mensaje "✅ resolved" en el chat thread +- Webhook simulado: issue → "In Review" sin evidencia → bot lo revierte a "In Progress" y nagea al assignee +- Observability dashboard: `llm_usage` table se llena via `onFinish` del chatRoute; pricing funciona con prefix-match para model IDs versionados de OpenRouter +- `/integrations` end-to-end con los 5 providers: OpenRouter (test+save directo), Linear (picker de teams), Slack (picker cuando hay scopes, input manual cuando falta `channels:read`), Resend (PAT + fromEmail manual), GitHub (picker de repos — a unificar en #5d) +- Reporter email en `chat.tsx` viene de `useAuth().user.email` en vez de localStorage (key `reporter_email` muerta) + +No probado todavía: + +- Wiki RAG end-to-end (generación + query) — existe el código pero no se validó esta semana +- Webhook real desde Linear (solo simulado vía `scripts/simulate-webhook.sh`) +- Producción / deployment (solo docker-compose local) + +--- + +## Dominios del sistema + +``` +┌─ frontend/ ──────────────────────── React SPA (Vite + TanStack Router) +│ ├─ src/routes/chat.tsx → chat UI + stream de workflow +│ ├─ src/routes/observability.* → dashboard de costos + agent usage +│ ├─ src/routes/projects.* → wiki generation UI +│ └─ src/components/triage-card.tsx → card con severity, assignee, ciclo, dueDate +│ +├─ runtime/ ──────────────────────── Node.js + Mastra framework +│ ├─ src/mastra/index.ts → Mastra instance + todas las API routes +│ │ (/chat, /webhooks/linear, /workflows/*/stream) +│ ├─ src/mastra/agents/ +│ │ ├─ orchestrator.ts → agente principal (Mercury-2 primary) +│ │ ├─ triage-agent.ts → clasificación de severity +│ │ └─ resolution-reviewer.ts → revisa evidencia de resolución +│ ├─ src/mastra/workflows/ +│ │ └─ triage-workflow.ts → intake → triage → dedup → ticket → notify → suspend +│ ├─ src/mastra/tools/ → linear, resend, slack, github, wiki, display +│ ├─ src/lib/ +│ │ ├─ observability-routes.ts → /observability/{stats,costs,agents,workflows,pricing} +│ │ ├─ usage-logger.ts → logUsage() inserta en llm_usage +│ │ ├─ provider-pricing.ts → MODEL_PRICING hardcoded + prefix match +│ │ ├─ webhook-routes.ts → handlers de webhooks +│ │ ├─ project-routes.ts → CRUD de proyectos +│ │ ├─ integration-routes.ts → configs de integraciones (aquí va a vivir BYO-keys) +│ │ ├─ wiki-rag.ts → pipeline de generación de wiki +│ │ └─ config.ts → LINEAR_CONSTANTS (solo TEAM_ID queda hardcoded) +│ └─ src/lib/auth.ts → better-auth setup +│ +├─ scripts/ +│ ├─ simulate-webhook.sh → POST fake Linear webhook al runtime +│ ├─ setup-webhook-ngrok.sh → ngrok + registra webhook en Linear +│ └─ check-env.sh → valida que no queden CHANGEME en .env +│ +└─ Infra + ├─ docker-compose.yml → runtime + frontend + libsql + └─ Dockerfile.{runtime,frontend} +``` + +**Storage:** libsql (sqld) en contenedor propio. Tablas clave: +- `llm_usage` — tokens y costo por call (filled via onFinish) +- `workflow_runs` — runId ↔ threadId ↔ issueId mapping +- `local_tickets` — cache de tickets creados por el sistema (para reporter_email lookup en webhook) +- `card_states` — estado persistido de cada triage card (confirmed/error/linearUrl) +- `projects` + `wiki_documents` + `wiki_chunks` — wiki RAG +- `auth_*` — better-auth tables + +--- + +## Priorización del trabajo pendiente + +Orden recomendado. Cada uno puede ser una sesión propia: + +### 1. Webhook signature verification ✅ DONE (2026-04-17) +`/api/webhooks/linear` valida HMAC-SHA256 + replay window ±60s vía `LinearWebhookClient.parseData` del SDK oficial. Fail-closed: sin secret registrado responde 503; firma inválida o ausente responde 401. +- Helper: `runtime/src/lib/verify-linear-signature.ts` (6 unit tests) +- Storage: tabla `webhook_secrets(provider PK, webhook_id, secret, created_at, updated_at)` + helper `runtime/src/lib/webhook-secrets.ts` (4 unit tests) +- `POST /api/linear/webhook/setup` captura el `secret` que Linear devuelve al crear el webhook y hace UPSERT en la tabla +- `scripts/simulate-webhook.sh` firma payloads (HMAC vía openssl) leyendo el secret de la DB, o de `WEBHOOK_SECRET` si se exporta +- **Para multi-tenant (#3-5)**: el secret actualmente es global (un row); cuando vivamos per-project, la tabla crece a `(project_id, provider)` como composite PK y el handler lee el secret del project al que pertenece el webhook. + +### 2. Verificar flujo de wiki end-to-end ✅ DONE (2026-04-17) +Pipeline validado con `sindresorhus/slugify` (8 files / 29 chunks / ready). El orchestrator llama `queryWikiTool` correctamente y aterriza las respuestas en el código real cuando el usuario pregunta por el proyecto. + +Fixes que aterrizaron en esta pasada: +- **Symlinks rotos mataban el scan**: `scanFiles` en `wiki-rag.ts` usaba `statSync` que tira ENOENT cuando el repo contiene symlinks cuyos targets no existen tras el `git clone --depth 1` (ej. `skills/mastra` en este mismo repo apunta a `../.agents/...`). Switched a `lstatSync` + try/catch — los symlinks se saltan silenciosamente. +- **Columna `projects.error` faltaba en DBs upgradeadas**: el `CREATE TABLE IF NOT EXISTS` tenía la columna pero no había `ALTER TABLE ADD COLUMN` para upgrade. Agregado a `init-db.mjs`. Sin esto, cualquier fallo del pipeline era swallowed por el `.catch(()=>{})` del error handler. +- **`queryWiki` idempotente**: short-circuits a `{ results: [], totalResults: 0 }` si el índice `wiki_vectors` no existe, en lugar de hacer embedding y tirar "no such table". El índice lo crea `LibSQLVector.createIndex` dentro de `generateWiki` on-demand. +- **Triage workflow skip cuando no hay wiki**: `triage-workflow.ts:147` ahora hace un `SELECT status, chunks_count FROM projects` antes de llamar `queryWiki`; si no está ready o tiene 0 chunks, lo brinca y deja el heurístico solo. + +**Project-aware orchestrator** (patrón canónico de Mastra — RequestContext + dynamic instructions, ver `runtime/node_modules/@mastra/core/dist/docs/references/docs-server-request-context.md`): +1. Middleware server (`mastra/index.ts`) lee `x-project-id` header y lo pone en `requestContext` +2. Frontend manda el header via `DefaultChatTransport.headers()` +3. Orchestrator usa `instructions: async ({ requestContext })` que consulta `projects` y arma un prompt con "Active Project: name=... id=... status=..." + guía de cuándo llamar `queryWikiTool` +4. `queryWikiTool` lee `ctx.requestContext.get('projectId')` como fallback si el LLM no lo pasa explícito + +**Mejora UX frontend**: `projects.lazy.tsx` ahora hace `queryClient.setQueryData` optimista con el response del POST, así el card aparece inmediato en vez de esperar al refetch del invalidateQueries. + +**Followup que salió de este arco** (no bloqueante): +- Duplicated wiki-status state: `projects.wiki_status` (usada por `scoped-routes.ts`) y `projects.status` (usada por `wiki-rag.ts` + `project-routes.ts` + UI) son dos fuentes de verdad. Consolidar en un solo path cuando se refactoree para multi-tenant (#3-5). +- `local_tickets.project_id NOT NULL` constraint: el workflow no pasa `projectId` al crear la row, por eso está vacía. No bloqueante (webhook handler usa `workflow_runs` para el resume). Relacionado con multi-tenant. + +### 3. Schema de per-tenant keys + encryption ✅ DONE (2026-04-20) — committed +Infra lista y probada — sin tools/agents/UI wired todavía. Commits: `74e05f2 feat(integrations)` + `4ec307a chore(types)` (fix de TS drift en zod-schemas.ts + provider-pricing.ts). TSC limpio (0 errores). Archivos: +- `runtime/src/lib/crypto-envelope.ts` — AES-256-GCM con DEK per-row wrappeado bajo `APP_MASTER_KEY`. Versión byte bindeada como AAD → downgrade falla auth. Tagged unions para loadMasterKey/decrypt (no `any`). 19 unit tests. +- `runtime/src/lib/integration-keys.ts` — `setIntegrationKey`, `getIntegrationKey`, `listIntegrations`, `deleteIntegrationKey`, `markTested`. Cache LRU in-memory con TTL 60s, invalidada en set/delete/markTested. List nunca expone plaintext ni ciphertext. 17 unit tests cubren round-trip, tamper, wrong master key, missing key, cache hit/expiry, aislamiento entre proyectos. +- `runtime/src/lib/schemas/integrations.ts` — `integrationProviderSchema` (enum cerrado `linear|resend|slack|github|openrouter`), `integrationStatusSchema` (`active|disabled|invalid`), `integrationMetaSchema` (`z.record(z.string(), z.string())` — cada provider parsea contra su schema rico en el boundary si necesita). +- `runtime/src/db/schema.ts` — tabla `projectIntegrations` con composite PK `(project_id, provider)`, BLOB `encrypted_key`, JSON `meta`. Relation `projectIntegrationsRelations` + `projects.integrations: many()`. +- `runtime/src/db/zod-schemas.ts` — `projectIntegrationsSelect/InsertSchema` via drizzle-zod + `.extend()`. Nota: drizzle-zod 0.8 compone su output contra tipos de `zod/v4`, así que el overlay importa `zod/v4` aunque el resto del codebase use el root `zod` (v3.25, con subpath v4). Compatible en runtime, distinto a nivel de tipo. +- `runtime/init-db.mjs` — `CREATE TABLE IF NOT EXISTS project_integrations` + `idx_project_integrations_project_id`. +- `.env.example` — `APP_MASTER_KEY` con nota de generación (`openssl rand -base64 32`). **No se tocó `.env`** — cuando se necesite ejercer integration-keys hay que generar la key y setearla. + +**Para multi-tenant (#4-5)**: el secret de Linear webhook (tabla `webhook_secrets`) todavía es global (un row); cuando los tools lean del tenant context, esa tabla también se refactorea a `(project_id, provider)` como composite PK. + +**Gotcha de type-level zod v3 vs v4**: si agregas otro createSelectSchema/Insert con `.extend()`, usa `zod/v4` para el overlay. Para zod solo a nivel de parse/validate, el root `zod` (v3) está bien. + +### 4a. Refactor de tools (Linear/Resend/Slack/GitHub) ✅ DONE (2026-04-20) +Partido en 4a (tools) + 4b (agents' `model:`) para reducir blast radius. 4a cerrado con los tools downstream de proveedores; los agentes LLM siguen leyendo `OPENROUTER_API_KEY` de env (esos son 4b). + +**Helper nuevo — `runtime/src/lib/tenant-keys.ts`**: +- `resolveKey(provider, projectId?, opts?)` devuelve `{ key: string | null, source: 'tenant' | 'env' | 'none' }` +- Flujo: si hay `projectId` → `getIntegrationKey` → si `ok` devuelve tenant; si `not_found` | `master_key_missing` | sin projectId → fallback a `process.env.`. `decrypt_failed` **no** cae a env (es tampering real, mejor fallar que usar una key equivocada). +- Mapping: `linear→LINEAR_API_KEY`, `resend→RESEND_API_KEY`, `slack→SLACK_BOT_TOKEN`, `github→GITHUB_TOKEN`, `openrouter→OPENROUTER_API_KEY` +- Log one-liner `[tenant-keys] project= provider=

source=` solo en cambio de fuente por pair (no spam) +- Flag `envFallback:false` para strict-mode futuro +- 13 unit tests en `tenant-keys.test.ts` + +**Workflow pattern — `callTool` sintético**: +`runtime/src/mastra/workflows/triage-workflow.ts:callTool(tool, input, ctx?)` acepta `{ projectId }` opcional y arma un `runtimeContext = { requestContext: { get: k => k==='projectId' ? ctx.projectId : undefined } }` que se pasa como segundo arg a `tool.execute`. Esto replica lo que el middleware HTTP (`x-project-id`) hace para el agent path, pero desde dentro del workflow que no pasa por el middleware. **Sin esto, los tools invocados desde el workflow nunca verían el projectId**, aunque el agent path sí. + +**`projectId` propagado por el chain de steps**: `ticketStep → notifyStep → suspendStep → verifyStep → notifyResolutionStep` — agregado como `z.string().optional()` en cada schema. Crítico que esté en `suspendStep.inputSchema` porque Mastra preserva ese snapshot durante el suspend/resume del webhook, así los tools post-webhook también resuelven per-tenant. + +**Tools refactorizados** (singleton module-level removido, cliente nuevo per-call): +- `linear.ts` — 7 tools, construye `LinearClient` per-call via `resolveLinearClient(toolCtx)` +- `resend.ts` — 2 tools, `resolveResend(toolCtx)` → `{ client, from }`. FROM email: env `RESEND_FROM_EMAIL` → fallback literal. (Hook para `meta.fromEmail` cuando UI de #5 lo guarde.) +- `slack.ts` — 3 tools, `resolveSlack(toolCtx)`. Channel: `ctx.channel` → `metaChannel` (futuro) → `config.SLACK_CHANNEL_ID` → skip +- `github.ts` — 2 tools, swap directo de `process.env.GITHUB_TOKEN` por `resolveKey('github', projectId)`. `scrubToken(msg, token?)` ahora recibe el token explícitamente + +**Workflow consumers** que antes pasaban `config.LINEAR_API_KEY` a helpers — migrados a `resolveKey('linear', inputData.projectId)` upfront por step: +- `resolveStateId('TODO'|'BACKLOG'|'IN_REVIEW', linearApiKey)` (3 call-sites en ticketStep + verifyStep) +- `resolveLabelId('CRITICAL'|'HIGH'|...|'BUG', linearApiKey)` (2 call-sites en ticketStep) + +**Out-of-scope explícitamente** (requieren commits separados): +- `mastra/index.ts:42` — `LinearClient` global para `linear-sync` cron (system-level, no per-request) +- `mastra/index.ts:471,479` — `process.env.GITHUB_TOKEN` para repo lookup en una route admin +- `webhook_secrets` table — aún global, refactor a `(project_id, provider)` cuando haya UI +- Agents LLM (`createOpenRouter` en los 5 agentes + `attachments.ts` + `wiki-rag.ts`) → **#4b** + +**Tests**: +- `linear.test.ts`: 2 tests actualizados al nuevo contrato (S11 singleton → "constructs fresh client per call"; S2 dynamic-import → `vi.doMock('./../../lib/tenant-keys')` en vez de `config`) +- `resend.test.ts`: mismo patrón (el singleton test verifica via `mockEmailsSend` para evitar rebinding con `vi.resetModules()`) +- `slack.test.ts`: sin flip — ya usaba `execute!({...})` directo, no chequeaba singleton +- `vitest.config.ts`: agregado `SLACK_BOT_TOKEN`, `SLACK_CHANNEL_ID`, `GITHUB_TOKEN` al env (antes venían por `vi.mock('config')`) + +**Smoke test validado (runId `5bfd1fe0-...` → TRI-49 real)**: con `.env` actual y SIN `project_integrations` seedeado, los 3 tools loggean `source=env` y el workflow corre hasta notify+suspend. Linear + Slack OK. Resend devolvió "Unable to fetch data" pero eso es un issue de network/key pre-existing ajeno al refactor. + +**Para #4b (agents)**: +Usar `model: async ({ requestContext }) => createOpenRouter({ apiKey: await resolveKeyValue('openrouter', requestContext.get('projectId')) })(...)` — patrón canónico Mastra de dynamic model. Blast radius alto porque toca cada stream de chat; probar con un solo agent primero (recomiendo `slack-notification-agent` que se invoca desde el workflow con un proyecto activo en context). + +### 4b. Refactor de agentes (`model:` dynamic para OpenRouter key) ✅ DONE (2026-04-21) — `124f29a` + +Todos los OpenRouter clients construidos module-level fueron reemplazados por resolución per-call. Helper nuevo + 5 agentes + 1 tool + 1 lib + 5 workflow call sites migrados. + +**Helper — `runtime/src/lib/tenant-openrouter.ts`**: +- `resolveOpenRouterFromContext({ requestContext })` para agentes (lee `projectId` del contexto) +- `resolveOpenRouterFromProjectId(projectId)` para wiki-rag/attachments donde `projectId` ya viene como arg +- Ambos devuelven la factory `createOpenRouter(...)` (cliente fresco por call, ningún estado compartido) +- Si nada resuelve, `apiKey: undefined` → OpenRouter tira 401 limpio en vez de silenciar +- 6 unit tests en `tenant-openrouter.test.ts` + +**Agentes** — todos con `model: async ({ requestContext }) => ...`: +- `agents/orchestrator.ts` — preserva extraBody con fallback chain + max_tokens 4000 +- `agents/triage-agent.ts` — mercury plain +- `agents/resolution-reviewer.ts` — mercury plain +- `agents/code-review-agent.ts` — mercury plain +- `agents/slack-notification-agent.ts` — preserva extraBody con fallback chain + max_tokens 800 + +**Tool — `mastra/tools/attachments.ts`**: +- Ambos paths resuelven per-tenant: AI SDK vision (imagen) + raw fetch a `openrouter.ai/api/v1/chat/completions` (PDF) +- Lee `projectId` desde `toolCtx.requestContext.get('projectId')` +- Input shape cambió de `(input)` directo a `(input, toolCtx?)` con manejo dual `{ context } | direct` (mismo patrón que resend.ts) + +**Lib — `lib/wiki-rag.ts`**: +- `generateWiki(projectId, ...)` y `queryWiki(query, projectId?, ...)` resuelven via helper +- Query-side con projectId undefined → cae al env (preserva comportamiento pre-tenant) + +**Workflow — `mastra/workflows/triage-workflow.ts`**: +Los 5 call sites de `.generate()` necesitan `requestContext` sintético porque no pasan por el middleware HTTP. Agregado helper `tenantContext(projectId)` que construye `new RequestContext([['projectId', ...]])`: +- `notifyStep:701` — `slackNotificationAgent.generate(msg, { requestContext })` +- `verifyStep:858` — `resolutionReviewer.generate(msg, { requestContext })` (ramo sin PR) +- `verifyStep:879` — `resolutionReviewer.generate(msg, { requestContext })` (ramo con PR, Promise.all) +- `verifyStep:882` — `codeReviewAgent.generate(msg, { requestContext })` +- `notifyResolutionStep:1022` — `slackNotificationAgent.generate(msg, { requestContext })` + +**Admin route `/api/test/slack-agent`** (`mastra/index.ts`): +Ahora acepta `projectId` opcional en el body y lo propaga vía `new RequestContext` al agent. Útil para smoke manual sin correr el workflow completo. + +**Smoke validado (2026-04-21)**: +- `POST /api/test/slack-agent` con `projectId=11111111-...` → logs `[tenant-keys] project= provider=openrouter source=env` + `provider=slack source=env`. Mensaje en Slack OK. +- `POST /chat` con header `x-project-id=b6892f6b-...` (slugify-test) → logs `[tenant-keys] project= provider=openrouter source=env`. Stream respondió "OK". + +**Gotchas**: +- `DynamicArgument` de Mastra acepta `T | (({ requestContext, mastra }) => T | Promise)` — la signatura exacta del agent `model:` está en `@mastra/core/dist/types/dynamic-argument.d.ts`. +- Workflow agents bypasseaan el middleware HTTP. Sin `new RequestContext([['projectId', ...]])` como segundo arg de `.generate()`, el agent ve un contexto vacío y cae al env aunque el projectId exista en `inputData`. +- `attachments.ts`: si agregas un proveedor que use OpenRouter con un endpoint raw (como el file-parser plugin), resolver la key vía `resolveKey('openrouter', projectId)` en vez de `process.env` directo — si no, el branch raw tendría comportamiento diferente al AI SDK. + +**Para #5 (UI BYO keys)**: +Backend de integrations ya listo: tabla `project_integrations` + `crypto-envelope` + `integration-routes.ts`. Falta solo UI — rediseño de `/integrations` con cards por dominio (ver siguiente sección). + +### 5a. UI BYO keys — OpenRouter slice ✅ CODE COMPLETE (2026-04-21) + +Primer slice de la UI de integrations, solo OpenRouter activa. Cierra el round-trip encrypt → decrypt → agent stream con un tenant key seedeado desde la UI. + +**Archivos**: +- `runtime/src/lib/auth-helpers.ts` (nuevo) — `extractSessionToken` + `getUserIdFromRequest`. 10 unit tests. Reemplaza las 3 copias inline en `project-routes.ts` y `mastra/index.ts`. +- `runtime/src/lib/integration-routes.ts` (rewrite completo) — 4 routes contra `project_integrations` encriptado: `GET`, `PUT`, `DELETE`, `POST /test`. Ownership check vía `projects.user_id = userId`. Drop del path plaintext viejo que escribía a `projects.linear_token / github_token / slack_*`. +- `runtime/src/lib/integration-routes.test.ts` (nuevo) — 15 tests: auth/ownership, PUT/DELETE, OpenRouter test mock (200/401/network), round-trip `resolveKey` devuelve `source=tenant` + aislamiento entre projects. +- `frontend/src/routes/integrations.lazy.tsx` (nuevo) — Página `/integrations` con 4 cards por dominio. Solo OpenRouter activa (input password + "Test & Save" + badge status + delete con confirm); las otras 3 son stubs "Coming soon". +- `frontend/src/routes/__root.tsx` — agregado nav entry `Integrations` con ícono `KeyRound` entre Projects y Settings. +- `frontend/src/routeTree.gen.ts` — incluye `/integrations` (regenera en build). +- `runtime/src/lib/project-routes.test.ts` — removido el `describe('integration-routes', ...)` viejo (215 líneas) y la única data-isolation test que tocaba `testLinearTokenRoute`. Helper `loadIntegrationRoutes` limpiado. + +**API shape**: +``` +GET /projects/:projectId/integrations → IntegrationSummary[] +PUT /projects/:projectId/integrations/:provider → { apiKey, meta? } → IntegrationSummary +DELETE /projects/:projectId/integrations/:provider → { deleted: true } +POST /projects/:projectId/integrations/:provider/test → { apiKey, meta? } | (sin body) → { valid, reason?, integration? } +``` + +`test` endpoint sólo persiste + `markTested(true)` cuando la key valida. Bogus/network errors NO persisten. Si no se pasa body, reusa la stored key (útil para "retest sin re-pegar"). + +**Gotcha del endpoint OpenRouter**: el handoff inicial decía usar `/v1/models` para validar. ❌ Ese endpoint es público y devuelve 200 incluso sin token. Cambiado a **`/api/v1/auth/key`** que 401s para bogus y 200s con metadata para real. + +**Gotcha del router plugin**: `@tanstack/router-plugin` no regeneró `routeTree.gen.ts` al correr `vite build` standalone — el `.tanstack/tmp` estaba root-owned y el plugin no pudo escribir. Workaround: editar el file manualmente siguiendo el pattern (el build del Docker image lo regenera correctamente porque el container user sí tiene permisos). + +**APP_MASTER_KEY setup** (prerequisito de smoke): +El `.env` actual **no contiene** `APP_MASTER_KEY` (el handoff anterior afirmaba que sí, pero `grep '^APP_MASTER_KEY' .env` retorna vacío). Antes de smokear: +```bash +echo "APP_MASTER_KEY=$(openssl rand -base64 32)" >> .env +docker compose up -d --force-recreate runtime # restart NO re-lee env_file +``` + +**Status del smoke** (parcial — faltan los pasos UI): +- ✅ `GET /projects/:id/integrations` sin cookie → 401 +- ✅ `GET /projects/:id/integrations` con cookie válida → 200, data `[]` +- ✅ `GET /projects/ghost/integrations` con cookie → 404 (no existence leak) +- ⏳ `POST .../openrouter/test` con bogus key → pendiente (bloqueado por APP_MASTER_KEY) +- ⏳ `POST .../openrouter/test` con real key → persist + `resolveKey` source=tenant +- ⏳ DELETE → `resolveKey` vuelve a env +- ⏳ UI walkthrough: abrir `/integrations`, pegar key, ver green "Saved", ver nav-switch a otro project → card independiente + +**Baselines**: TSC runtime 14 errores (baja 12 vs 26 baseline porque el rewrite eliminó el código viejo que los tenía). TSC frontend limpio. Tests: +25 nuevos passing (10 auth-helpers + 15 integration-routes), 106 failing estable (sin regresiones). + +**Followups para #5b**: +- Cards activas para **Linear** (paste key → `/api/v1/viewer` test → auto-fetch teams → guardar `teamId` en meta), **Resend** (paste key → send probe email → guardar `fromEmail` en meta), **Slack** (paste bot token → `auth.test` → guardar `channelId` en meta), **GitHub** (paste PAT → `/user` test → guardar `owner/repo` en meta). +- **`scoped-routes.ts` flip al encriptado**: los 3 endpoints `/projects/:id/linear/{issues,cycle,members}` hoy leen `project.linear_token` directo. Cuando la card de Linear aterrice en #5b, flipiar a `resolveKey('linear', projectId)` para que la UI pueda verificar que la key seedeada funciona end-to-end. +- **`webhook_secrets` table refactor**: todavía global (un row, PK=provider). Cuando la card de Linear registre webhooks per-tenant, mover a `(project_id, provider)` composite PK. +- **Ownership check en `scoped-routes.ts`**: hoy cualquier cookie-authed user puede leer los Linear issues de cualquier project. Replicar el pattern de `assertProjectOwnership` al hacer el flip. +- **Drop de columnas plaintext**: `projects.linear_token / linear_team_id / linear_webhook_id / linear_webhook_url / github_token / github_repo_owner / github_repo_name / slack_enabled / slack_channel_id / slack_webhook_url / resend_api_key`. Dejarlas nullable hasta que todos los readers hayan migrado; dropearlas en migration posterior. +- **Out of scope permanente**: `mastra/index.ts:43` LinearClient singleton (linear-sync cron es system-level, no per-tenant). + +### 5b. UI BYO keys — Linear slice ✅ DONE (2026-04-22) + +Aterrizó la card de Linear + cleanup del `/settings` legacy + flip del `scoped-routes.ts` al path encriptado + ownership gate en todas las rutas `/projects/:id/*`. Aún faltan Resend / Slack / GitHub cards (ahora #5c). + +**Qué shipped:** +- **Backend — `testLinearKey` en `runProviderTest`**: GraphQL `{ viewer { id } teams(first:100) { nodes { id name key } } }`. Auth header es raw PAT, sin `Bearer` (ver gotcha abajo). Retorna `{ valid: true, preview: { teams: [...] } }`. +- **`TestResult.preview` — contrato nuevo**: la valid-arm ahora tiene un campo opcional `preview`. Cuando está presente, `POST /integrations/:provider/test` valida pero **no persiste** — el cliente hace el save con `PUT /integrations/:provider` una vez el usuario eligió su meta. OpenRouter no tiene preview → sigue persistiendo en `/test` como atajo. Mismo shape reusable para Slack (channels) / GitHub (repos) cuando aterricen. +- **Frontend — `LinearCard` en `/integrations`**: paste PAT → Test → dropdown de teams del response → Save (PUT con `meta.teamId/teamName/teamKey`). Estado configured muestra "Team: X (KEY)" + Change + Trash con `ConfirmDialog`. Mismo styling/shape que `OpenRouterCard`. +- **`scoped-routes.ts` flipped**: los 3 handlers Linear (`/issues`, `/cycle`, `/members`) ya no leen `project.linear_token`. Resuelven via `getIntegrationKey(projectId, 'linear')` con fallback a env + legacy `projects.linear_team_id`. Helper nuevo: `resolveLinearContext(projectId, project)` retorna `{apiKey, teamId, source}` o error `no_key`/`no_team`. +- **Ownership gate en las 5 rutas scoped**: `assertProjectOwnership` extraído de `integration-routes.ts` a `auth-helpers.ts` y aplicado a `/linear/{issues,cycle,members}` + `/wiki/{generate,status}`. Antes cualquier cookie-authed user leía data de cualquier project — bug de seguridad pre-existing cerrado. +- **`/settings` deprecado**: sacado del sidebar (`__root.tsx`). La ruta `/settings` sigue viva con un banner "Moved to /integrations" para no 404 a bookmarks. Los endpoints runtime (`/api/config/status`, `/api/linear/members` global, `/api/linear/webhook/setup`) quedan en pie — se borran en el slice cleanup final, después de que Resend/Slack/GitHub cards migren su flow. +- **Tests**: `scoped-routes.test.ts` nuevo (13 passing, ownership + tenant/env resolución + regression guard contra lectura de `linear_token` plaintext). `integration-routes.test.ts` +6 (testLinearKey + preview no-persist + PUT con meta + raw header contract). El bloque scoped-routes de `project-routes.test.ts` quedó `describe.skip` — lo reemplaza el file nuevo con mejor wiring. + +**Baselines**: TSC runtime 5 errores (baja 9 vs 14 baseline — el flip eliminó los `projectId: string | undefined → getProject(projectId)` legacy). TSC frontend limpio. Tests: +19 nuevos passing, 106 failing estable (sin regresiones; los 13 tests que antes cubrían scoped-routes en project-routes.test.ts están skipped porque los reemplaza scoped-routes.test.ts). + +**Gotchas nuevos:** +- **Linear PAT header**: `Authorization: ` raw, SIN `Bearer`. OAuth tokens sí usan `Bearer`, pero los usuarios pegan PATs (`lin_api_...`) en la UI. Confirmado contra docs oficiales. +- **Linear 200 con GraphQL errors**: un PAT inválido puede devolver 200 HTTP con `errors: [{extensions: {code: "AUTHENTICATION_ERROR"}}]` en vez de 401 HTTP directo. `testLinearKey` inspecciona ambos paths. +- **Caddy strip `/api` prefix**: `registerApiRoute('/x', ...)` se sirve en `/x` (sin `/api`). Caddy strippea el prefix en `@projects_api_prefixed`, `@obs_api`, `@integrations_api`. Al testear con curl directo a `localhost:4111`, usar la path sin `/api`. Via frontend (`localhost:3001` o browser) sí va con `/api`. +- **`project-routes.test.ts` cookie format**: usa `cookie: 'session=test-session-token'` (broken, no matchea el regex `better-auth.session_token=...` de `auth-helpers.extractSessionToken`). Los tests CRUD del file siguen rotos por eso — son parte del baseline. No es regresión de este slice. + +**Followups para #5c (próximo):** +- **Resend card**: `fromEmail` como input manual (la API no tiene `/me`); test probando `GET /domains`. Meta = `{ fromEmail }`. Migrar `reporter_email` de `localStorage` (hoy en `chat.tsx:352,498,515`) a `meta.defaultReporterEmail`. +- **Slack card**: `auth.test` + `conversations.list` → picker de channel → guardar `channelId` + `channelName` en meta. +- **GitHub card**: `GET /user` + owner/repo como 2do paso (picker tipo GitHub App, o input manual con validación contra `GET /repos/:owner/:repo`). +- **Borrar endpoints legacy del runtime**: `/api/config/status`, `/api/linear/members` (global), `/api/linear/webhook/setup` + la ruta `/settings` completa + los imports en `mastra/index.ts`. Hacerlo después de los 3 cards arriba. +- **`webhook_secrets` refactor a PK compuesta**: sigue global. Cuando Linear card agregue "Register webhook" (dentro de #5c o slice dedicado), migrar a `(project_id, provider)` con `setWebhookSecret(projectId, provider, secret)`. Incluye cambio en el handler `/webhooks/linear` para lookup por project. +- **Drop columnas plaintext en `projects`**: `linear_token`, `linear_webhook_id/url`, `github_token`, `github_repo_owner/name`, `slack_enabled`, `slack_channel_id`, `slack_webhook_url`, `resend_api_key`, `linear_team_id` (leído solo por env-fallback hoy; una vez que todos los projects migren, se puede dropear). Después del cleanup de endpoints. + +### 5c. UI BYO keys — Slack/Resend/GitHub ✅ DONE (2026-04-22) + +Cerró el bucle de 5 providers en `/integrations`. 2 commits: `d3d9c7c` (backend) + `4a350be` (frontend). + +**Qué shipped:** +- **Backend — `resolveKey` shape change**: ahora retorna `{key, meta, source}` en vez de `{key, source}`. Meta se propaga desde la row tenant en `getIntegrationKey`. Callers que solo necesitan `key` siguen funcionando (additive). Tool consumers que leen meta: `resolveResend` (meta.fromEmail precedence meta > env > fallback) y `resolveSlack` (metaChannel de la row, salda el TODO de slack.ts:31-35). +- **Backend — 3 dispatchers nuevos en `integration-routes.ts`**: + - `testSlackKey`: POST `auth.test` (valida token) + GET `conversations.list?types=public_channel,private_channel&limit=200` → `preview.channels`. **Tolerante a missing_scope**: si auth.test pasa pero la list falla (ej. token con solo `chat:write`), retorna `valid:true` con `preview.channels: []`. UI cae a input manual. + - `testResendKey`: GET `/domains` → `valid:true`/`invalid_key` binario. Sin preview → persiste con `meta.fromEmail` del body (atajo tipo OpenRouter). + - `testGithubKey`: GET `/user` + GET `/user/repos?per_page=100&sort=updated` → `preview.repos`. Headers: Bearer + `X-GitHub-Api-Version: 2022-11-28`. +- **Frontend — `components/picker.tsx` nuevo**: `Picker` genérico con `items/value/getValue/getLabel/onChange/placeholder`. Outside-click + Escape. `TeamPicker` borrado — `LinearCard` migrado al shared. Rule-of-three cumplido: Linear teams + Slack channels + GitHub repos. +- **Frontend — 3 cards nuevas en `integrations.lazy.tsx`**: + - `SlackCard`: `tokenValidated` state separa "token ok" de "enumeration ok". Si channels está vacío → input manual de channelId con hint de scopes (`channels:read`, `groups:read`). Meta guardada: `{channelId, channelName?}` — channelName solo si pickeado del picker. + - `ResendCard`: 2 inputs (PAT + fromEmail) + validación client-side de email con regex. Un solo botón "Test & Save". Meta: `{fromEmail}`. + - `GitHubCard`: flujo tipo Linear (PAT → Test → picker → Save). Meta: `{owner, repo, repoFullName}`. **Nota**: este picker queda redundante con `projects.repo_url` — se simplifica a "verify access" en #5d. + - Los 3 `StubCard` borrados. +- **Frontend — `chat.tsx` reporter_email migration**: los 3 callsites (`handleCreateTicket`, `handleUpdateExisting`, `handleCreateNew`) leen `user?.email` de `useAuth()` en vez de `localStorage.getItem('reporter_email')`. Early-return si no hay user (chat está detrás de auth). La key `reporter_email` en localStorage quedó muerta — verificado sin writers restantes. +- **Tests**: `integration-routes.test.ts` +10 (Slack preview/missing_scope/invalid_auth/network/PUT con channelId, Resend persist+meta/401, GitHub preview/401/headers). `tenant-keys.test.ts` +1 meta propagation. `resend.test.ts` +1 meta.fromEmail override via dynamic re-import (patrón ya usado en el file, cuidado con el leak flag de vi.doMock+resetModules). `integration-routes.test.ts` -1 (borrado el test `not_implemented` ahora obsoleto — los 5 providers tienen dispatcher). + +**Baselines al cierre**: runtime TSC 5 errores estable, frontend TSC limpio, tests 106 failing baseline, +12 nuevos passing, 0 regresiones. + +**Gotchas nuevos:** +- **lucide-react pineado ^1.7.0**: no exporta `Github` icon — usé `Code2` para la GitHubCard (mismo icono que tenía el StubCard). Si alguien quiere el logo real eventualmente, es slice aparte (bump de lucide). +- **Slack missing_scope no es fatal**: tokens con solo `chat:write` (lo típico de un bot minimal) no pueden listar canales. Mi dispatcher lo trata como "token valid, UI lo resuelve". Si en el futuro agregamos un warning textual en la response, extender `TestResult` con campo `warning`. +- **Resend preview shortcut**: a diferencia de Slack/Linear/GitHub, Resend persiste en `/test` sin preview (como OpenRouter) porque `fromEmail` viene del body del request — no hay nada que pickear. El cliente manda `{apiKey, meta: {fromEmail}}` directo al `/test` y backend hace set+markTested en un call. + +**Followups (abiertos para #5d y posteriores):** +- **`GitHubCard` → PAT + "Verify access to {project.repoFullName}"**: sin picker de repos. El repo del proyecto es source of truth. Si el PAT no tiene acceso → error claro. +- **Private repo detection al crear proyecto** (#5d Pattern C — ver abajo). +- **`wiki_status='needs_auth'`** como nuevo estado, con UI prompt para conectar GitHub. +- **Clone autenticado en wiki-rag.ts**: inyectar PAT `https://x-access-token:@github.com/...` cuando hay integration GitHub para el proyecto. +- **`webhook_secrets` refactor a PK compuesta** (pendiente de #5b): seguirá global hasta que se agregue "Register webhook" button. +- **Drop columnas plaintext en `projects`**: sin cambio desde #5b. Requiere migrar los callers primero. +- **Legacy endpoints runtime**: `/api/config/status`, global `/api/linear/members`, `/settings` route con banner — todos siguen vivos. Cleanup en slice aparte (no crítico). + +### 5d. GitHub/wiki unification + private repo detection ✅ DONE (2026-04-22) + +Unifica `projects.repo_url` (wiki clone) con el `meta.repoFullName` de la integration GitHub (fuente única = `projects.repo_url`) y habilita repos privados vía PAT del tenant. Pattern C progressive locked in y shipped. + +**Decisiones UX aplicadas** (pre-acordadas, no redebatir): +- Un proyecto = un repo. GitHub card sin picker; "Verify access to {projectRepoFullName}". Si el usuario quiere otro repo, crea proyecto nuevo. +- Probe detecta privacidad al crear proyecto. Non-github URLs (gitlab/bitbucket/self-hosted) saltan el probe y siguen el path público de siempre — el probe es optimización, no gate. +- `needs_auth` vive en `projects.status` (la columna activa), no en `projects.wiki_status` (que está efectivamente muerto post-#2 bug pre-existing). Consolidar las dos columnas queda como followup. + +**Qué shipped:** + +- **Backend — `runtime/src/lib/github-repo.ts` (nuevo)**: `parseGithubRepoUrl` acepta https/ssh/scp URLs (`.git` opcional, trailing slash OK, case-preservado) y retorna null para non-github. `buildAuthenticatedCloneUrl(owner,repo,pat)` → `https://x-access-token:@github.com/owner/repo.git` con PAT URL-encoded. `scrubPatFromString(s, pat?)` redacta tanto el patrón `x-access-token:[^@]*@` como el PAT literal. 19 unit tests. + +- **Backend — probe en `POST /projects`** (`project-routes.ts`): después del INSERT, si `parseGithubRepoUrl(repositoryUrl)` matchea, llama `probeGithubRepoVisibility` con AbortController 2000ms. 200 → `status` sigue `'pending'`, se dispara `generateWiki`. 404/403 → UPDATE `status='needs_auth'` + `wiki_error='Private repo or not found. Connect GitHub in /integrations to enable wiki.'`, NO dispara `generateWiki`. Timeout/network → fallback al path público (log `[projects] GitHub probe failed... treating as public`). Non-github URLs saltan el probe por completo. Response body refleja el status final. 4 tests nuevos en `project-routes.probe.test.ts` (nuevo file — reemplaza la cobertura que el viejo `project-routes.test.ts` no daba por quedar en baseline failing). + +- **Backend — clone auth + PAT scrub en `wiki-rag.ts`**: `generateWiki` resuelve `resolveKey('github', projectId)` antes del `execFileSync`. Si key + URL parsea github → clone URL se reescribe via `buildAuthenticatedCloneUrl`. Defense-in-depth: si el proyecto está en `status='needs_auth'` y sigue sin PAT, early-return sin tocar la DB (en vez de re-intentar clone público que fallará). Todos los `console.warn`/`console.error` + el write a `projects.error` pasan por `scrubPatFromString(msg, cloneSecret)`. El git stderr rutinariamente echoe la URL autenticada, así que el scrub evita que el PAT quede plaintext en la DB o en los logs. + +- **Backend — `testGithubKey` simplificado** (`integration-routes.ts`): drop del `GET /user/repos`. Solo `GET /user` (200 → `{valid:true}` sin preview, 401/403 → `invalid_key`). El path shortcut-persist de `/test` sigue vivo pero escribe meta vacío; PUT siempre sobre-escribe con el meta real del repo del proyecto, así que la UI va direct-PUT. + +- **Backend — `PUT /integrations/github` con repo-access validation** (`integration-routes.ts:handleGithubPut`): cuando `provider==='github'`, el handler branchea al helper dedicado. Load project → parse `repo_url`. Si non-github → 400 `PROJECT_REPO_NOT_GITHUB`. Si github → probe `GET api.github.com/repos/:o/:r` con Bearer PAT. 200 → `setIntegrationKey` con meta auto-derivado `{owner, repo, repoFullName}` (ignora cualquier meta del body); si `status ∈ {needs_auth, error}` → UPDATE a `'pending'`, `wiki_error=NULL`, dispara `generateWiki` en background. 401/403 → 400 `REPO_ACCESS_DENIED`. 404 → 400 `REPO_NOT_FOUND_WITH_TOKEN`. 6 tests nuevos en `integration-routes.test.ts` cubren los 5 paths + el guard que NO retriggerea wiki cuando ya estaba pending/ready. + +- **Backend — `__setClientForTests` real en integration-routes.ts**: era no-op desde #5a, ahora cablea un client compartido que los tests wirean junto con keys + auth. Necesario para que los nuevos tests de PUT /github puedan asertar contra project rows reales en `:memory:` libsql. + +- **Frontend — `Project.status` type + `projects.lazy.tsx`**: unión extendida con `'needs_auth'`, nuevo entry en `statusConfig` (Lock ámbar), prompt inline "Private repo detected — connect GitHub" con Link a `/integrations` (aparece debajo del status badge cuando `status==='needs_auth'`). `refetchInterval:5000` existente auto-recoge el flip tras wiki-retry. + +- **Frontend — `GitHubCard` rewrite**: sin picker, sin `repos`/`setRepos`/`selectedRepoId` state, sin `testMutation` separado. Single PUT mutation con `{apiKey}` a `/integrations/github`. Botón dinámico "Verify access to {repoFullName}" — repoFullName viene de `useQuery(['project', projectId])` contra `/projects/:id` o del meta de la integration configurada. Branch non-github: si `parseGithubRepo(project.repositoryUrl)` es null, render disabled con "This project's repository isn't hosted on GitHub, so no token is needed." (pre-empta el `PROJECT_REPO_NOT_GITHUB` del backend). Configured state suma NUEVO botón "Retry wiki" que POSTea `/projects/:id/wiki/generate` e invalida `['projects']` — útil tras fallas transitorias del clone sin tener que re-pegar el PAT. Success/error toasts separados para verify vs retry. Dead types `GithubRepo` + `repos?` del `TestPreview` (frontend + backend `integration-routes.ts`) removidos. + +**Baselines al cierre**: runtime TSC 5 errores estable (3 scoped-routes Cycle casts + 2 sendSlackMessage possibly-undefined — pre-existing). Frontend TSC limpio. Tests: 106 failing baseline estable + 29 nuevos passing (**19** github-repo parser/scrub + **4** POST /projects probe + **5** PUT /github variants + **1** /test simplificado). **0 regresiones netas**. Total 573 passing / 106 failing / 59 skipped. + +**Gotchas nuevos:** + +- **PAT leak via git stderr** — `execFileSync('git', [..., cloneUrl, tmpDir])` echoes the clone URL (incluyendo `x-access-token:@`) en stderr cuando el clone falla. El write a `projects.error` y cualquier `console.error` del pipeline path DEBEN pasar por `scrubPatFromString(msg, cloneSecret)`. Si agregás otro write/log en `wiki-rag.ts` o código que manipule el clone URL, acordate del scrub. +- **`needs_auth` es un valor de `projects.status`, no un booleano aparte** — las rutas viejas que chequeaban `status === 'ready'` como gate siguen funcionando (needs_auth no es ready). Handlers que hacen `project.status === 'ready' || project.status === 'error'` para el "done" flag del wiki status endpoint correctamente tratan needs_auth como no-done. No hay que propagar chequeos defensivos nuevos en los callers. +- **Retry trigger semantics en PUT /github** — dispara `generateWiki` solo si `status ∈ {needs_auth, error}`. Pending/processing/ready no retriggerean (evita re-vectorizar repos ya indexados y race conditions con el processing en vuelo). Si el usuario quiere forzar un retry en un proyecto `ready`, usa el botón "Retry wiki" en la GitHubCard configurada (llama `/projects/:id/wiki/generate` direct). +- **ESM + vi.hoisted para mocks de módulos con consts** — `vi.mock('./wiki-rag', () => ({ generateWiki: mockVar }))` con `const mockVar = vi.fn(...)` falla porque el `vi.mock` se hoistea arriba del `const`. Usar `const { mockVar } = vi.hoisted(() => ({ mockVar: vi.fn(...) }))` — patrón aplicado en `integration-routes.test.ts`. +- **Tests `:memory:` contra `project-routes.ts` necesitan `__setClientForTests`** — vi.spyOn sobre exports ESM de `@libsql/client` falla (`Cannot redefine property`). Export un setter en vez de spiar. Patrón ya aplicado en scoped-routes, integration-keys, y ahora project-routes + integration-routes. + +**Followups (no bloqueantes, abiertos para slices futuros):** + +- **Consolidar `projects.status` ↔ `projects.wiki_status` ↔ `projects.error` ↔ `projects.wiki_error`**: pre-existing dual-source-of-truth en varias dimensiones. `wiki-rag.ts` escribe `projects.error` pero el `GET /projects` lee `projects.wiki_error` → errores de wiki nunca llegan a la UI. `scoped-routes.ts:/wiki/generate` escribe `wiki_status='processing'` pero `generateWiki` escribe `status`. No tocado en #5d (scope-out explícito). Consolidar en un solo path. +- **`scoped-routes.ts:/wiki/generate` no chequea `status==='needs_auth'`**: hoy no hay UI pública que trigger ese endpoint (excepto el retry button nuevo en GitHubCard, que va desde un proyecto que YA tiene PAT configurado), pero si se expone, debería respetar needs_auth. Añadir guard. +- **PATCH `/projects/:id` cambiando `repo_url` cuando hay integration github existente**: meta de la integration quedaría stale. Opciones: invalidar la integration (`status='invalid'`) al cambiar repo_url, o re-correr el repo-access probe inline. No crítico porque el backend hace server-side validation al usar el PAT. +- **`webhook_secrets` refactor a PK compuesta**: sigue global. Pending desde #5a. +- **Drop columnas plaintext en `projects`**: `linear_token`, `linear_webhook_id/url`, `github_token`, `github_repo_owner/name`, `slack_enabled`, `slack_channel_id`, `slack_webhook_url`, `resend_api_key`, `linear_team_id`. Todos los readers ya migraron; dejarlas nullable y dropear en migration dedicada. +- **Legacy endpoints runtime**: `/api/config/status`, global `/api/linear/members`, `/settings` route con banner — cleanup aparte. +- **`mastra/index.ts:/wiki/status` (admin route)** lee `projects.status` + `projects.error` de la row más reciente (no scoped por user). Es pre-existing admin endpoint; no scoped a tenant. Revisar si sigue siendo necesario o si `scoped-routes.ts:/wiki/status` lo reemplaza. +- **`lucide-react` bump** para tener el logo oficial de GitHub (hoy usamos `Code2` como proxy). No crítico. + +**Smoke validado** (runtime + frontend containers recreados 2026-04-22 tras landing completo): init-db no tuvo nuevos ALTERs (`needs_auth` es solo un string en la columna TEXT existente), runtime arrancó sin errores de import, containers healthy. El matrix UI (crear proyecto público → proyecto privado → connect PAT → retry wiki → PAT sin acceso → URL gitlab → verificar que `projects.error` no contiene el PAT tras un clone fallido) queda para el usuario — todos los code paths están testeados unit + integration. + +### 6. Test-connection buttons + onboarding wizard +- Endpoint `POST /integrations/:provider/test` con la key provisional, sin persistir — **DONE en #5a-#5c para los 5 providers**. Falta el wizard. +- Wizard de primer uso: "antes de usar Triage necesitas configurar: [✓ Linear] [✗ Email] [✗ Slack] [✓ LLM]" + +### Postergable +- RBAC intra-tenant (al principio basta con project owner) +- Billing / límites de uso (trackeo en `llm_usage` ya, enforcement después) +- SSO / SAML +- Rate limiting en `/chat` (necesario antes de producción pública, no antes de multi-tenant) +- Migraciones de DB propias (actualmente `init-db.ts` hace ALTER IF NOT EXISTS; va a doler eventualmente) + +--- + +## Gotchas conocidos (leer antes de tocar código) + +- **Docker `.env` reload**: `docker compose restart ` NO re-lee `env_file`. Usar `docker compose up -d --force-recreate ` cuando cambias secretos. Verificar con `docker exec printenv `. +- **Mastra v1.4 tool invocation**: llamar `tool.execute({context:{...}})` directo retorna `{success:false}` silencioso. Usar el helper `callTool(tool, args)` del workflow que pasa ambas shapes + runtimeContext `{}`. +- **OpenRouter version-pinned IDs**: retorna modelos como `inception/mercury-2-20260304`. `MODEL_PRICING` usa prefix match en `provider-pricing.ts`. Si agregas modelo nuevo, solo necesitas la key base. +- **Rules of Hooks**: `frontend/src/routes/chat.tsx` tiene early returns por `authLoading` y `currentProjectId` — **deben ir después** de todos los hooks. Un refactor descuidado reintroduce el bug de hooks. +- **Workflow tools y `requestContext`**: los tools invocados desde el workflow via `callTool()` NO pasan por el middleware HTTP que pone `projectId` en `requestContext`. El workflow tiene que construir un runtimeContext sintético manualmente: `callTool(tool, input, { projectId: inputData.projectId })`. Sin eso, los tools siempre ven `projectId=undefined` y caen a env-fallback. Esto significa que `projectId` **debe** propagarse por las schemas del workflow chain (ticketStep→notifyStep→suspendStep→verifyStep→notifyResolutionStep) como `z.string().optional()`. Crítico que esté en `suspendStep.inputSchema` para sobrevivir el suspend/resume snapshot. +- **`vi.doMock` + `vi.resetModules` leak**: si un test hace `vi.doMock('../../lib/tenant-keys')` y después `vi.resetModules()`, el mock queda activo para tests subsiguientes en el mismo file. Tests que checquean `(Resend as any).mock.calls.length` después de `await import('resend')` van a ver un mock rebindeado en vez del original. Preferir verificar via `mockEmailsSend.toHaveBeenCalledTimes(n)` (la call chain del mock original) en vez de contar constructor calls. +- **Memory storage per-agent**: `orchestrator.ts` usa su propio `LibSQLStore` (`memory-store`). Mastra top-level storage es separado (`triage-main`). Para leer mensajes del thread usar `agent.getMemory()?.recall()` — el endpoint `/memory/threads/:threadId/messages` ya lo hace. +- **`include_reasoning: true` en OpenRouter** reserva el full output budget → 402 spurious aunque haya saldo. Está removido, no lo re-agregues sin verificar. +- **Slack minimal-scope tokens**: un bot con solo `chat:write` autentica en `auth.test` pero falla en `conversations.list` con `missing_scope`. El dispatcher lo trata como "valid, preview vacío" y el UI cae a input manual de channelId. Si en el futuro agregamos un indicador textual en la response, extender `TestResult` con campo opcional `warning`. No es un bug — es reflejo de cómo está configurado el bot en `.env`. +- **`resolveKey` retorna `meta`**: desde #5c el shape es `{key, meta, source}`. Mocks viejos que retornan `{key: null, source: 'none'}` sin meta pasan hoy porque nada lee `meta` en esos paths, pero si agregás código nuevo que lea meta (ej. el github clone auth de #5d) acordate de actualizar los mocks. +- **lucide-react ^1.7.0**: versión pineada pre-rebranding, no tiene brand icons como `Github`. Usar `Code2` o `FolderGit2` como proxy. Bump aparte si querés el logo real. + +--- + +## Comandos de desarrollo + +```bash +# Levantar todo +cd /home/riislly/triage && docker compose up -d + +# Ver logs +docker compose logs -f runtime +docker compose logs -f frontend + +# Reiniciar runtime después de cambios (código tiene hot-reload; +# para cambios en .env usar --force-recreate) +docker compose restart runtime +docker compose up -d --force-recreate runtime # si cambiaste .env + +# Testear webhook sin mover ticket en Linear (ahora firma el payload con HMAC-SHA256) +./scripts/simulate-webhook.sh done +./scripts/simulate-webhook.sh in-review +# El script lee el secret de webhook_secrets; si no hay, exportar WEBHOOK_SECRET= + +# Inspeccionar libsql +docker exec triage-runtime-1 node -e " + const { createClient } = require('@libsql/client'); + const db = createClient({ url: 'http://libsql:8080' }); + db.execute('SELECT * FROM llm_usage ORDER BY created_at DESC LIMIT 5').then(r => + console.log(JSON.stringify(r.rows, null, 2)) + ); +" + +# Frontend: http://localhost:3001 +# Runtime API: http://localhost:4111 (health: /health, API: /api/*) +``` + +--- + +## Dónde consultar + +- **Memoria persistente:** `/home/riislly/.claude/projects/-home-riislly-triage/memory/MEMORY.md` — entradas sobre docker env reload, webhook scripts, y llm_usage wiring. +- **Infra handoff:** `HANDOFF.md` en root — diagrama de contenedores + puertos. +- **Arquitectura general:** `ARCHITECTURE.md` en root. +- **Docs de Mastra:** usar MCP `mastra` tools (`searchMastraDocs`, `readMastraDocs`) para verificar API signatures; están cambiando rápido (v1.4). +- **Git log reciente (todo pusheado a origin):** + - `4a350be` — feat(ui): Slack/Resend/GitHub cards + account-email reporter (#5c) + - `d3d9c7c` — feat(integrations): Slack/Resend/GitHub test dispatchers + tenant meta (#5c) + - `4c518bf` — feat(ui): Linear card + agent project clarification (#5b) + - `38fc56e` — feat(integrations): Linear tenant key + scoped-routes ownership (#5b) + - `6206218` — docs: handoff update for #5a slice closure + #5b followups + - `1caef24` — feat(ui): /integrations page with OpenRouter card + confirm dialog (#5a) + - `82713c2` — refactor(integrations): rewrite integration-routes against encrypted path + auth-helpers (#5a) + - `124f29a` — feat(integrations): resolve OpenRouter keys per-tenant for agents/tools (#4b) + - `54de998` — feat(integrations): resolve API keys per-tenant in tools (#4a) + - `74e05f2` — feat(integrations): envelope-encrypted per-tenant keys schema (#3) + - `6dcc311` — feat(wiki): project-aware orchestrator + pipeline fixes (#2) + - `63b13e6` — feat(webhooks): verify Linear signatures con HMAC-SHA256 + secret persistence (#1) + +--- + +## Qué decirle al próximo agente al arrancar + +> "Estamos en `fix/ui-cleanup-cycle-panel`. #1-#5d cerrado code-side (tip pushed: `4a350be`; #5d landed local pending commit). Arco multi-tenant completo: webhook verif, wiki, per-tenant schema/keys/agents/tools, 5 integration cards (OpenRouter/Linear/Slack/Resend/GitHub) + GitHub/wiki unification con private-repo detection (Pattern C). Lee `docs/HANDOFF-MULTI-TENANT-HARDENING-2026-04-17.md` **sección 5d** antes de tocar código del lado github/wiki — ahí están los gotchas frescos (PAT leak en git stderr + scrub obligatorio, retry trigger semantics, ESM `vi.hoisted` para module mocks) y los followups que quedaron explícitamente fuera. Smoke UI del #5d (probe público, needs_auth flip, connect PAT, retry wiki, PAT sin acceso, URL gitlab, PAT scrub en DB) todavía no corrió — todos los paths están testeados unit + integration pero el walkthrough en browser queda pendiente. Próximo slice natural es #6 (onboarding wizard + cleanup de endpoints legacy + drop de columnas plaintext). No toques `.env` ni hagas commits sin que te lo pida. Si ves algo raro en el estado del repo, preguntame antes de accionar." diff --git a/frontend/bun.lock b/frontend/bun.lock index 6dd32bc..bbfcf01 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -5,14 +5,25 @@ "": { "name": "vite-app", "dependencies": { + "@ai-sdk/react": "^3.0.155", "@fontsource-variable/geist": "^5.2.8", + "@fontsource-variable/inter": "^5.2.8", + "@fontsource-variable/jetbrains-mono": "^5.2.8", + "@fontsource-variable/space-grotesk": "^5.2.10", + "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.2.1", + "@tanstack/react-query": "^5.96.2", + "@tanstack/react-router": "^1.168.10", + "ai": "^6.0.153", + "better-auth": "^1.6.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.7.0", "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1", "shadcn": "^4.2.0", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.1", @@ -20,6 +31,8 @@ }, "devDependencies": { "@eslint/js": "^9.39.4", + "@tanstack/router-devtools": "^1.166.11", + "@tanstack/router-plugin": "^1.167.12", "@types/node": "^24.12.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -37,6 +50,14 @@ }, }, "packages": { + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="], + + "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], + + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.23", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg=="], + + "@ai-sdk/react": ["@ai-sdk/react@3.0.170", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.23", "ai": "6.0.168", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-YUDn+mK0c8iUz14rCBf1A0zg6SV5b5aSVUz+azF1bdBd1SFXVI19dKYR+PQSpZY+0+z+zs252AAsacUqiO98Kw=="], + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], @@ -97,6 +118,24 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@better-auth/core": ["@better-auth/core@1.6.6", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-NdG2oTlbkA8TrxvFUL8QDpyTxY3JtCGwuf2qzg1nu9VroSV5ujWZz/N9CGQrZd7k0P41SYuXIl64Ysx3uDIymQ=="], + + "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.6", "", { "peerDependencies": { "@better-auth/core": "^1.6.6", "@better-auth/utils": "0.4.0", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-guG4pXD7TDo0HWO/orvIvGN+2wpNjXNZ0qywx48IatMJSXqwb09mCeYwX0wOMRopVQu5if9LEQSH977ueKQvgA=="], + + "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.6", "", { "peerDependencies": { "@better-auth/core": "^1.6.6", "@better-auth/utils": "0.4.0", "kysely": "^0.28.14" }, "optionalPeers": ["kysely"] }, "sha512-k8IWYk+MnvD/+Oxswbzzc+ujYZFWuadqmh6y/oBHkDOm8Jgf52dwTml0pzNMksIj9tKIexBzXmDTuL7TTPfluw=="], + + "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.6", "", { "peerDependencies": { "@better-auth/core": "^1.6.6", "@better-auth/utils": "0.4.0" } }, "sha512-S7TpQYbZwLeEa8mog5dzBGxv5pEhVDMF8J89fMCTb82TIHOOnFkBmStfbJZmPBV0Pwozoc+IV24R6ePxDWfrEA=="], + + "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.6", "", { "peerDependencies": { "@better-auth/core": "^1.6.6", "@better-auth/utils": "0.4.0", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-TngjKM3xdj6odQG2IXIE1bXPnMdzPtzXVGRFI4JNAMLFe932ntrSgZqAcbyl/iroQcScyX+DtI9ttNwdRf2S4w=="], + + "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.6", "", { "peerDependencies": { "@better-auth/core": "^1.6.6", "@better-auth/utils": "0.4.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-aYuxHrh2WxYwXF1RwWdwz8pqS3aXEIUyxwHrmFRqWQ2cHJcemD2qFgaWTuwomRFU12/oAQTZy1NMmFQLr7crPA=="], + + "@better-auth/telemetry": ["@better-auth/telemetry@1.6.6", "", { "peerDependencies": { "@better-auth/core": "^1.6.6", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21" } }, "sha512-yGpy0y6cJCZx7S8TqbgV+eOK1o99cjEParTxaW9uwqxphz/2QzVWgeAe6rSA6C8fXJQmOsVbz/LctqZaGjG+IA=="], + + "@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], + + "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], + "@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.60.2", "", { "dependencies": { "commander": "^11.1.0", "dotenv": "^17.2.1", "eciesjs": "^0.4.10", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "picomatch": "^4.0.2", "which": "^4.0.0", "yocto-spinner": "^1.1.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-r4AznHUvfLONuWdoSIQtut6Ez/ym+lGXRtDvRaoAEMEhAmwSoK24jRsfR28vcb3ygWm7qeYOcbZolhtseJl6mA=="], "@ecies/ciphers": ["@ecies/ciphers@0.2.6", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g=="], @@ -181,6 +220,12 @@ "@fontsource-variable/geist": ["@fontsource-variable/geist@5.2.8", "", {}, "sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw=="], + "@fontsource-variable/inter": ["@fontsource-variable/inter@5.2.8", "", {}, "sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ=="], + + "@fontsource-variable/jetbrains-mono": ["@fontsource-variable/jetbrains-mono@5.2.8", "", {}, "sha512-WBA9elru6Jdp5df2mES55wuOO0WIrn3kpXnI4+W2ek5u3ZgLS9XS4gmIlcQhiZOWEKl95meYdvK7xI+ETLCq/Q=="], + + "@fontsource-variable/space-grotesk": ["@fontsource-variable/space-grotesk@5.2.10", "", {}, "sha512-yJQO/o35/hAP3CFnpdFTwQku2yzJOae2HIpBmqkOVoxhhXJaQP3g+b6Jrz7u+eI7A5ZdCIf88uMWpBJdFiGr5w=="], + "@hono/node-server": ["@hono/node-server@1.19.13", "", { "peerDependencies": { "hono": "^4" } }, "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -215,11 +260,11 @@ "@mswjs/interceptors": ["@mswjs/interceptors@0.41.3", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA=="], - "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], + "@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="], "@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], - "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -233,6 +278,10 @@ "@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], @@ -409,6 +458,8 @@ "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="], @@ -437,8 +488,38 @@ "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="], + "@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="], + "@tailwindcss/vite": ["@tailwindcss/vite@4.2.2", "", { "dependencies": { "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "tailwindcss": "4.2.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w=="], + "@tanstack/history": ["@tanstack/history@1.161.6", "", {}, "sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg=="], + + "@tanstack/query-core": ["@tanstack/query-core@5.99.2", "", {}, "sha512-1HunU0bXVsR1ZJMZbcOPE6VtaBJxsW809RE9xPe4Gz7MlB0GWwQvuTPhMoEmQ/hIzFKJ/DWAuttIe7BOaWx0tA=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.99.2", "", { "dependencies": { "@tanstack/query-core": "5.99.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-vM91UEe45QUS9ED6OklsVL15i8qKcRqNwpWzPTVWvRPRSEgDudDgHpvyTjcdlwHcrKNa80T+xXYcchT2noPnZA=="], + + "@tanstack/react-router": ["@tanstack/react-router@1.168.23", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.168.15", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-+GblieDnutG6oipJJPNtRJjrWF8QTZEG/l0532+BngFkVK48oHNOcvIkSoAFYftK1egAwM7KBxXsb0Ou+X6/MQ=="], + + "@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.166.13", "", { "dependencies": { "@tanstack/router-devtools-core": "1.167.3" }, "peerDependencies": { "@tanstack/react-router": "^1.168.15", "@tanstack/router-core": "^1.168.11", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-6yKRFFJrEEOiGp5RAAuGCYsl81M4XAhJmLcu9PKj+HZle4A3dsP60lwHoqQYWHMK9nKKFkdXR+D8qxzxqtQbEA=="], + + "@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="], + + "@tanstack/router-core": ["@tanstack/router-core@1.168.15", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-Wr0424NDtD8fT/uALobMZ9DdcfsTyXtW5IPR++7zvW8/7RaIOeaqXpVDId8ywaGtqPWLWOfaUg2zUtYtukoXYA=="], + + "@tanstack/router-devtools": ["@tanstack/router-devtools@1.166.13", "", { "dependencies": { "@tanstack/react-router-devtools": "1.166.13", "clsx": "^2.1.1", "goober": "^2.1.16" }, "peerDependencies": { "@tanstack/react-router": "^1.168.15", "csstype": "^3.0.10", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["csstype"] }, "sha512-Qs8gkyI7m+eAxG3VcIOHuTSsUfA5ZxZcOa99ZyIIIJFxW6hy1k+m2s1J0ZYN1SNlip8P2ofd/MHiqmR1IWipMg=="], + + "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.167.3", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16" }, "peerDependencies": { "@tanstack/router-core": "^1.168.11", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-fJ1VMhyQgnoashTrP763c2HRc9kofgF61L7Jb3F6eTHAmCKtGVx8BRtiFt37sr3U0P0jmaaiiSPGP6nT5JtVNg=="], + + "@tanstack/router-generator": ["@tanstack/router-generator@1.166.32", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.168.15", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "magic-string": "^0.30.21", "prettier": "^3.5.0", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-VuusKwEXcgKq+myq1JQfZogY8scTXIIeFls50dJ/UXgCXWp5n14iFreYNlg41wURcak2oA3M+t2TVfD0xUUD6g=="], + + "@tanstack/router-plugin": ["@tanstack/router-plugin@1.167.22", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.168.15", "@tanstack/router-generator": "1.166.32", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.168.21", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"], "bin": { "intent": "bin/intent.js" } }, "sha512-wYPzIvBK8bcmXVUpZfSgGBXOrfBAdF4odKevz6rejio5rEd947NtKDF5R7eYdwlAOmRqYpLJnJ1QHkc5t8bY4w=="], + + "@tanstack/router-utils": ["@tanstack/router-utils@1.161.6", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-nRcYw+w2OEgK6VfjirYvGyPLOK+tZQz1jkYcmH5AjMamQ9PycnlxZF2aEZtPpNoUsaceX2bHptn6Ub5hGXqNvw=="], + + "@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="], + + "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.161.7", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ=="], + "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -449,10 +530,20 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], @@ -461,6 +552,8 @@ "@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.1", "@typescript-eslint/type-utils": "8.58.1", "@typescript-eslint/utils": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ=="], @@ -483,6 +576,10 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.1", "", { "dependencies": { "@typescript-eslint/types": "8.58.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + + "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], @@ -493,6 +590,8 @@ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "ai": ["ai@6.0.168", "", { "dependencies": { "@ai-sdk/gateway": "3.0.104", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2HqCJuO+1V2aV7vfYs5LFEUfxbkGX+5oa54q/gCCTL7KLTdbxcCu5D7TdLA5kwsrs3Szgjah9q6D9tpjHM3hUQ=="], + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -501,16 +600,30 @@ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], + "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="], + + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.10.16", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA=="], + "better-auth": ["better-auth@1.6.6", "", { "dependencies": { "@better-auth/core": "1.6.6", "@better-auth/drizzle-adapter": "1.6.6", "@better-auth/kysely-adapter": "1.6.6", "@better-auth/memory-adapter": "1.6.6", "@better-auth/mongo-adapter": "1.6.6", "@better-auth/prisma-adapter": "1.6.6", "@better-auth/telemetry": "1.6.6", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.5", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.14", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-w8Dtwz2oWzOT6xVrhvin62nn6NA8KMTKm7nOb9vokZuEj1kj70R918hH84ThcP/wTBa8KXXvx0WEsn+R6caGyQ=="], + + "better-call": ["better-call@1.3.5", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + "body-parser": ["body-parser@2.2.2", "", { "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" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], "brace-expansion": ["brace-expansion@1.1.13", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w=="], @@ -531,8 +644,20 @@ "caniuse-lite": ["caniuse-lite@1.0.30001787", "", {}, "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg=="], + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], @@ -551,6 +676,8 @@ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], @@ -563,6 +690,8 @@ "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], @@ -579,6 +708,8 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + "dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="], "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], @@ -591,12 +722,18 @@ "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], "dotenv": ["dotenv@17.4.1", "", {}, "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw=="], @@ -653,6 +790,8 @@ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], @@ -667,6 +806,8 @@ "express-rate-limit": ["express-rate-limit@8.3.2", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], @@ -727,10 +868,14 @@ "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], + "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], + "goober": ["goober@2.1.18", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], @@ -743,6 +888,10 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="], "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], @@ -751,6 +900,8 @@ "hono": ["hono@4.12.12", "", {}, "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q=="], + "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], @@ -767,12 +918,22 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], @@ -781,6 +942,8 @@ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], @@ -805,6 +968,8 @@ "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], + "isbot": ["isbot@5.1.39", "", {}, "sha512-obH0yYahGXdzNxo+djmHhBYThUKDkz565cxkIlt2L9hXfv1NlaLKoDBHo6KxXsYrIXx2RK3x5vY36CfZcobxEw=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], @@ -821,6 +986,8 @@ "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], @@ -835,6 +1002,8 @@ "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + "kysely": ["kysely@0.28.16", "", {}, "sha512-3i5pmOiZvMDj00qhrIVbH0AnioVTx22DMP7Vn5At4yJO46iy+FM8Y/g61ltenLVSo3fiO8h8Q3QOFgf/gQ72ww=="], + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], @@ -869,14 +1038,48 @@ "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "lucide-react": ["lucide-react@1.7.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], + + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], + + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], @@ -885,6 +1088,62 @@ "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], @@ -907,6 +1166,8 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "nanostores": ["nanostores@1.3.0", "", {}, "sha512-XPUa/jz+P1oJvN9VBxw4L9MtdFfaH3DAryqPssqhb2kXjmb9npz0dly6rCsgFWOPr4Yg9mTfM3MDZgZZ+7A3lA=="], + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], @@ -917,6 +1178,8 @@ "node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="], + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -945,6 +1208,8 @@ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], @@ -959,6 +1224,8 @@ "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], @@ -967,7 +1234,7 @@ "postcss": ["postcss@8.5.9", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw=="], - "postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + "postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], @@ -981,6 +1248,8 @@ "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], @@ -999,6 +1268,8 @@ "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], @@ -1007,14 +1278,26 @@ "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + + "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], + + "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], "rettime": ["rettime@0.10.1", "", {}, "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw=="], @@ -1023,6 +1306,8 @@ "rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="], + "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], @@ -1037,8 +1322,14 @@ "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + "seroval": ["seroval@1.5.2", "", {}, "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q=="], + + "seroval-plugins": ["seroval-plugins@1.5.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg=="], + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + "set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="], + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], "shadcn": ["shadcn@4.2.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-ZDuV340itidaUd4Gi1BxQX+Y7Ush6BHp6URZBM2RyxUUBZ6yFtOWIr4nVY+Ro+YRSpo82v7JrsmtcU5xoBCMJQ=="], @@ -1063,6 +1354,8 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], @@ -1071,6 +1364,8 @@ "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + "stringify-object": ["stringify-object@5.0.0", "", { "dependencies": { "get-own-enumerable-keys": "^1.0.0", "is-obj": "^3.0.0", "is-regexp": "^3.1.0" } }, "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg=="], "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], @@ -1081,8 +1376,14 @@ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], + + "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "swr": ["swr@2.4.1", "", { "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" } }, "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA=="], + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], @@ -1091,6 +1392,8 @@ "tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="], + "throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="], + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], @@ -1105,6 +1408,10 @@ "tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], "ts-morph": ["ts-morph@26.0.0", "", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="], @@ -1113,6 +1420,8 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], @@ -1129,10 +1438,24 @@ "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], + "until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="], "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], @@ -1151,10 +1474,16 @@ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + "vite": ["vite@7.3.2", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="], "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], @@ -1181,24 +1510,34 @@ "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], - "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + + "@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], "@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], "@dotenvx/dotenvx/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], + "@ecies/ciphers/@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], "@modelcontextprotocol/sdk/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + "@modelcontextprotocol/sdk/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "@noble/curves/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], @@ -1223,12 +1562,24 @@ "ajv-formats/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "better-auth/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "eciesjs/@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], + + "eciesjs/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "eslint-plugin-react-hooks/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -1237,19 +1588,25 @@ "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + "readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], "router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], - "shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "shadcn/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 642d635..6592c75 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,7 +21,7 @@ "better-auth": "^1.6.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "lucide-react": "^1.7.0", + "lucide-react": "^1.14.0", "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", @@ -137,6 +137,7 @@ "node_modules/@babel/core": { "version": "7.29.0", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -497,6 +498,7 @@ "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.6.2.tgz", "integrity": "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA==", "license": "MIT", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", @@ -612,6 +614,7 @@ "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.4.0.tgz", "integrity": "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA==", "license": "MIT", + "peer": true, "dependencies": { "@noble/hashes": "^2.0.1" } @@ -631,7 +634,8 @@ "node_modules/@better-fetch/fetch": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.21.tgz", - "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==" + "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==", + "peer": true }, "node_modules/@dotenvx/dotenvx": { "version": "1.60.2", @@ -1612,6 +1616,7 @@ "node_modules/@noble/ciphers": { "version": "1.3.0", "license": "MIT", + "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -1692,6 +1697,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -3807,6 +3813,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.168.10.tgz", "integrity": "sha512-/RmDlOwDkCug609KdPB3U+U1zmrtadJpvsmRg2zEn8TRCKRNri7dYZIjQZbNg8PgUiRL4T6njrZBV1ChzblNaA==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", @@ -3876,6 +3883,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.168.9.tgz", "integrity": "sha512-18oeEwEDyXOIuO1VBP9ACaK7tYHZUjynGDCoUh/5c/BNhia9vCJCp9O0LfhZXOorDc/PmLSgvmweFhVmIxF10g==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^2.0.0", @@ -4243,6 +4251,7 @@ "version": "24.12.2", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4250,6 +4259,7 @@ "node_modules/@types/react": { "version": "19.2.14", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4258,6 +4268,7 @@ "version": "19.2.3", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4315,6 +4326,7 @@ "version": "8.58.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.1", "@typescript-eslint/types": "8.58.1", @@ -4588,6 +4600,7 @@ "version": "8.16.0", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4932,6 +4945,7 @@ "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.3.5.tgz", "integrity": "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA==", "license": "MIT", + "peer": true, "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", @@ -5018,6 +5032,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -5461,7 +5476,8 @@ }, "node_modules/csstype": { "version": "3.2.3", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", @@ -5779,6 +5795,7 @@ "version": "9.39.4", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6558,6 +6575,7 @@ "node_modules/hono": { "version": "4.12.12", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -6910,6 +6928,7 @@ "node_modules/jose": { "version": "6.2.2", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/panva" } @@ -7007,6 +7026,7 @@ "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.15.tgz", "integrity": "sha512-r2clcf7HLWvDXaVUEvQymXJY4i3bSOIV3xsL/Upy3ZfSv5HeKsk9tsqbBptLvth5qHEIhxeHTA2jNLyQABkLBA==", "license": "MIT", + "peer": true, "engines": { "node": ">=20.0.0" } @@ -7343,7 +7363,9 @@ } }, "node_modules/lucide-react": { - "version": "1.7.0", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz", + "integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -8403,6 +8425,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": "^20.0.0 || >=22.0.0" } @@ -8819,6 +8842,7 @@ "version": "3.8.1", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -9095,6 +9119,7 @@ "node_modules/react": { "version": "19.2.4", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -9102,6 +9127,7 @@ "node_modules/react-dom": { "version": "19.2.4", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9528,6 +9554,7 @@ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.2.tgz", "integrity": "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" } @@ -9908,7 +9935,8 @@ }, "node_modules/tailwindcss": { "version": "4.2.2", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.2", @@ -10114,6 +10142,7 @@ "version": "5.9.3", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10414,6 +10443,7 @@ "node_modules/vite": { "version": "7.3.2", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -10695,6 +10725,7 @@ "node_modules/zod": { "version": "4.3.6", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/package.json b/frontend/package.json index abd4e0e..00e163a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,24 +17,24 @@ "@fontsource-variable/inter": "^5.2.8", "@fontsource-variable/jetbrains-mono": "^5.2.8", "@fontsource-variable/space-grotesk": "^5.2.10", + "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.2.1", "@tanstack/react-query": "^5.96.2", "@tanstack/react-router": "^1.168.10", "ai": "^6.0.153", + "better-auth": "^1.6.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "lucide-react": "^1.7.0", + "lucide-react": "^1.14.0", "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", - "@tailwindcss/typography": "^0.5.16", "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1", "shadcn": "^4.2.0", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.1", - "tw-animate-css": "^1.4.0", - "better-auth": "^1.6.1" + "tw-animate-css": "^1.4.0" }, "devDependencies": { "@eslint/js": "^9.39.4", diff --git a/frontend/public/brands/github-dark.svg b/frontend/public/brands/github-dark.svg new file mode 100644 index 0000000..c454d05 --- /dev/null +++ b/frontend/public/brands/github-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/brands/github-light.svg b/frontend/public/brands/github-light.svg new file mode 100644 index 0000000..a808c6c --- /dev/null +++ b/frontend/public/brands/github-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/brands/linear.svg b/frontend/public/brands/linear.svg new file mode 100644 index 0000000..99209b4 --- /dev/null +++ b/frontend/public/brands/linear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/brands/openrouter-dark.svg b/frontend/public/brands/openrouter-dark.svg new file mode 100644 index 0000000..61033bf --- /dev/null +++ b/frontend/public/brands/openrouter-dark.svg @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/frontend/public/brands/openrouter-light.svg b/frontend/public/brands/openrouter-light.svg new file mode 100644 index 0000000..4db31cd --- /dev/null +++ b/frontend/public/brands/openrouter-light.svg @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/frontend/public/brands/resend-dark.svg b/frontend/public/brands/resend-dark.svg new file mode 100644 index 0000000..4371f60 --- /dev/null +++ b/frontend/public/brands/resend-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/brands/resend-light.svg b/frontend/public/brands/resend-light.svg new file mode 100644 index 0000000..67af6bf --- /dev/null +++ b/frontend/public/brands/resend-light.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/brands/slack.svg b/frontend/public/brands/slack.svg new file mode 100644 index 0000000..6d14c72 --- /dev/null +++ b/frontend/public/brands/slack.svg @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/frontend/src/components/brand-icon.tsx b/frontend/src/components/brand-icon.tsx new file mode 100644 index 0000000..21aaeca --- /dev/null +++ b/frontend/src/components/brand-icon.tsx @@ -0,0 +1,54 @@ +/** + * BrandIcon — themed brand logos sourced from svgl.app. + * + * SVGs are downloaded once into `/public/brands/` so the app stays free of + * runtime CDN dependencies. Logos that look different on light vs dark + * backgrounds ship as two variants (`-light.svg` + `-dark.svg`) + * and the active theme picks the right one via `resolvedTheme`. Mono-tone + * logos (Linear, Slack) ship as a single file. + * + * lucide-react dropped brand icons on trademark grounds; svgl is the + * upstream source that the brand owners themselves point to, which makes + * it the right place to pull from for a few stable logos. + */ +import { useTheme } from "@/components/theme-provider" + +type Brand = "github" | "linear" | "slack" | "resend" | "openrouter" + +// Brands that ship a single mono-tone SVG (no theme swap). +const SINGLE_VARIANT: Record = { + github: false, + linear: true, + slack: true, + resend: false, + openrouter: false, +} + +const BRAND_LABEL: Record = { + github: "GitHub", + linear: "Linear", + slack: "Slack", + resend: "Resend", + openrouter: "OpenRouter", +} + +export function BrandIcon({ + name, + className, +}: { + name: Brand + className?: string +}) { + const { resolvedTheme } = useTheme() + const src = SINGLE_VARIANT[name] + ? `/brands/${name}.svg` + : `/brands/${name}-${resolvedTheme}.svg` + return ( + {`${BRAND_LABEL[name]} + ) +} diff --git a/frontend/src/components/confirm-dialog.tsx b/frontend/src/components/confirm-dialog.tsx new file mode 100644 index 0000000..2273acc --- /dev/null +++ b/frontend/src/components/confirm-dialog.tsx @@ -0,0 +1,156 @@ +import { useEffect, useRef, useState } from "react" +import { AlertTriangle, X } from "lucide-react" + +export interface ConfirmDialogProps { + open: boolean + title: string + description?: string + confirmLabel?: string + cancelLabel?: string + variant?: "default" | "destructive" + loading?: boolean + onConfirm: () => void + onCancel: () => void +} + +const ENTER_MS = 180 +const EXIT_MS = 140 + +/** + * Neumorphic confirm dialog — matches `/integrations` card aesthetic. + * + * Mount / unmount choreography: + * `open` true → mount → next frame add `visible` → fade + scale in + * `open` false → remove `visible` → wait EXIT_MS → unmount + * + * Drop-in replacement for `window.confirm()` with focus handling, ESC and + * backdrop-click to cancel. + */ +export function ConfirmDialog({ + open, + title, + description, + confirmLabel = "Confirm", + cancelLabel = "Cancel", + variant = "default", + loading = false, + onConfirm, + onCancel, +}: ConfirmDialogProps) { + const confirmRef = useRef(null) + const [mounted, setMounted] = useState(open) + const [visible, setVisible] = useState(false) + + // Sync `open` prop to the mount/visible state machine. + useEffect(() => { + if (open) { + setMounted(true) + // Defer one frame so the initial closed styles apply first and the + // browser has something to transition from. + const id = requestAnimationFrame(() => setVisible(true)) + return () => cancelAnimationFrame(id) + } + setVisible(false) + const id = window.setTimeout(() => setMounted(false), EXIT_MS) + return () => window.clearTimeout(id) + }, [open]) + + useEffect(() => { + if (!visible) return + const handler = (e: KeyboardEvent) => { + if (e.key === "Escape" && !loading) onCancel() + } + document.addEventListener("keydown", handler) + if (variant !== "destructive") { + confirmRef.current?.focus() + } + return () => document.removeEventListener("keydown", handler) + }, [visible, onCancel, variant, loading]) + + if (!mounted) return null + + const confirmClass = + variant === "destructive" + ? "bg-red-500/90 text-white hover:bg-red-500 shadow-neu-sm" + : "bg-primary text-primary-foreground hover:opacity-90 shadow-neu-sm" + + const backdropClass = visible + ? "opacity-100 backdrop-blur-sm" + : "opacity-0 backdrop-blur-0" + const panelClass = visible + ? "opacity-100 scale-100 translate-y-0" + : "opacity-0 scale-[0.96] translate-y-2" + + return ( +

{ + if (e.target === e.currentTarget && !loading) onCancel() + }} + > +
+ + +
+ {variant === "destructive" && ( +
+ +
+ )} +
+

+ {title} +

+ {description && ( +

+ {description} +

+ )} +
+
+ +
+ + +
+
+
+ ) +} diff --git a/frontend/src/components/picker.tsx b/frontend/src/components/picker.tsx new file mode 100644 index 0000000..62f9ea3 --- /dev/null +++ b/frontend/src/components/picker.tsx @@ -0,0 +1,92 @@ +import { useEffect, useRef, useState } from "react" +import { Check, ChevronDown } from "lucide-react" + +interface PickerProps { + items: T[] + value: string + getValue: (item: T) => string + getLabel: (item: T) => React.ReactNode + onChange: (value: string) => void + placeholder?: string +} + +// Generic single-select dropdown. Unstyled-select alternative to setSearchQuery(e.target.value)} - className="w-56 rounded-xl border border-border bg-background pl-9 pr-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/30" - /> + + + {/* Progress section */} +
+

Progress

+
+
+
{metrics.scope}
+
Scope
+
+
+
{startedPct}%
+
Started
+
+
+
{completedPct}%
+
Completed
+
- {/* Refresh */} - + {/* Simple progress bar */} +
+
+
- {/* Board */} -
- {isError && !isInitialLoad && ( -
-
- - - {isStale - ? "Linear API unreachable — showing last successful snapshot." - : "Linear API unreachable — unable to load tickets."} - -
+ {/* Tabs */} +
+
+ {tabs.map((tab) => ( + ))} +
+
+ + {/* Tab content */} +
+ {activeTab === 'assignees' && + assigneeBreakdown.map((item) => ( +
+ {item.name === 'Unassigned' ? ( +
+ +
) : ( - + )} - Retry - -
- )} - {isLoading ? ( -
- -
- ) : ( -
- {COLUMNS.map((col) => ( - {item.name} + 0 ? (item.total / totalIssues) * 100 : 0} size={18} strokeWidth={2} /> + + {totalIssues > 0 ? Math.round((item.total / totalIssues) * 100) : 0}% of {totalIssues} + +
+ ))} + + {activeTab === 'labels' && + labelBreakdown.map((item) => ( +
+ - ))} -
- )} + {item.name} + {item.count} +
+ ))} + + {activeTab === 'priority' && + priorityBreakdown.map((item) => ( +
+ {item.priority > 0 && item.priority <= 4 ? ( + + ) : ( +
+ )} + {item.label} + {item.count} +
+ ))} + + {activeTab === 'projects' && + projectBreakdown.map((item) => ( +
+ + {item.name} + {item.count} +
+ ))}
- ) + ); } -// --------------------------------------------------------------------------- -// Column -// --------------------------------------------------------------------------- +// ---------- Main Page ---------- -function KanbanColumn({ - title, - icon: Icon, - accent, - bg, - tickets, -}: { - title: string - icon: React.ElementType - accent: string - bg: string - tickets: BoardTicket[] -}) { - return ( -
- {/* Column header */} -
-
- +function BoardPage() { + const [currentProjectId] = useCurrentProjectId(); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + + const { data, isLoading, error } = useQuery({ + queryKey: ['linear-issues'], + queryFn: () => apiFetch('/linear/issues'), + staleTime: 300_000, // 5 minutes — data is cached on the server + enabled: !!currentProjectId, + }); + + const { data: cycleData } = useQuery({ + queryKey: ['linear-cycle-active'], + queryFn: () => apiFetch('/linear/cycle/active'), + staleTime: 300_000, + enabled: !!currentProjectId, + }); + + // Sync status query + const { data: syncStatus } = useQuery<{ lastSyncedAt: string | null; syncInProgress: boolean }>({ + queryKey: ['linear-sync-status'], + queryFn: () => apiFetch('/linear/sync/status'), + refetchInterval: 30_000, + enabled: !!currentProjectId, + }); + + // Manual sync mutation + const syncMutation = useMutation({ + mutationFn: () => apiFetch('/linear/sync', { method: 'POST' }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['linear-issues'] }); + queryClient.invalidateQueries({ queryKey: ['linear-sync-status'] }); + }, + }); + + const handleSync = useCallback(() => { + if (!syncMutation.isPending) { + syncMutation.mutate(); + } + }, [syncMutation]); + + // Trigger a sync on mount (session start) + useEffect(() => { + if (currentProjectId) { + apiFetch('/linear/sync', { method: 'POST' }) + .then(() => { + queryClient.invalidateQueries({ queryKey: ['linear-issues'] }); + queryClient.invalidateQueries({ queryKey: ['linear-sync-status'] }); + }) + .catch(() => { /* sync failure is non-fatal */ }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentProjectId]); + + const issues = data ?? {}; + const allIssues = useMemo(() => getAllIssues(issues), [issues]); + const totalCount = allIssues.length; + + // Build swimlane data: group issues by assignee, then by column + const swimlanes = useMemo(() => { + const byAssignee: Record> = {}; + + for (const [stateName, stateIssues] of Object.entries(issues)) { + for (const issue of stateIssues) { + const name = issue.assignee?.name ?? 'Unassigned'; + if (!byAssignee[name]) byAssignee[name] = {}; + if (!byAssignee[name][stateName]) byAssignee[name][stateName] = []; + byAssignee[name][stateName].push(issue); + } + } + + // Sort: assigned users alphabetically, Unassigned last + return Object.entries(byAssignee) + .sort(([a], [b]) => { + if (a === 'Unassigned') return 1; + if (b === 'Unassigned') return -1; + return a.localeCompare(b); + }) + .map(([name, issuesByColumn]) => { + const total = Object.values(issuesByColumn).flat().length; + return { name, issuesByColumn, total }; + }); + }, [issues]); + + // Count per column + const columnCounts = useMemo(() => { + const counts: Record = {}; + for (const col of COLUMNS) { + counts[col.key] = (issues[col.key] ?? []).length; + } + return counts; + }, [issues]); + + // Gate: require a project to be selected + if (!currentProjectId) { + return ( +
+
+
+ +
+

+ Select or create a project to view the board +

+

+ A project is required before you can use the board. +

+
-

- {title} -

- - {tickets.length} -
+ ); + } - {/* Cards */} -
- {tickets.length === 0 ? ( -
-
- + if (isLoading) { + return ( +
+ {/* Header skeleton */} +
+
+
+
+
+
+
+
+
+
+
+ {/* Body skeleton */} +
+ {/* Kanban skeleton */} +
+
+ {COLUMNS.map((col) => ( +
+
+
+
+
+
+ {[...Array(3)].map((_, i) => ( +
+
+
+
+
+
+
+
+
+
+
+
+ ))} +
+ ))}
-

No tickets

- ) : ( - tickets.map((ticket) => ( - - )) - )} + {/* Sidebar skeleton */} +
+
+
+
+
+ {[...Array(3)].map((_, i) => ( +
+
+
+
+ ))} +
+
+
-
- ) -} - -// --------------------------------------------------------------------------- -// Ticket card -// --------------------------------------------------------------------------- - -function TicketCard({ ticket }: { ticket: BoardTicket }) { - // Guard createdAt: the backend may serialize it as undefined or a raw - // unparseable string, and Intl.DateTimeFormat throws RangeError on an - // Invalid Date in some locales. Omit the chip entirely when unparseable. - const parsed = ticket.createdAt ? new Date(ticket.createdAt) : null - const formattedDate = - parsed && Number.isFinite(parsed.getTime()) - ? new Intl.DateTimeFormat("en-US", { - month: "short", - day: "numeric", - }).format(parsed) - : null + ); + } - return ( - - {/* Top row: identifier + severity */} -
- - {ticket.identifier} - - + if (error) { + return ( +
+
Failed to load board: {error.message}
+ ); + } - {/* Title */} -

- {ticket.title} -

- - {/* Bottom row: assignee + date */} -
- {ticket.assignee ? ( -
-
- {ticket.assignee.initials} -
+ return ( +
+ {/* Top header */} +
+
+ + triage-hackathon + / + {cycleData?.name ?? 'Board'} + {totalCount} Issues +
+
+ {syncStatus?.lastSyncedAt && ( - {ticket.assignee.name} + Last synced: {formatTimeAgo(syncStatus.lastSyncedAt)} -
- ) : ( -
-
- ? -
- - Unassigned - -
- )} - {formattedDate && ( -
- - {formattedDate} -
- )} + )} + + + +
- {/* External link indicator */} - {ticket.url !== "#" && ( -
+ ); } diff --git a/frontend/src/routes/chat.tsx b/frontend/src/routes/chat.tsx index 8c7cc2e..95805b0 100644 --- a/frontend/src/routes/chat.tsx +++ b/frontend/src/routes/chat.tsx @@ -1,14 +1,17 @@ -import { createFileRoute } from "@tanstack/react-router" +import { createFileRoute, useNavigate } from "@tanstack/react-router" import { useChat } from "@ai-sdk/react" import { DefaultChatTransport } from "ai" import { useRef, useState, useCallback, useEffect, useMemo } from "react" -import { Send, Paperclip, X, ZoomIn, FileText, FileCode, File as FileIcon, Brain, ChevronDown } from "lucide-react" +import { Send, Paperclip, X, ZoomIn, FileText, FileCode, File as FileIcon, Brain, ChevronDown, FolderGit2 } from "lucide-react" import { Button } from "@/components/ui/button" import { toolComponents, DuplicatePrompt } from "@/components/tool-registry" import { TriageCard } from "@/components/triage-card" import { getDraft, saveDraft, clearDraft } from "@/lib/chat-draft" import { useAuth } from "@/hooks/use-auth" import { useConversations } from "@/hooks/use-conversations" +import { useCurrentProjectId } from "@/components/project-selector" +import { apiFetch } from "@/lib/api" +import { getConfig } from "@/lib/config" import Markdown from "react-markdown" import remarkGfm from "remark-gfm" @@ -35,16 +38,32 @@ interface Attachment { } function ChatPage() { - const { user } = useAuth() + const { user, isLoading: authLoading } = useAuth() + const [currentProjectId] = useCurrentProjectId() + const navigate = useNavigate() + const { activeThreadId, updateTitle, ensureConversation, refreshConversations } = useConversations() const threadIdRef = useRef(activeThreadId) threadIdRef.current = activeThreadId + const projectIdRef = useRef(currentProjectId) + projectIdRef.current = currentProjectId + const transport = useMemo(() => new DefaultChatTransport({ api: "/chat", credentials: "include", + // Server middleware reads `x-project-id` and sets it on Mastra's + // requestContext so the orchestrator's dynamic instructions + tools + // can scope to the active project. Keep it in the body too for + // backward compat with any code path that still inspects request JSON. + headers: () => { + const h: Record = {} + if (projectIdRef.current) h["x-project-id"] = projectIdRef.current + return h + }, body: () => ({ + projectId: projectIdRef.current, memory: { thread: threadIdRef.current, resource: user?.id ?? "anonymous", @@ -57,17 +76,37 @@ function ChatPage() { id: activeThreadId, }) - // Load messages from server when switching threads + // Load messages from server when mounting or switching threads. + // We intentionally refetch on every mount (not just when activeThreadId + // changes) so navigating away (/board) and back restores the conversation + // instead of showing an empty chat until the user manually switches. + // Streaming protection: skip refetch only while useChat is actively streaming. const prevThreadRef = useRef(null) useEffect(() => { - // Skip if we're staying on the same thread (avoids re-fetch during streaming) - if (prevThreadRef.current === activeThreadId) return + if (prevThreadRef.current === activeThreadId && (status === 'streaming' || status === 'submitted')) return prevThreadRef.current = activeThreadId const controller = new AbortController() - fetch(`/api/memory/threads/${activeThreadId}/messages?agentId=orchestrator`, { credentials: "include", signal: controller.signal }) - .then((res) => (res.ok ? res.json() : null)) - .then((data: { messages?: Array> } | null) => { + + // Fetch messages and persisted card states in parallel + const messagesP = fetch(`/memory/threads/${activeThreadId}/messages?agentId=orchestrator`, { credentials: "include", signal: controller.signal }) + .then((res) => (res.ok ? res.json() : null)) as Promise<{ messages?: Array> } | null> + + const cardStatesP = apiFetch<{ cardStates: Record }>(`/memory/card-states/${activeThreadId}`) + .catch(() => ({ cardStates: {} as Record })) + + Promise.all([messagesP, cardStatesP]).then(([data, persistedCards]) => { + // Restore persisted card states into React state + if (persistedCards.cardStates && Object.keys(persistedCards.cardStates).length > 0) { + const restored: Record = {} + for (const [key, val] of Object.entries(persistedCards.cardStates)) { + restored[key] = { state: val.state as "confirmed" | "error" } + } + setCardStates(restored) + } else { + setCardStates({}) + } + if (!data?.messages?.length) { // New thread with no messages — clear any stale UI messages setMessages([]) @@ -121,6 +160,7 @@ function ChatPage() { }) .catch(() => { /* thread doesn't exist yet — that's fine */ }) return () => controller.abort() + // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeThreadId, setMessages]) // Auto-title from first user message + refresh thread list after response completes @@ -306,23 +346,228 @@ function ChatPage() { ) // ---- Ticket actions ---- - // When user clicks "Create Ticket" on the triage card, send a message - // to the agent so it creates the Linear issue with full context. - const handleCreateTicket = (_triageData: Record, cardKey: string) => { - setCardStates((prev) => ({ ...prev, [cardKey]: { state: "confirmed" } })) - sendMessage({ text: "Confirmed. Create the Linear ticket with the details above." }) + // When user clicks "Create Ticket" on the triage card, stream workflow progress via SSE + // into a single WorkflowTimeline component that updates in-place as events arrive. + const handleCreateTicket = async (triageData: Record, cardKey: string) => { + const reporterEmail = user?.email + if (!reporterEmail) return + setCardStates((prev) => ({ ...prev, [cardKey]: { state: 'submitting' } })) + + // Steps accumulated in one assistant message rendered as WorkflowTimeline. + type TStep = { step: string; status: 'running' | 'completed' | 'error' | 'suspended'; message?: string; data?: Record } + const timelineMessageId = `workflow-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` + const collectedSteps: TStep[] = [] + let active = true + + const renderTimeline = () => { + const timelinePart = { + type: 'tool-workflowTimeline', + state: 'output-available', + output: { steps: [...collectedSteps], active }, + } + setMessages((prev) => { + const arr = prev as unknown as Array<{ id: string; role: string; parts: unknown[]; createdAt?: Date }> + const existingIdx = arr.findIndex((m) => m.id === timelineMessageId) + if (existingIdx >= 0) { + const next = arr.slice() + next[existingIdx] = { ...arr[existingIdx], parts: [timelinePart] } + return next as any + } + return [ + ...arr, + { id: timelineMessageId, role: 'assistant', parts: [timelinePart], createdAt: new Date() }, + ] as any + }) + } + + const addStep = (s: TStep) => { + // Replace any 'running' entry for the same stepId instead of duplicating + const existingRunningIdx = collectedSteps.findIndex((x) => x.step === s.step && x.status === 'running') + if (existingRunningIdx >= 0 && s.status !== 'running') { + collectedSteps[existingRunningIdx] = s + } else if (!collectedSteps.some((x) => x.step === s.step && x.status === s.status)) { + collectedSteps.push(s) + } + renderTimeline() + } + + // Seed with an initial running step so the timeline shows immediately + addStep({ step: 'intake', status: 'running', message: 'Analyzing incident' }) + + let linearUrl = '' + + try { + const config = await getConfig() + const response = await fetch(`${config.apiUrl}/workflows/triage-workflow/stream`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ + description: triageData.summary ?? '', + reporterEmail, + repository: 'Agentic-Engineering-Agency/triage', + threadId: activeThreadId, + assigneeId: triageData.assigneeId, + assigneeEmail: triageData.assigneeEmail, + assigneeName: triageData.assigneeName, + cycleId: triageData.cycleId, + dueDate: triageData.dueDate, + }), + }) + + if (!response.ok || !response.body) { + throw new Error(`Stream request failed: ${response.status}`) + } + + const reader = response.body.getReader() + const decoder = new TextDecoder() + let buffer = '' + + while (true) { + const { done, value } = await reader.read() + if (done) break + + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split('\n') + buffer = lines.pop() ?? '' + + for (const line of lines) { + if (!line.startsWith('data: ')) continue + try { + const event = JSON.parse(line.slice(6)) as TStep & { step: string } + + // Track ticket metadata for persistence & link rendering + if (event.data?.issueUrl) linearUrl = event.data.issueUrl as string + + // Skip the "done" synthetic event — it just marks stream end + if (event.step === 'done') continue + + addStep(event) + } catch { + // Ignore malformed SSE lines + } + } + } + + active = false + renderTimeline() + + setCardStates((prev) => ({ ...prev, [cardKey]: { state: 'confirmed' } })) + + // Persist card state so it survives page reload + const [messageId, toolIndexStr] = cardKey.split(/-(?=[^-]*$)/) + apiFetch('/memory/card-state', { + method: 'POST', + body: JSON.stringify({ + threadId: activeThreadId, + messageId, + toolIndex: Number(toolIndexStr), + state: 'confirmed', + linearUrl: linearUrl || ((triageData.linearUrl as string) ?? undefined), + }), + }).catch((err) => console.error('[chat] Failed to persist card state:', err)) + + // Persist the WorkflowTimeline tool invocation so reloads reconstruct + // the same visual. Stored as a tool-invocation part in the Mastra + // message store; the thread-load effect converts it back to tool-*. + apiFetch('/memory/save-message', { + method: 'POST', + body: JSON.stringify({ + threadId: activeThreadId, + role: 'assistant', + parts: [ + { + type: 'tool-invocation', + toolInvocation: { + toolName: 'workflowTimeline', + state: 'result', + result: { steps: collectedSteps, active: false }, + }, + }, + ], + }), + }).catch((err) => console.error('[chat] Failed to persist workflow timeline:', err)) + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to create ticket' + active = false + addStep({ step: 'error', status: 'error', message }) + setCardStates((prev) => ({ ...prev, [cardKey]: { state: 'error', errorMessage: message } })) + } } - const handleUpdateExisting = (dupData: Record) => { - sendMessage({ text: `Update the existing ticket: ${dupData.existingTicketTitle ?? ""}` }) + const handleUpdateExisting = async (dupData: Record) => { + const reporterEmail = user?.email + if (!reporterEmail) return + try { + await apiFetch('/workflows/triage-workflow/trigger', { + method: 'POST', + body: JSON.stringify({ + description: `Update existing ticket: ${dupData.existingTicketTitle ?? ''}`, + reporterEmail, + repository: 'Agentic-Engineering-Agency/triage', + }), + }) + console.log('[chat] Update existing triggered') + } catch (error) { + console.error('[chat] Update existing failed:', error) + } } - const handleCreateNew = (_dupData: Record) => { - sendMessage({ text: "Ignore the duplicate, create a new ticket with the triage details above." }) + const handleCreateNew = async (_dupData: Record) => { + const reporterEmail = user?.email + if (!reporterEmail) return + try { + await apiFetch('/workflows/triage-workflow/trigger', { + method: 'POST', + body: JSON.stringify({ + description: _dupData.summary ?? '', + reporterEmail, + repository: 'Agentic-Engineering-Agency/triage', + }), + }) + console.log('[chat] Create new triggered') + } catch (error) { + console.error('[chat] Create new failed:', error) + } } const hasMessages = messages.length > 0 + // Guard: don't render chat UI until auth is verified. + if (authLoading) { + return ( +
+
+
+ ) + } + + // Gate: require a project to be selected. + if (!currentProjectId) { + return ( +
+
+
+ +
+

+ Select or create a project to start triaging +

+

+ A project is required before you can use the chat. +

+ +
+
+ ) + } + return (
{/* Messages area — scrollable, centered */} diff --git a/frontend/src/routes/integrations.lazy.tsx b/frontend/src/routes/integrations.lazy.tsx new file mode 100644 index 0000000..2e77553 --- /dev/null +++ b/frontend/src/routes/integrations.lazy.tsx @@ -0,0 +1,1378 @@ +import { createLazyFileRoute } from "@tanstack/react-router" +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { useState } from "react" +import { + KeyRound, + CheckCircle2, + AlertCircle, + Loader2, + Trash2, + FolderGit2, +} from "lucide-react" +import { apiFetch } from "@/lib/api" +import { useCurrentProjectId } from "@/components/project-selector" +import { ConfirmDialog } from "@/components/confirm-dialog" +import { Picker } from "@/components/picker" +import { BrandIcon } from "@/components/brand-icon" + +export const Route = createLazyFileRoute("/integrations")({ + component: IntegrationsPage, +}) + +type Provider = "openrouter" | "linear" | "resend" | "slack" | "github" +type Status = "active" | "disabled" | "invalid" + +interface IntegrationSummary { + provider: Provider + status: Status + meta: Record + lastTestedAt: string | null + createdAt: string + updatedAt: string +} + +interface LinearTeam { + id: string + name: string + key: string +} +interface SlackChannel { + id: string + name: string + isPrivate: boolean +} + +interface TestPreview { + teams?: LinearTeam[] + channels?: SlackChannel[] +} + +type TestResponse = + | { + valid: true + integration: IntegrationSummary + } + | { + valid: true + preview: TestPreview + } + | { + valid: false + reason: "invalid_key" | "network" | "not_implemented" + message?: string + } + +// Brand icons (GitHub, Linear, Slack, Resend, OpenRouter) now route through +// `BrandIcon` which serves themed SVGs sourced from svgl.app and shipped from +// `/public/brands/`. Lucide-react dropped brand logos on trademark grounds. + +function IntegrationsPage() { + const [currentProjectId] = useCurrentProjectId() + + if (!currentProjectId) { + return ( +
+
+
+ +
+

+ Select a project +

+

+ Integrations are per-project. Choose one from the sidebar to + configure its keys. +

+
+
+ ) + } + + return +} + +function IntegrationsContent({ projectId }: { projectId: string }) { + const { data: integrations, isLoading } = useQuery({ + queryKey: ["integrations", projectId], + queryFn: () => + apiFetch(`/projects/${projectId}/integrations`), + }) + + const byProvider: Partial> = {} + for (const row of integrations ?? []) byProvider[row.provider] = row + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

+ Integrations +

+

+ Per-project API keys. Stored encrypted at rest. +

+
+
+
+ + {/* Content */} +
+ {isLoading && ( +
+ +
+ )} + + {!isLoading && ( + <> + + + + + + + + + + + + + + + + + + )} +
+
+ ) +} + +function DomainSection({ + title, + description, + children, +}: { + title: string + description: string + children: React.ReactNode +}) { + return ( +
+
+

+ {title} +

+

{description}

+
+
{children}
+
+ ) +} + +function StatusBadge({ status }: { status: Status | "not-configured" }) { + if (status === "active") { + return ( + + + Active + + ) + } + if (status === "invalid") { + return ( + + + Invalid + + ) + } + if (status === "disabled") { + return ( + + Disabled + + ) + } + return ( + + Not configured + + ) +} + +function OpenRouterCard({ + projectId, + summary, +}: { + projectId: string + summary: IntegrationSummary | undefined +}) { + const queryClient = useQueryClient() + const [apiKey, setApiKey] = useState("") + const [editing, setEditing] = useState(false) + const [testError, setTestError] = useState(null) + const [testSuccess, setTestSuccess] = useState(false) + const [deleteOpen, setDeleteOpen] = useState(false) + + const testMutation = useMutation({ + mutationFn: async (key: string) => { + const res = await apiFetch( + `/projects/${projectId}/integrations/openrouter/test`, + { + method: "POST", + body: JSON.stringify({ apiKey: key }), + }, + ) + return res + }, + onSuccess: (res) => { + if (res.valid) { + setTestSuccess(true) + setTestError(null) + setApiKey("") + setEditing(false) + queryClient.invalidateQueries({ queryKey: ["integrations", projectId] }) + setTimeout(() => setTestSuccess(false), 2500) + } else { + setTestSuccess(false) + setTestError(reasonToMessage(res, "OpenRouter")) + } + }, + onError: (err: Error) => { + setTestError(err.message) + setTestSuccess(false) + }, + }) + + const deleteMutation = useMutation({ + mutationFn: () => + apiFetch(`/projects/${projectId}/integrations/openrouter`, { + method: "DELETE", + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["integrations", projectId] }) + setApiKey("") + setEditing(false) + setTestError(null) + setDeleteOpen(false) + }, + }) + + const configured = !!summary + const status: Status | "not-configured" = configured ? summary!.status : "not-configured" + const showInput = editing || !configured + + return ( +
+
+
+
+ +
+
+

OpenRouter

+

+ Key for all LLM agents and wiki embeddings. +

+
+
+ +
+ + {showInput ? ( +
+ { + setApiKey(e.target.value) + setTestError(null) + setTestSuccess(false) + }} + placeholder="sk-or-..." + className="w-full rounded-xl border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/30 font-mono" + /> +
+ + {configured && ( + + )} +
+
+ ) : ( +
+
+ + ●●●●●●●●●●●●●●●● + + + +
+ {summary?.lastTestedAt && ( +

+ Tested {formatRelative(summary.lastTestedAt)} +

+ )} +
+ )} + + {testError && ( +

{testError}

+ )} + {testSuccess && ( +

+ ✓ Key validated and saved. +

+ )} + + setDeleteOpen(false)} + onConfirm={() => deleteMutation.mutate()} + /> +
+ ) +} + +function LinearCard({ + projectId, + summary, +}: { + projectId: string + summary: IntegrationSummary | undefined +}) { + const queryClient = useQueryClient() + const [apiKey, setApiKey] = useState("") + const [teams, setTeams] = useState([]) + const [selectedTeamId, setSelectedTeamId] = useState("") + const [editing, setEditing] = useState(false) + const [testError, setTestError] = useState(null) + const [saveSuccess, setSaveSuccess] = useState(false) + const [deleteOpen, setDeleteOpen] = useState(false) + + const testMutation = useMutation({ + mutationFn: async (key: string) => { + const res = await apiFetch( + `/projects/${projectId}/integrations/linear/test`, + { method: "POST", body: JSON.stringify({ apiKey: key }) }, + ) + return res + }, + onSuccess: (res) => { + if (res.valid && "preview" in res) { + setTestError(null) + setTeams(res.preview.teams ?? []) + setSelectedTeamId(res.preview.teams?.[0]?.id ?? "") + } else if (res.valid) { + // Backend returned a persisted integration without preview — shouldn't + // happen for Linear but handle gracefully. + setTestError(null) + queryClient.invalidateQueries({ queryKey: ["integrations", projectId] }) + } else { + setTeams([]) + setSelectedTeamId("") + setTestError(reasonToMessage(res, "Linear")) + } + }, + onError: (err: Error) => { + setTestError(err.message) + }, + }) + + const saveMutation = useMutation({ + mutationFn: async () => { + const team = teams.find((t) => t.id === selectedTeamId) + if (!team) throw new Error("Select a team before saving") + return apiFetch( + `/projects/${projectId}/integrations/linear`, + { + method: "PUT", + body: JSON.stringify({ + apiKey, + meta: { teamId: team.id, teamName: team.name, teamKey: team.key }, + }), + }, + ) + }, + onSuccess: () => { + setSaveSuccess(true) + setApiKey("") + setTeams([]) + setSelectedTeamId("") + setEditing(false) + queryClient.invalidateQueries({ queryKey: ["integrations", projectId] }) + setTimeout(() => setSaveSuccess(false), 2500) + }, + onError: (err: Error) => { + setTestError(err.message) + }, + }) + + const deleteMutation = useMutation({ + mutationFn: () => + apiFetch(`/projects/${projectId}/integrations/linear`, { method: "DELETE" }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["integrations", projectId] }) + setApiKey("") + setTeams([]) + setSelectedTeamId("") + setEditing(false) + setTestError(null) + setDeleteOpen(false) + }, + }) + + const configured = !!summary + const status: Status | "not-configured" = configured ? summary!.status : "not-configured" + const showInput = editing || !configured + + return ( +
+
+
+
+ +
+
+

Linear

+

+ Personal API token + team for ticket creation. +

+
+
+ +
+ + {showInput ? ( +
+ { + setApiKey(e.target.value) + setTestError(null) + setTeams([]) + setSelectedTeamId("") + }} + placeholder="lin_api_..." + className="w-full rounded-xl border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/30 font-mono" + /> + + {teams.length > 0 && ( +
+ + t.id} + getLabel={(t) => ( + <> + {t.name}{" "} + ({t.key}) + + )} + onChange={setSelectedTeamId} + placeholder="Select a team" + /> +
+ )} + +
+ {teams.length === 0 ? ( + + ) : ( + + )} + {configured && ( + + )} +
+
+ ) : ( +
+
+ + ●●●●●●●●●●●●●●●● + + + +
+ {summary?.meta.teamName && ( +

+ Team: {summary.meta.teamName} + {summary.meta.teamKey && ( + + ({summary.meta.teamKey}) + + )} +

+ )} + {summary?.lastTestedAt && ( +

+ Tested {formatRelative(summary.lastTestedAt)} +

+ )} +
+ )} + + {testError && ( +

{testError}

+ )} + {saveSuccess && ( +

+ ✓ Linear connected. +

+ )} + + setDeleteOpen(false)} + onConfirm={() => deleteMutation.mutate()} + /> +
+ ) +} + +function SlackCard({ + projectId, + summary, +}: { + projectId: string + summary: IntegrationSummary | undefined +}) { + const queryClient = useQueryClient() + const [apiKey, setApiKey] = useState("") + const [channels, setChannels] = useState([]) + const [selectedChannelId, setSelectedChannelId] = useState("") + const [manualChannelId, setManualChannelId] = useState("") + // `tokenValidated` separates "token is good" from "enumeration succeeded". + // A bot token with only chat:write authenticates fine but can't list channels + // — we still let the user save by typing the channel ID manually. + const [tokenValidated, setTokenValidated] = useState(false) + const [editing, setEditing] = useState(false) + const [testError, setTestError] = useState(null) + const [saveSuccess, setSaveSuccess] = useState(false) + const [deleteOpen, setDeleteOpen] = useState(false) + + const testMutation = useMutation({ + mutationFn: async (key: string) => + apiFetch( + `/projects/${projectId}/integrations/slack/test`, + { method: "POST", body: JSON.stringify({ apiKey: key }) }, + ), + onSuccess: (res) => { + if (res.valid && "preview" in res) { + setTestError(null) + setTokenValidated(true) + setChannels(res.preview.channels ?? []) + setSelectedChannelId(res.preview.channels?.[0]?.id ?? "") + } else if (!res.valid) { + setTokenValidated(false) + setChannels([]) + setSelectedChannelId("") + setTestError(reasonToMessage(res, "Slack")) + } + }, + onError: (err: Error) => setTestError(err.message), + }) + + const saveMutation = useMutation({ + mutationFn: async () => { + const picked = channels.find((c) => c.id === selectedChannelId) + const channelId = picked?.id ?? manualChannelId.trim() + if (!channelId) throw new Error("Select or enter a channel before saving") + const meta: Record = { channelId } + if (picked) meta.channelName = picked.name + return apiFetch( + `/projects/${projectId}/integrations/slack`, + { method: "PUT", body: JSON.stringify({ apiKey, meta }) }, + ) + }, + onSuccess: () => { + setSaveSuccess(true) + setApiKey("") + setChannels([]) + setSelectedChannelId("") + setManualChannelId("") + setTokenValidated(false) + setEditing(false) + queryClient.invalidateQueries({ queryKey: ["integrations", projectId] }) + setTimeout(() => setSaveSuccess(false), 2500) + }, + onError: (err: Error) => setTestError(err.message), + }) + + const deleteMutation = useMutation({ + mutationFn: () => + apiFetch(`/projects/${projectId}/integrations/slack`, { method: "DELETE" }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["integrations", projectId] }) + setApiKey("") + setChannels([]) + setSelectedChannelId("") + setManualChannelId("") + setTokenValidated(false) + setEditing(false) + setTestError(null) + setDeleteOpen(false) + }, + }) + + const configured = !!summary + const status: Status | "not-configured" = configured ? summary!.status : "not-configured" + const showInput = editing || !configured + + return ( +
+
+
+
+ +
+
+

Slack

+

+ Bot token + channel for triage notifications. +

+
+
+ +
+ + {showInput ? ( +
+ { + setApiKey(e.target.value) + setTestError(null) + setTokenValidated(false) + setChannels([]) + setSelectedChannelId("") + setManualChannelId("") + }} + placeholder="xoxb-..." + className="w-full rounded-xl border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/30 font-mono" + /> + + {tokenValidated && channels.length > 0 && ( +
+ + c.id} + getLabel={(c) => ( + <> + {c.isPrivate ? "🔒 " : "#"} + {c.name} + + )} + onChange={setSelectedChannelId} + placeholder="Select a channel" + /> +
+ )} + + {tokenValidated && channels.length === 0 && ( +
+ + setManualChannelId(e.target.value)} + placeholder="C01234ABCDE" + className="w-full rounded-xl border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/30 font-mono" + /> +

+ Token authenticated but lacks channels:read/groups:read. + Paste the channel ID manually (right-click channel → Copy link). +

+
+ )} + +
+ {!tokenValidated ? ( + + ) : ( + + )} + {configured && ( + + )} +
+
+ ) : ( +
+
+ + ●●●●●●●●●●●●●●●● + + + +
+ {(summary?.meta.channelName || summary?.meta.channelId) && ( +

+ Channel:{" "} + + {summary.meta.channelName + ? `#${summary.meta.channelName}` + : summary.meta.channelId} + +

+ )} + {summary?.lastTestedAt && ( +

+ Tested {formatRelative(summary.lastTestedAt)} +

+ )} +
+ )} + + {testError && ( +

{testError}

+ )} + {saveSuccess && ( +

+ ✓ Slack connected. +

+ )} + + setDeleteOpen(false)} + onConfirm={() => deleteMutation.mutate()} + /> +
+ ) +} + +function ResendCard({ + projectId, + summary, +}: { + projectId: string + summary: IntegrationSummary | undefined +}) { + const queryClient = useQueryClient() + const [apiKey, setApiKey] = useState("") + const [fromEmail, setFromEmail] = useState("") + const [editing, setEditing] = useState(false) + const [testError, setTestError] = useState(null) + const [testSuccess, setTestSuccess] = useState(false) + const [deleteOpen, setDeleteOpen] = useState(false) + + const isValidEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(fromEmail) + + const testMutation = useMutation({ + mutationFn: async () => + apiFetch( + `/projects/${projectId}/integrations/resend/test`, + { + method: "POST", + body: JSON.stringify({ apiKey, meta: { fromEmail } }), + }, + ), + onSuccess: (res) => { + if (res.valid) { + setTestSuccess(true) + setTestError(null) + setApiKey("") + setFromEmail("") + setEditing(false) + queryClient.invalidateQueries({ queryKey: ["integrations", projectId] }) + setTimeout(() => setTestSuccess(false), 2500) + } else { + setTestSuccess(false) + setTestError(reasonToMessage(res, "Resend")) + } + }, + onError: (err: Error) => { + setTestError(err.message) + setTestSuccess(false) + }, + }) + + const deleteMutation = useMutation({ + mutationFn: () => + apiFetch(`/projects/${projectId}/integrations/resend`, { method: "DELETE" }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["integrations", projectId] }) + setApiKey("") + setFromEmail("") + setEditing(false) + setTestError(null) + setDeleteOpen(false) + }, + }) + + const configured = !!summary + const status: Status | "not-configured" = configured ? summary!.status : "not-configured" + const showInput = editing || !configured + + return ( +
+
+
+
+ +
+
+

Resend

+

+ API key + verified from address for outbound email. +

+
+
+ +
+ + {showInput ? ( +
+ { + setApiKey(e.target.value) + setTestError(null) + }} + placeholder="re_..." + className="w-full rounded-xl border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/30 font-mono" + /> + { + setFromEmail(e.target.value) + setTestError(null) + }} + placeholder="triage@yourdomain.com" + className="w-full rounded-xl border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/30" + /> +

+ Must be an address on a domain verified in your Resend account. +

+ +
+ + {configured && ( + + )} +
+
+ ) : ( +
+
+ + ●●●●●●●●●●●●●●●● + + + +
+ {summary?.meta.fromEmail && ( +

+ From:{" "} + {summary.meta.fromEmail} +

+ )} + {summary?.lastTestedAt && ( +

+ Tested {formatRelative(summary.lastTestedAt)} +

+ )} +
+ )} + + {testError && ( +

{testError}

+ )} + {testSuccess && ( +

+ ✓ Resend connected. +

+ )} + + setDeleteOpen(false)} + onConfirm={() => deleteMutation.mutate()} + /> +
+ ) +} + +interface ProjectForGithub { + id: string + name: string + repositoryUrl: string + status: string +} + +/** + * Simple github.com URL parser — mirrors the backend helper so the card can + * pre-empt a "project repo isn't on GitHub" error and render a friendlier + * disabled state up-front. Backend still enforces authoritatively. + */ +function parseGithubRepo(url: string): { owner: string; repo: string; full: string } | null { + if (!url) return null + const m = + /^(?:https?|ssh):\/\/(?:[^@]+@)?github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/i.exec(url.trim()) ?? + /^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?\/?$/i.exec(url.trim()) + if (!m) return null + return { owner: m[1], repo: m[2], full: `${m[1]}/${m[2]}` } +} + +function GitHubCard({ + projectId, + summary, +}: { + projectId: string + summary: IntegrationSummary | undefined +}) { + const queryClient = useQueryClient() + const [apiKey, setApiKey] = useState("") + const [editing, setEditing] = useState(false) + const [verifyError, setVerifyError] = useState(null) + const [saveSuccess, setSaveSuccess] = useState(false) + const [retrySuccess, setRetrySuccess] = useState(false) + const [deleteOpen, setDeleteOpen] = useState(false) + + // The card needs the project's repo URL to render "Verify access to X". + // `/projects/:id` is a cheap ownership-checked read. + const { data: project } = useQuery({ + queryKey: ["project", projectId], + queryFn: () => apiFetch(`/projects/${projectId}`), + }) + + const parsed = project ? parseGithubRepo(project.repositoryUrl) : null + const repoFullName = summary?.meta.repoFullName ?? parsed?.full ?? null + + const verifyMutation = useMutation({ + mutationFn: async () => + apiFetch( + `/projects/${projectId}/integrations/github`, + { method: "PUT", body: JSON.stringify({ apiKey }) }, + ), + onSuccess: () => { + setSaveSuccess(true) + setApiKey("") + setEditing(false) + setVerifyError(null) + queryClient.invalidateQueries({ queryKey: ["integrations", projectId] }) + // Project status may have flipped needs_auth → pending; refresh so the + // UI picks that up without a tab switch. + queryClient.invalidateQueries({ queryKey: ["projects"] }) + queryClient.invalidateQueries({ queryKey: ["project", projectId] }) + setTimeout(() => setSaveSuccess(false), 2500) + }, + onError: (err: Error) => setVerifyError(err.message), + }) + + const retryMutation = useMutation({ + mutationFn: () => + apiFetch(`/projects/${projectId}/wiki/generate`, { method: "POST" }), + onSuccess: () => { + setRetrySuccess(true) + queryClient.invalidateQueries({ queryKey: ["projects"] }) + queryClient.invalidateQueries({ queryKey: ["project", projectId] }) + setTimeout(() => setRetrySuccess(false), 2500) + }, + onError: (err: Error) => setVerifyError(err.message), + }) + + const deleteMutation = useMutation({ + mutationFn: () => + apiFetch(`/projects/${projectId}/integrations/github`, { method: "DELETE" }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["integrations", projectId] }) + setApiKey("") + setEditing(false) + setVerifyError(null) + setDeleteOpen(false) + }, + }) + + const configured = !!summary + const status: Status | "not-configured" = configured + ? summary!.status + : "not-configured" + const showInput = editing || !configured + + // Project repo isn't on GitHub — don't even offer the PAT input. Mirrors + // the backend PROJECT_REPO_NOT_GITHUB rejection but avoids a round-trip. + const projectIsNonGithub = project !== undefined && parsed === null && Boolean(project.repositoryUrl) + + return ( +
+
+
+
+ +
+
+

GitHub

+

+ Personal access token (repo scope) for private-repo wiki + evidence lookups. +

+
+
+ +
+ + {projectIsNonGithub ? ( +

+ This project's repository isn't hosted on GitHub, so no token is needed. +

+ ) : showInput ? ( +
+ { + setApiKey(e.target.value) + setVerifyError(null) + }} + placeholder="ghp_... or github_pat_..." + className="w-full rounded-xl border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/30 font-mono" + /> + +
+ + {configured && ( + + )} +
+
+ ) : ( +
+
+ + ●●●●●●●●●●●●●●●● + + + + +
+ {repoFullName && ( +

+ Repo:{" "} + {repoFullName} +

+ )} + {summary?.lastTestedAt && ( +

+ Tested {formatRelative(summary.lastTestedAt)} +

+ )} +
+ )} + + {verifyError && ( +

{verifyError}

+ )} + {saveSuccess && ( +

+ ✓ GitHub connected. +

+ )} + {retrySuccess && ( +

+ ✓ Wiki generation started. +

+ )} + + setDeleteOpen(false)} + onConfirm={() => deleteMutation.mutate()} + /> +
+ ) +} + +function reasonToMessage( + res: Extract, + providerName: string, +): string { + if (res.reason === "invalid_key") return `Key rejected by ${providerName} (401).` + if (res.reason === "network") + return `Couldn't reach ${providerName}${res.message ? ` — ${res.message}` : ""}.` + if (res.reason === "not_implemented") + return "Test connection isn't implemented for this provider yet." + return "Test failed." +} + +function formatRelative(iso: string): string { + const then = new Date(iso).getTime() + const diff = Date.now() - then + const min = Math.round(diff / 60_000) + if (min < 1) return "just now" + if (min < 60) return `${min} min ago` + const hr = Math.round(min / 60) + if (hr < 24) return `${hr} hr ago` + const d = Math.round(hr / 24) + return `${d} d ago` +} diff --git a/frontend/src/routes/observability.lazy.tsx b/frontend/src/routes/observability.lazy.tsx new file mode 100644 index 0000000..b1206a8 --- /dev/null +++ b/frontend/src/routes/observability.lazy.tsx @@ -0,0 +1,530 @@ +import { createLazyFileRoute, useNavigate } from '@tanstack/react-router' +import { useQuery } from '@tanstack/react-query' +import { useState } from 'react' +import { apiFetch } from '@/lib/api' +import { useCurrentProjectId } from '@/components/project-selector' +import { + Activity, + ChevronDown, + ChevronRight, + DollarSign, + FolderGit2, + RefreshCw, + Zap, +} from 'lucide-react' + +export const Route = createLazyFileRoute('/observability')({ + component: ObservabilityPage, +}) + +// ---------- Types ---------- + +interface StatsData { + totalRuns: number + runsByStatus: { running: number; suspended: number; completed: number } + totalCost: number + totalTokens: number + totalCalls: number +} + +interface CostByModel { + model: string + displayName: string + totalCost: number + totalInput: number + totalOutput: number + callCount: number +} + +interface CostByDay { + date: string + totalCost: number + callCount: number +} + +interface CostsData { + byModel: CostByModel[] + byDay: CostByDay[] + totals: { + totalCost: number + totalInput: number + totalOutput: number + totalCalls: number + } +} + +interface WorkflowRun { + runId: string + threadId: string + issueId: string + issueUrl: string + status: string + createdAt: string | number +} + +interface AgentUsage { + agentId: string + totalCalls: number + totalInputTokens: number + totalOutputTokens: number + totalCost: number + avgDurationMs: number +} + +interface PricingModel { + modelId: string + displayName: string + inputPer1M: number + outputPer1M: number + isFree: boolean +} + +interface PricingData { + models: PricingModel[] +} + +// ---------- Helpers ---------- + +function formatCost(value: number | null | undefined): string { + return `$${(value ?? 0).toFixed(4)}` +} + +function formatCostShort(value: number | null | undefined): string { + const v = value ?? 0 + if (v >= 1) return `$${v.toFixed(2)}` + return `$${v.toFixed(4)}` +} + +function formatTokens(n: number | null | undefined): string { + const val = n ?? 0 + if (val >= 1_000_000) return `${(val / 1_000_000).toFixed(1)}M` + if (val >= 1_000) return `${(val / 1_000).toFixed(1)}K` + return val.toLocaleString() +} + +function formatDuration(ms: number | null | undefined): string { + const val = ms ?? 0 + if (val < 1_000) return `${Math.round(val)}ms` + return `${(val / 1_000).toFixed(1)}s` +} + +function formatTimeAgo(iso: string): string { + const diffMs = Date.now() - new Date(iso).getTime() + const diffMin = Math.floor(diffMs / 60_000) + if (diffMin < 1) return 'just now' + if (diffMin < 60) return `${diffMin}m ago` + const diffHr = Math.floor(diffMin / 60) + if (diffHr < 24) return `${diffHr}h ago` + return `${Math.floor(diffHr / 24)}d ago` +} + +// ---------- Sub-components ---------- + +function StatusBadge({ status }: { status: string }) { + const config: Record = { + completed: { dotColor: 'bg-green-500', textColor: 'text-green-400', label: 'Completed' }, + suspended: { dotColor: 'bg-amber-500', textColor: 'text-amber-400', label: 'Suspended' }, + running: { dotColor: 'bg-blue-500', textColor: 'text-blue-400', label: 'Running' }, + } + const c = config[status] ?? { dotColor: 'bg-gray-500', textColor: 'text-gray-400', label: status } + return ( + + + {c.label} + + ) +} + +function StatusPill({ label, count, color }: { label: string; count: number; color: string }) { + return ( + + + {count} {label} + + ) +} + +// ---------- Main Page ---------- + +function ObservabilityPage() { + const [currentProjectId] = useCurrentProjectId() + const navigate = useNavigate() + const [pricingOpen, setPricingOpen] = useState(false) + + const REFETCH_INTERVAL = 60_000 + + const { data: stats, isLoading: statsLoading, dataUpdatedAt: statsUpdatedAt } = useQuery({ + queryKey: ['observability-stats'], + queryFn: () => apiFetch('/observability/stats'), + refetchInterval: REFETCH_INTERVAL, + enabled: !!currentProjectId, + }) + + const { data: costs, isLoading: costsLoading } = useQuery({ + queryKey: ['observability-costs'], + queryFn: () => apiFetch('/observability/costs'), + refetchInterval: REFETCH_INTERVAL, + enabled: !!currentProjectId, + }) + + const { data: workflows, isLoading: workflowsLoading } = useQuery({ + queryKey: ['observability-workflows'], + queryFn: () => apiFetch('/observability/workflows'), + refetchInterval: REFETCH_INTERVAL, + enabled: !!currentProjectId, + }) + + const { data: agents, isLoading: agentsLoading } = useQuery({ + queryKey: ['observability-agents'], + queryFn: () => apiFetch('/observability/agents'), + refetchInterval: REFETCH_INTERVAL, + enabled: !!currentProjectId, + }) + + const { data: pricing } = useQuery({ + queryKey: ['observability-pricing'], + queryFn: () => apiFetch('/observability/pricing'), + staleTime: 300_000, + enabled: !!currentProjectId, + }) + + // Gate: require a project to be selected + if (!currentProjectId) { + return ( +
+
+
+ +
+

+ Select or create a project to view observability +

+

+ A project is required before you can use the observability dashboard. +

+ +
+
+ ) + } + + const successRate = + stats && stats.totalRuns > 0 + ? ((stats.runsByStatus.completed / stats.totalRuns) * 100).toFixed(1) + : '0.0' + + const isLoading = statsLoading || costsLoading || workflowsLoading || agentsLoading + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

+ Observability +

+

+ Workflow runs, costs, and agent metrics +

+
+
+
+ + + {statsUpdatedAt + ? `Updated ${formatTimeAgo(new Date(statsUpdatedAt).toISOString())}` + : 'Loading...'} + + + Auto-refresh 60s + +
+
+ + {/* Content */} +
+ {/* Summary Cards */} +
+ {/* Total Workflow Runs */} +
+
+ + Total Workflow Runs +
+ {statsLoading ? ( +
+ ) : ( + <> +
{stats?.totalRuns ?? 0}
+
+ {(stats?.runsByStatus.completed ?? 0) > 0 && ( + + )} + {(stats?.runsByStatus.suspended ?? 0) > 0 && ( + + )} + {(stats?.runsByStatus.running ?? 0) > 0 && ( + + )} +
+ + )} +
+ + {/* Success Rate */} +
+
+ + Success Rate +
+ {statsLoading ? ( +
+ ) : ( + <> +
{successRate}%
+

+ {stats?.runsByStatus.completed ?? 0} completed of {stats?.totalRuns ?? 0} +

+ + )} +
+ + {/* Total Cost */} +
+
+ + Total Cost +
+ {statsLoading ? ( +
+ ) : ( + <> +
{formatCostShort(stats?.totalCost ?? 0)}
+

+ {formatTokens(stats?.totalTokens ?? 0)} tokens used +

+ + )} +
+ + {/* Total API Calls */} +
+
+ + Total API Calls +
+ {statsLoading ? ( +
+ ) : ( +
{(stats?.totalCalls ?? 0).toLocaleString()}
+ )} +
+
+ + {/* Cost Breakdown Section */} +
+

Cost Breakdown by Model

+ {costsLoading ? ( +
+ {[...Array(3)].map((_, i) => ( +
+ ))} +
+ ) : costs?.byModel && costs.byModel.length > 0 ? ( +
+ + + + + + + + + + + + {costs.byModel.map((row) => ( + + + + + + + + ))} + {/* Totals row */} + + + + + + + + +
ModelCallsInput TokensOutput TokensCost
+ {row.displayName} + {row.model} + {row.callCount.toLocaleString()}{formatTokens(row.totalInput)}{formatTokens(row.totalOutput)}{formatCost(row.totalCost)}
Total{costs.totals.totalCalls.toLocaleString()}{formatTokens(costs.totals.totalInput)}{formatTokens(costs.totals.totalOutput)}{formatCost(costs.totals.totalCost)}
+
+ ) : ( +

No cost data available yet.

+ )} +
+ + {/* Recent Workflow Runs */} +
+

Recent Workflow Runs

+ {workflowsLoading ? ( +
+ {[...Array(5)].map((_, i) => ( +
+ ))} +
+ ) : workflows && workflows.length > 0 ? ( +
+ + + + + + + + + + + {workflows.slice(0, 20).map((run) => ( + + + + + + + ))} + +
TimeStatusIssueRun ID
{formatTimeAgo(String(run.createdAt))} + + + {run.issueUrl ? ( + + {run.issueId} + + ) : ( + {run.issueId || '-'} + )} + + {run.runId} +
+
+ ) : ( +

No workflow runs recorded yet.

+ )} +
+ + {/* Agent Usage */} +
+

Agent Usage

+ {agentsLoading ? ( +
+ {[...Array(3)].map((_, i) => ( +
+ ))} +
+ ) : agents && agents.length > 0 ? ( +
+ + + + + + + + + + + + + {agents.map((agent) => ( + + + + + + + + + ))} + +
AgentCallsInput TokensOutput TokensCostAvg Duration
{agent.agentId}{agent.totalCalls.toLocaleString()}{formatTokens(agent.totalInputTokens)}{formatTokens(agent.totalOutputTokens)}{formatCost(agent.totalCost)}{formatDuration(agent.avgDurationMs)}
+
+ ) : ( +

No agent usage data yet.

+ )} +
+ + {/* Pricing Reference (collapsible) */} +
+ + {pricingOpen && pricing?.models && ( +
+ + + + + + + + + + + {pricing.models.map((model) => ( + + + + + + + ))} + +
ModelInput / 1M tokensOutput / 1M tokensFree Tier
+ {model.displayName} + {model.modelId} + {formatCostShort(model.inputPer1M)}{formatCostShort(model.outputPer1M)} + {model.isFree ? ( + Yes + ) : ( + No + )} +
+
+ )} +
+
+
+ ) +} diff --git a/frontend/src/routes/projects.lazy.tsx b/frontend/src/routes/projects.lazy.tsx index 5ef67e4..a179db6 100644 --- a/frontend/src/routes/projects.lazy.tsx +++ b/frontend/src/routes/projects.lazy.tsx @@ -1,4 +1,4 @@ -import { createLazyFileRoute } from "@tanstack/react-router" +import { createLazyFileRoute, Link } from "@tanstack/react-router" import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" import { useState } from "react" import { @@ -8,10 +8,12 @@ import { CheckCircle2, AlertCircle, Clock, + Lock, Trash2, Database, FileCode, ExternalLink, + Pencil, } from "lucide-react" export const Route = createLazyFileRoute("/projects")({ @@ -23,7 +25,7 @@ interface Project { name: string repositoryUrl: string branch: string - status: "pending" | "processing" | "ready" | "error" + status: "pending" | "processing" | "ready" | "error" | "needs_auth" documentsCount: number chunksCount: number error: string | null @@ -34,9 +36,29 @@ interface Project { function ProjectsPage() { const queryClient = useQueryClient() const [showForm, setShowForm] = useState(false) + const [editingId, setEditingId] = useState(null) const [name, setName] = useState("") const [repoUrl, setRepoUrl] = useState("") const [branch, setBranch] = useState("main") + const [description, setDescription] = useState("") + + const resetForm = () => { + setEditingId(null) + setName("") + setRepoUrl("") + setBranch("main") + setDescription("") + setShowForm(false) + } + + const startEdit = (project: Project) => { + setEditingId(project.id) + setName(project.name) + setRepoUrl(project.repositoryUrl) + setBranch(project.branch || "main") + setDescription((project as unknown as { description?: string }).description ?? "") + setShowForm(true) + } const { data: projects, isLoading } = useQuery({ queryKey: ["projects"], @@ -50,21 +72,32 @@ function ProjectsPage() { refetchInterval: 5000, // Poll for status updates during processing }) - const createMutation = useMutation({ + const saveMutation = useMutation({ mutationFn: async () => { - const res = await fetch("/projects", { - method: "POST", + const body = JSON.stringify({ name, repositoryUrl: repoUrl, branch, description }) + const url = editingId ? `/projects/${editingId}` : "/projects" + const method = editingId ? "PATCH" : "POST" + const res = await fetch(url, { + method, headers: { "Content-Type": "application/json", Accept: "application/json" }, - body: JSON.stringify({ name, repositoryUrl: repoUrl, branch }), + body, }) - return res.json() + return (await res.json()) as { success: boolean; data?: Project } }, - onSuccess: () => { + onSuccess: (response) => { + // Inject the server-returned project into the cache so it shows up + // immediately — otherwise the UI waits on the invalidation refetch + // and the list looks empty for a beat after create. + if (response?.success && response.data) { + const incoming = response.data + queryClient.setQueryData(["projects"], (old) => { + if (!old) return [incoming] + if (editingId) return old.map((p) => (p.id === editingId ? incoming : p)) + return [incoming, ...old] + }) + } queryClient.invalidateQueries({ queryKey: ["projects"] }) - setShowForm(false) - setName("") - setRepoUrl("") - setBranch("main") + resetForm() }, }) @@ -80,6 +113,12 @@ function ProjectsPage() { }, }) + const handleDelete = (id: string, name: string) => { + if (window.confirm(`Delete project "${name}"? This cannot be undone.`)) { + deleteMutation.mutate(id) + } + } + return (
{/* Header */} @@ -98,7 +137,7 @@ function ProjectsPage() {