diff --git a/.gitignore b/.gitignore index 900b34b..942e7c9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ dist/ # dependencies (bun install) node_modules +.pnpm-store # output out diff --git a/pkg/create/templates.go b/pkg/create/templates.go index f99c4e6..c3bc6d1 100644 --- a/pkg/create/templates.go +++ b/pkg/create/templates.go @@ -18,6 +18,7 @@ const ( TemplateStagehand = "stagehand" TemplateOpenAGIComputerUse = "openagi-computer-use" TemplateClaudeAgentSDK = "claude-agent-sdk" + TemplateQaAgent = "qa-agent" ) type TemplateInfo struct { @@ -84,6 +85,11 @@ var Templates = map[string]TemplateInfo{ Description: "Implements a Claude Agent SDK browser automation agent", Languages: []string{LanguageTypeScript, LanguagePython}, }, + TemplateQaAgent: { + Name: "QA Agent", + Description: "Visual QA testing agent using AI vision models", + Languages: []string{LanguageTypeScript}, + }, } // GetSupportedTemplatesForLanguage returns a list of all supported template names for a given language @@ -200,6 +206,11 @@ var Commands = map[string]map[string]DeployConfig{ NeedsEnvFile: true, InvokeCommand: `kernel invoke ts-claude-agent-sdk agent-task --payload '{"task": "Go to https://news.ycombinator.com and get the top 3 stories"}'`, }, + TemplateQaAgent: { + EntryPoint: "index.ts", + NeedsEnvFile: true, + InvokeCommand: `kernel invoke ts-qa-agent qa-test --payload '{"url": "https://cash.app", "model": "claude"}'`, + }, }, LanguagePython: { TemplateSampleApp: { diff --git a/pkg/templates/typescript/qa-agent/README.md b/pkg/templates/typescript/qa-agent/README.md new file mode 100644 index 0000000..712a942 --- /dev/null +++ b/pkg/templates/typescript/qa-agent/README.md @@ -0,0 +1,567 @@ +# Kernel QA Agent + +An AI-powered quality assurance agent that uses vision models (Claude, GPT-4o, Gemini) to automatically analyze websites for compliance issues, policy violations, broken UI, and design quality. + +## What it does + +The QA Agent performs comprehensive analysis on websites by: + +- **Compliance Checking**: Validates accessibility (WCAG/ADA), legal requirements, brand guidelines, and industry regulations +- **Policy Violation Detection**: Identifies content policy violations and security issues +- **Broken UI Analysis**: Detects visual defects and design inconsistencies +- **AI-Powered Insights**: Uses vision models to identify compliance gaps that traditional tools miss +- **Comprehensive Reports**: Generates both JSON (machine-readable) and HTML (human-readable) reports with actionable recommendations + +## Key Features + +- **Multi-Domain Compliance**: Accessibility, legal, brand, and regulatory compliance checking +- **Industry-Specific Rules**: Pre-configured compliance checks for finance, healthcare, and e-commerce +- **Policy Enforcement**: Automated detection of content and security policy violations +- **Configurable Vision Models**: Choose between Claude (Anthropic), GPT-4o (OpenAI), or Gemini (Google) +- **Risk-Based Reporting**: Issues categorized by severity and risk level +- **Actionable Recommendations**: Specific guidance on how to fix each issue +- **Rich HTML Reports**: Beautiful, interactive reports with embedded screenshots and compliance standards +- **CI/CD Integration**: Structured JSON output for automated compliance pipelines + +## Input + +```json +{ + "url": "https://cash.app", + "model": "claude", // Options: "claude", "gpt4o", "gemini" (default: "claude") + "checks": { + "compliance": { + "accessibility": true, // WCAG/ADA compliance + "legal": true, // Legal requirements (privacy, terms, etc.) + "brand": false, // Brand guidelines (requires brandGuidelines) + "regulatory": true // Industry-specific regulations + }, + "policyViolations": { + "content": true, // Content policy violations + "security": true // Security issues + }, + "brokenUI": false // Visual/UI issues (optional) + }, + "context": { + "industry": "finance", // e.g., "finance", "healthcare", "ecommerce" + "brandGuidelines": "...", // Brand guidelines text (optional) + "customPolicies": "..." // Custom policies to enforce (optional) + } +} +``` + +## Output + +```json +{ + "success": true, + "summary": { + "totalIssues": 5, + "criticalIssues": 1, + "warnings": 3, + "infos": 1 + }, + "issues": [ + { + "severity": "critical", + "category": "functional", + "description": "Broken images detected", + "page": "https://example.com", + "location": "Main hero section", + "screenshot": "base64_encoded_screenshot" + } + ], + "jsonReport": "{ ... }", + "htmlReport": "..." +} +``` + +## Quick Start + +### Option 1: Web UI (Easiest) + +1. Install dependencies: +```bash +pnpm install +``` + +2. Create your `.env` file (copy from `env.example` and add your API keys) + +3. Start the UI server: +```bash +pnpm ui +``` + +4. Open http://localhost:3000 in your browser and use the visual interface! + +### Option 2: Command Line + +Run directly from the command line (see Local Testing section below). + +### Option 3: Deploy to Kernel + +Deploy and invoke remotely (see Deploy section below). + +## Setup + +### 1. Install Dependencies + +The template will automatically install dependencies when created. If needed, run: + +```bash +pnpm install +``` + +### 2. Configure API Keys + +Create a `.env` file in your project directory (you can copy from `env.example`): + +```env +# At least ONE of these is required, depending on which model you plan to use: +ANTHROPIC_API_KEY=your-anthropic-api-key # For Claude (recommended) +OPENAI_API_KEY=your-openai-api-key # For GPT-4o +GOOGLE_API_KEY=your-google-api-key # For Gemini + +# Optional: Kernel API key (if not using kernel login) +KERNEL_API_KEY=your-kernel-api-key +``` + +### 3. Get API Keys + +- **Anthropic (Claude)**: https://console.anthropic.com/ +- **OpenAI (GPT-4o)**: https://platform.openai.com/api-keys +- **Google (Gemini)**: https://aistudio.google.com/app/apikey + +## Deploy + +Deploy your QA agent to Kernel: + +```bash +kernel login # If you haven't already +kernel deploy index.ts --env-file .env +``` + +## Usage + +### Financial Services Compliance (Cash App Example) + +Check a financial services website for regulatory compliance: + +```bash +kernel invoke ts-qa-agent qa-test --payload '{ + "url": "https://cash.app", + "model": "claude", + "checks": { + "compliance": { + "accessibility": true, + "legal": true, + "regulatory": true + }, + "policyViolations": { + "content": true, + "security": true + } + }, + "context": { + "industry": "finance" + } +}' +``` + +### Healthcare Compliance + +Check HIPAA and healthcare compliance: + +```bash +kernel invoke ts-qa-agent qa-test --payload '{ + "url": "https://healthcare-example.com", + "checks": { + "compliance": { + "accessibility": true, + "legal": true, + "regulatory": true + }, + "policyViolations": { + "security": true + } + }, + "context": { + "industry": "healthcare" + } +}' +``` + +### Brand Compliance Audit + +Verify brand guidelines adherence: + +```bash +kernel invoke ts-qa-agent qa-test --payload '{ + "url": "https://example.com", + "checks": { + "compliance": { + "brand": true + } + }, + "context": { + "brandGuidelines": "Logo must be in top-left, primary color #007bff, font family Inter, 16px minimum body text" + } +}' +``` + +### Accessibility Only + +Check WCAG 2.1 AA compliance: + +```bash +kernel invoke ts-qa-agent qa-test --payload '{ + "url": "https://example.com", + "checks": { + "compliance": { + "accessibility": true + } + } +}' +``` + +### Custom Policy Enforcement + +Enforce custom content policies: + +```bash +kernel invoke ts-qa-agent qa-test --payload '{ + "url": "https://example.com", + "checks": { + "policyViolations": { + "content": true + } + }, + "context": { + "customPolicies": "No medical claims without FDA approval. No income guarantees. All testimonials must include disclaimers." + } +}' +``` + +### Web UI (Recommended) + +The easiest way to use the QA agent is through the built-in web interface: + +```bash +# Make sure you have your .env file configured +pnpm install # First time only +pnpm ui +``` + +Then open http://localhost:3000 in your browser. You'll get a beautiful interface where you can: +- Enter the URL to test +- Select which compliance checks to run +- Choose the AI model +- Provide industry context +- View results with interactive reports +- Export HTML reports + +### Command Line Testing + +You can also run the agent from the command line: + +```bash +# Make sure you have your .env file configured +npx tsx index.ts https://example.com claude +``` + +Arguments: +1. URL to test (default: https://cash.app) +2. Model to use (default: claude) + +## Web UI Features + +The built-in web interface (`pnpm ui`) provides: + +- **Visual Form Builder**: Easy-to-use checkboxes and inputs for all options +- **Real-time Analysis**: See results immediately in the browser +- **Interactive Results**: Click to expand sections and view details +- **Export Reports**: Download full HTML reports with one click +- **Model Comparison**: Easily test different vision models +- **Industry Presets**: Quick selection for finance, healthcare, e-commerce +- **Custom Policies**: Define your own compliance rules +- **Brand Guidelines**: Input specific brand requirements + +## Vision Model Comparison + +| Model | Provider | Best For | Cost | Speed | +|-------|----------|----------|------|-------| +| Claude (Sonnet 3.5) | Anthropic | Most accurate compliance analysis | $$$ | Fast | +| GPT-4o | OpenAI | Good balance of quality and cost | $$ | Fast | +| Gemini (2.0 Flash) | Google | Cost-effective, high volume | $ | Very Fast | + +**Recommendation**: Start with Claude for the most thorough compliance analysis, then switch to GPT-4o or Gemini for production/regular monitoring. + +## Use Cases + +### 1. Financial Services Compliance Monitoring + +Monitor Cash App or other fintech sites for regulatory compliance: + +```bash +kernel invoke ts-qa-agent qa-test --payload '{ + "url": "https://cash.app", + "checks": { + "compliance": {"accessibility": true, "legal": true, "regulatory": true}, + "policyViolations": {"security": true} + }, + "context": {"industry": "finance"} +}' +``` + +**Catches**: Missing risk disclaimers, APR disclosure issues, FDIC notice problems, accessibility violations + +### 2. Pre-Launch Compliance Audit + +Verify compliance before launching a new product or feature: + +```bash +kernel invoke ts-qa-agent qa-test --payload '{ + "url": "https://staging.myapp.com/new-feature", + "checks": { + "compliance": {"accessibility": true, "legal": true} + } +}' +``` + +**Catches**: Missing legal notices, WCAG violations, required disclosures + +### 3. Brand Consistency Enforcement + +Ensure marketing pages follow brand guidelines: + +```bash +kernel invoke ts-qa-agent qa-test --payload '{ + "url": "https://myapp.com/landing", + "checks": {"compliance": {"brand": true}}, + "context": {"brandGuidelines": "Logo top-left, #007bff primary, Inter font"} +}' +``` + +**Catches**: Logo misuse, off-brand colors, typography violations + +### 4. Healthcare HIPAA Compliance + +Check healthcare websites for HIPAA compliance: + +```bash +kernel invoke ts-qa-agent qa-test --payload '{ + "url": "https://healthcareapp.com", + "checks": { + "compliance": {"legal": true, "regulatory": true}, + "policyViolations": {"security": true} + }, + "context": {"industry": "healthcare"} +}' +``` + +**Catches**: Missing privacy notices, exposed patient data, insecure forms + +### 5. Content Moderation + +Detect policy-violating content: + +```bash +kernel invoke ts-qa-agent qa-test --payload '{ + "url": "https://example.com", + "checks": {"policyViolations": {"content": true}}, + "context": {"customPolicies": "No unverified medical claims"} +}' +``` + +**Catches**: Misleading claims, prohibited content, policy violations + +## Report Formats + +### JSON Report + +Machine-readable format perfect for CI/CD integration: + +```json +{ + "metadata": { + "url": "https://example.com", + "model": "Claude (Anthropic)", + "timestamp": "2026-01-20T10:30:00.000Z", + "generatedBy": "Kernel QA Agent" + }, + "summary": { + "totalIssues": 5, + "critical": 1, + "warnings": 3, + "info": 1 + }, + "issuesByCategory": { + "visual": 3, + "functional": 2, + "accessibility": 0 + }, + "issues": [...] +} +``` + +### HTML Report + +Beautiful, interactive report with: +- Executive summary dashboard +- Issues grouped by severity and category +- Embedded screenshots for visual issues +- Responsive design for viewing on any device + +The HTML report is included in the response as `htmlReport` field. Save it to a file to view in your browser: + +```javascript +// In your integration code +const result = await invoke("qa-test", { url: "https://example.com" }); +fs.writeFileSync("qa-report.html", result.htmlReport); +``` + +## What Issues Does It Detect? + +### Compliance Issues + +#### Accessibility (WCAG 2.1 AA) +- ✓ Color contrast violations (text readability) +- ✓ Missing alt text indicators +- ✓ Form labels and ARIA attributes +- ✓ Focus indicators on interactive elements +- ✓ Heading hierarchy issues +- ✓ Font size and readability problems + +#### Legal Compliance +- ✓ Missing privacy policy links +- ✓ Missing terms of service +- ✓ Cookie consent banner (GDPR) +- ✓ Required disclaimers +- ✓ Data collection notices +- ✓ Age restriction warnings +- ✓ Copyright notices + +#### Brand Guidelines +- ✓ Logo usage and placement +- ✓ Color palette adherence +- ✓ Typography inconsistencies +- ✓ Spacing and layout violations +- ✓ Imagery style mismatches + +#### Regulatory (Industry-Specific) +- **Finance**: Risk disclaimers, APR display, fee disclosures, FDIC notices +- **Healthcare**: HIPAA notices, privacy practices, provider credentials +- **E-commerce**: Pricing transparency, return policies, shipping costs + +### Policy Violations + +#### Content Policy +- ✓ Inappropriate or offensive content +- ✓ Misleading claims or false advertising +- ✓ Unverified health/medical claims +- ✓ Get-rich-quick schemes +- ✓ Age-inappropriate content +- ✓ Prohibited products/services +- ✓ Copyright infringement + +#### Security Issues +- ✓ Exposed personal data +- ✓ Missing HTTPS indicators on forms +- ✓ Insecure payment displays +- ✓ Exposed API keys or tokens +- ✓ Weak password requirements +- ✓ Missing security badges +- ✓ Suspicious external links + +### Broken UI (Optional) +- ✓ Layout problems +- ✓ Spacing inconsistencies +- ✓ Broken or missing images +- ✓ Design inconsistencies + +## Limitations + +- Analyzes visible page content only (doesn't interact with modals or dynamic content) +- AI accuracy depends on the chosen model and screenshot quality +- Cannot verify actual HTTPS connection (only visible indicators) +- Brand guideline checking requires explicit guidelines in context +- Industry regulations are based on common requirements, not exhaustive legal analysis + +## Tips for Best Results + +1. **Be Specific with Industry**: Provide accurate industry context for best regulatory checks +2. **Use Staging First**: Test on staging to avoid affecting production analytics +3. **Start with Claude**: Use Claude for most accurate compliance analysis +4. **Provide Brand Guidelines**: Include specific, measurable brand rules for best results +5. **Custom Policies**: Write clear, specific custom policies for content checking +6. **Review AI Findings**: AI suggestions should be reviewed by compliance experts +7. **Regular Monitoring**: Run compliance checks regularly as part of CI/CD +8. **Combine with Tools**: This complements (not replaces) traditional compliance tools + +## Troubleshooting + +### "API key not found" Error + +Make sure your `.env` file is properly configured and you're deploying with `--env-file .env`. + +### "Navigation timeout" Error + +The target website may be slow or blocking automated browsers. Try: +- Increasing the timeout in the code +- Using Kernel's stealth mode (already enabled by default) +- Testing with a different URL + +### AI Returns No Issues on Problematic Page + +Try: +- Using a different vision model (Claude is most thorough) +- Enabling/disabling specific check types +- Checking if the page loaded correctly in the live view URL + +## Example Output + +``` +Starting QA analysis for: https://cash.app +Model: claude +Compliance checks: Accessibility=true, Legal=true, Brand=false, Regulatory=true +Policy checks: Content=true, Security=true +Using Claude (Anthropic) for analysis +Kernel browser live view url: https://kernel.sh/view/... + +Performing compliance checks... + Checking accessibility (WCAG 2.1 AA)... + Found 3 accessibility issues + Checking legal compliance... + Found 1 legal compliance issues + Checking finance regulatory compliance... + Found 2 regulatory compliance issues + +Compliance analysis: Found 6 issues + +Detecting policy violations... + Checking content policy... + Found 0 content policy violations + Checking security issues... + Found 1 security issues + +Policy analysis: Found 1 violations + +QA Analysis Complete! +Total issues found: 7 +- Critical: 1 +- Warnings: 4 +- Info: 2 +``` + +## Learn More + +- [Kernel Documentation](https://kernel.sh/docs) +- [Anthropic Claude Vision](https://docs.anthropic.com/claude/docs/vision) +- [OpenAI GPT-4o Vision](https://platform.openai.com/docs/guides/vision) +- [Google Gemini Multimodal](https://ai.google.dev/gemini-api/docs/vision) + +## Support + +For issues or questions: +- [Kernel Discord](https://discord.gg/kernel) +- [GitHub Issues](https://github.com/kernel/cli/issues) diff --git a/pkg/templates/typescript/qa-agent/_gitignore b/pkg/templates/typescript/qa-agent/_gitignore new file mode 100644 index 0000000..a117042 --- /dev/null +++ b/pkg/templates/typescript/qa-agent/_gitignore @@ -0,0 +1,43 @@ +# Dependencies +node_modules/ +package-lock.json + +# TypeScript +*.tsbuildinfo +dist/ +build/ + +# Environment +.env +.env.local +.env.*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Testing +coverage/ +.nyc_output/ + +# Misc +.cache/ +.temp/ +.tmp/ + +# QA Reports +reports/ +*.html diff --git a/pkg/templates/typescript/qa-agent/env.example b/pkg/templates/typescript/qa-agent/env.example new file mode 100644 index 0000000..3039541 --- /dev/null +++ b/pkg/templates/typescript/qa-agent/env.example @@ -0,0 +1,7 @@ +# Required: At least one model API key +ANTHROPIC_API_KEY=your-anthropic-api-key # For Claude (recommended for best vision capabilities) +OPENAI_API_KEY=your-openai-api-key # For GPT-4o +GOOGLE_API_KEY=your-google-api-key # For Gemini + +# Optional: Kernel API key (if not using kernel login) +KERNEL_API_KEY=your-kernel-api-key diff --git a/pkg/templates/typescript/qa-agent/index.ts b/pkg/templates/typescript/qa-agent/index.ts new file mode 100644 index 0000000..19a8c3a --- /dev/null +++ b/pkg/templates/typescript/qa-agent/index.ts @@ -0,0 +1,1654 @@ +import Anthropic from "@anthropic-ai/sdk"; +import { GoogleGenerativeAI } from "@google/generative-ai"; +import { Kernel, type KernelContext } from "@onkernel/sdk"; +import { config } from "dotenv"; +import OpenAI from "openai"; +import { chromium, type Page } from "playwright-core"; + +// Load environment variables from .env file +config(); + +const kernel = new Kernel(); +const app = kernel.app("ts-qa-agent"); + +// ============================================================================ +// Type Definitions +// ============================================================================ + +interface QaTaskInput { + url: string; + model?: "claude" | "gpt4o" | "gemini"; + dismissPopups?: boolean; + checks?: { + compliance?: { + accessibility?: boolean; + legal?: boolean; + brand?: boolean; + regulatory?: boolean; + }; + policyViolations?: { + content?: boolean; + security?: boolean; + }; + brokenUI?: boolean; + }; + context?: { + industry?: string; + brandGuidelines?: string; + customPolicies?: string; + }; +} + +interface QaIssue { + severity: "critical" | "warning" | "info"; + category: "visual" | "functional" | "accessibility" | "compliance" | "policy"; + description: string; + page: string; + location?: string; + screenshot?: string; + complianceType?: "accessibility" | "legal" | "brand" | "regulatory"; + standard?: string; + recommendation?: string; + violationType?: "content" | "security"; + riskLevel?: "high" | "medium" | "low"; +} + +interface QaTaskOutput { + success: boolean; + summary: { + totalIssues: number; + criticalIssues: number; + warnings: number; + infos: number; + }; + issues: QaIssue[]; + jsonReport: string; + htmlReport: string; +} + +// ============================================================================ +// Vision Provider Interface & Implementations +// ============================================================================ + +interface VisionProvider { + name: string; + analyzeScreenshot(screenshot: Buffer, prompt: string): Promise; +} + +class ClaudeVisionProvider implements VisionProvider { + name = "Claude (Anthropic)"; + private client: Anthropic; + + constructor(apiKey: string) { + this.client = new Anthropic({ apiKey }); + } + + async analyzeScreenshot(screenshot: Buffer, prompt: string): Promise { + const base64Image = screenshot.toString("base64"); + + const response = await this.client.messages.create({ + model: "claude-3-5-sonnet-20241022", + max_tokens: 2048, + messages: [ + { + role: "user", + content: [ + { + type: "image", + source: { + type: "base64", + media_type: "image/png", + data: base64Image, + }, + }, + { + type: "text", + text: prompt, + }, + ], + }, + ], + }); + + const textContent = response.content.find((block) => block.type === "text"); + return textContent && textContent.type === "text" ? textContent.text : ""; + } +} + +class GPT4oVisionProvider implements VisionProvider { + name = "GPT-4o (OpenAI)"; + private client: OpenAI; + + constructor(apiKey: string) { + this.client = new OpenAI({ apiKey }); + } + + async analyzeScreenshot(screenshot: Buffer, prompt: string): Promise { + const base64Image = screenshot.toString("base64"); + + const response = await this.client.chat.completions.create({ + model: "gpt-4o", + max_tokens: 2048, + messages: [ + { + role: "user", + content: [ + { + type: "image_url", + image_url: { + url: `data:image/png;base64,${base64Image}`, + }, + }, + { + type: "text", + text: prompt, + }, + ], + }, + ], + }); + + return response.choices[0]?.message?.content || ""; + } +} + +class GeminiVisionProvider implements VisionProvider { + name = "Gemini (Google)"; + private client: GoogleGenerativeAI; + + constructor(apiKey: string) { + this.client = new GoogleGenerativeAI(apiKey); + } + + async analyzeScreenshot(screenshot: Buffer, prompt: string): Promise { + const model = this.client.getGenerativeModel({ model: "gemini-2.0-flash-exp" }); + + const result = await model.generateContent([ + { + inlineData: { + mimeType: "image/png", + data: screenshot.toString("base64"), + }, + }, + prompt, + ]); + + return result.response.text(); + } +} + +function createVisionProvider(model: string): VisionProvider { + switch (model) { + case "claude": + const anthropicKey = process.env.ANTHROPIC_API_KEY; + if (!anthropicKey) { + throw new Error("ANTHROPIC_API_KEY is required for Claude model"); + } + return new ClaudeVisionProvider(anthropicKey); + + case "gpt4o": + const openaiKey = process.env.OPENAI_API_KEY; + if (!openaiKey) { + throw new Error("OPENAI_API_KEY is required for GPT-4o model"); + } + return new GPT4oVisionProvider(openaiKey); + + case "gemini": + const googleKey = process.env.GOOGLE_API_KEY; + if (!googleKey) { + throw new Error("GOOGLE_API_KEY is required for Gemini model"); + } + return new GeminiVisionProvider(googleKey); + + default: + throw new Error(`Unknown model: ${model}`); + } +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * Scroll through the page to trigger lazy loading of images + */ +async function scrollAndLoadImages(page: Page): Promise { + await page.evaluate(` + new Promise((resolve) => { + const scrollHeight = document.documentElement.scrollHeight; + const viewportHeight = window.innerHeight; + let currentPosition = 0; + const scrollStep = viewportHeight; + + function scrollNext() { + if (currentPosition < scrollHeight) { + window.scrollTo(0, currentPosition); + currentPosition += scrollStep; + setTimeout(scrollNext, 300); + } else { + window.scrollTo(0, 0); + setTimeout(resolve, 500); + } + } + + scrollNext(); + }) + `); +} + +/** + * Dismiss popups, modals, overlays, and toast notifications that may block content + */ +async function dismissPopups(page: Page): Promise { + try { + let dismissed = false; + + // First, identify if there's actually a popup/modal/overlay visible + const popupContainerSelectors = [ + '[role="dialog"]', + '[role="alertdialog"]', + '.modal', + '.popup', + '.overlay', + '.cookie-banner', + '.cookie-consent', + '#cookie-consent', + '.notification', + '.toast', + '.alert', + '.snackbar', + '[class*="modal" i]', + '[class*="popup" i]', + '[class*="overlay" i]', + '[class*="banner" i]', + '[class*="cookie" i]', + '[id*="cookie" i]', + '[id*="modal" i]', + '[id*="popup" i]', + ]; + + let popupContainer = null; + for (const selector of popupContainerSelectors) { + try { + const container = page.locator(selector).first(); + if (await container.isVisible({ timeout: 300 })) { + popupContainer = container; + console.log(` Found popup container: ${selector}`); + break; + } + } catch (e) { + // Continue + } + } + + // If no popup container found, try ESC key and exit (don't click random buttons) + if (!popupContainer) { + try { + await page.keyboard.press('Escape'); + await page.waitForTimeout(300); + console.log(' Pressed ESC key (no visible popup container found)'); + } catch (e) { + // ESC didn't work + } + return; + } + + // Strategy 1: Try Accept/OK/I Agree buttons WITHIN the popup container only + const acceptTexts = ['Accept all', 'Accept All', 'I agree', 'I Agree', 'Allow all', 'Allow All', 'Accept cookies', 'Accept Cookies']; + + for (const text of acceptTexts) { + try { + // Only look for buttons WITHIN the popup container + const button = popupContainer.locator(`button:has-text("${text}"), a:has-text("${text}"), div[role="button"]:has-text("${text}")`).first(); + if (await button.isVisible({ timeout: 500 })) { + console.log(` Found accept button in popup: "${text}"`); + await button.click(); + await page.waitForTimeout(800); + dismissed = true; + break; + } + } catch (e) { + // Continue + } + } + + // Strategy 2: Try CSS selectors for accept buttons WITHIN popup container + if (!dismissed) { + const acceptSelectors = [ + '[aria-label*="accept" i]', + '[aria-label*="agree" i]', + '[aria-label*="allow" i]', + '.accept-button', + '#accept-cookies', + 'button[id*="accept" i]', + 'button[class*="accept" i]', + '[data-action*="accept" i]', + ]; + + for (const selector of acceptSelectors) { + try { + // Only search within popup container + const button = popupContainer.locator(selector).first(); + if (await button.isVisible({ timeout: 300 })) { + console.log(` Found accept via selector in popup: ${selector}`); + await button.click(); + await page.waitForTimeout(800); + dismissed = true; + break; + } + } catch (e) { + // Continue + } + } + } + + // Strategy 3: Close buttons with text WITHIN popup container + if (!dismissed) { + const closeTexts = ['Close', '×', '✕', 'Dismiss', 'No thanks', 'No Thanks', 'Maybe later', 'Reject all', 'Reject All', 'Skip']; + + for (const text of closeTexts) { + try { + // Only look within popup container + const element = popupContainer.locator(`button:has-text("${text}"), a:has-text("${text}"), span[role="button"]:has-text("${text}"), div[role="button"]:has-text("${text}")`).first(); + if (await element.isVisible({ timeout: 500 })) { + console.log(` Found close element in popup: "${text}"`); + await element.click(); + await page.waitForTimeout(800); + dismissed = true; + break; + } + } catch (e) { + // Continue + } + } + } + + // Strategy 4: Close icons and buttons by CSS WITHIN popup container + if (!dismissed) { + const closeSelectors = [ + // Aria labels (accessibility) + '[aria-label*="close" i]', + '[aria-label*="dismiss" i]', + '[aria-label*="remove" i]', + + // Common close button classes + '.close-button', + '.close-icon', + '.modal-close', + '.popup-close', + '.dialog-close', + 'button.close', + '.toast-close', + '.notification-close', + '.banner-close', + + // Data attributes + '[data-dismiss="modal"]', + '[data-dismiss="toast"]', + '[data-dismiss="alert"]', + '[data-action*="close" i]', + '[data-action*="dismiss" i]', + + // Class name patterns + 'button[class*="close" i]', + 'button[class*="dismiss" i]', + 'span[class*="close" i]', + 'div[class*="close" i]', + 'a[class*="close" i]', + + // SVG close icons (often used in toasts) + 'svg[class*="close" i]', + 'button > svg', + '[aria-label*="close" i] > svg', + + // ID patterns + '#close-button', + '#dismiss-button', + 'button[id*="close" i]', + 'button[id*="dismiss" i]', + ]; + + for (const selector of closeSelectors) { + try { + // Only search within popup container + const element = popupContainer.locator(selector).first(); + if (await element.isVisible({ timeout: 300 })) { + console.log(` Found close via selector in popup: ${selector}`); + await element.click(); + await page.waitForTimeout(800); + dismissed = true; + break; + } + } catch (e) { + // Continue + } + } + } + + // Strategy 5: Look for common toast/notification containers and dismiss them + if (!dismissed) { + const toastSelectors = [ + '.toast', + '.notification', + '.alert', + '.snackbar', + '[role="alert"]', + '[role="status"]', + ]; + + for (const selector of toastSelectors) { + try { + const toast = page.locator(selector).first(); + if (await toast.isVisible({ timeout: 300 })) { + // Try to find a close button within the toast + const closeButton = toast.locator('button, [role="button"], .close, [aria-label*="close" i]').first(); + if (await closeButton.isVisible({ timeout: 300 })) { + console.log(` Found close button in toast: ${selector}`); + await closeButton.click(); + await page.waitForTimeout(500); + dismissed = true; + break; + } + } + } catch (e) { + // Continue + } + } + } + + // Strategy 6: Press ESC key (works for many modals) + try { + await page.keyboard.press('Escape'); + await page.waitForTimeout(300); + console.log(' Pressed ESC key'); + } catch (e) { + // ESC didn't work + } + + if (dismissed) { + console.log(' ✓ Successfully dismissed popup/toast'); + } else { + console.log(' No popups/toasts found to dismiss'); + } + + } catch (error) { + console.log(' Error dismissing popups:', error instanceof Error ? error.message : String(error)); + } +} + +function parseAIResponse(response: string): any[] { + try { + const jsonMatch = response.match(/\[[\s\S]*\]/); + if (jsonMatch) { + return JSON.parse(jsonMatch[0]); + } + return []; + } catch (error) { + console.error("Error parsing AI response:", error); + return []; + } +} + +function getIndustrySpecificPrompt(industry: string): string { + const prompts: Record = { + finance: `You are a QA expert testing financial websites. Only report MISSING critical regulatory elements. + +CRITICAL (must report): +- NO risk disclosures on investment/trading pages (stocks, crypto, loans) +- NO "Member FDIC" notice when claiming FDIC insurance +- NO APR disclosure on credit card/loan offers +- Credit products with NO terms or fee information visible +- Financial transactions over HTTP (not HTTPS) + +IGNORE (standard practices): +- Risk disclosures in footer or fine print (this is normal and acceptable) +- General disclaimers (standard legal protection) +- Links to full terms (users can click through) +- Small text for disclosures (legally compliant if present) +- Modern minimal designs with collapsible sections + +Major financial institutions (JPMorgan, Goldman Sachs, etc.) are generally compliant. Only flag MISSING required elements. + +Severity: +- "critical": Required disclosure completely absent +- "warning": Disclosure exists but hard to find + +Return JSON: [{"severity": "...", "standard": "...", "description": "...", "location": "...", "recommendation": "..."}] +If standard financial disclosures are present, return: []`, + + healthcare: `You are a QA expert testing healthcare websites. Only report MISSING critical health compliance elements. + +CRITICAL (must report): +- NO HIPAA privacy notice anywhere on patient portal +- NO provider credentials/licensing info on medical advice pages +- Health data collection with NO privacy disclosure +- Telehealth services with NO security/encryption notice +- Medical advice with NO disclaimer + +IGNORE (standard practices): +- Privacy policies in footer (standard location) +- Generic health disclaimers ("consult your doctor") +- Standard HIPAA notices in patient portals +- Links to full privacy practices +- Professional medical websites with standard layouts + +Major healthcare providers are typically compliant. Only flag ACTUALLY MISSING elements. + +Severity: +- "critical": Required HIPAA/health element missing entirely +- "warning": Element exists but not prominent + +Return JSON: [{"severity": "...", "standard": "...", "description": "...", "location": "...", "recommendation": "..."}] +If standard healthcare compliance elements exist, return: []`, + + ecommerce: `You are a QA expert testing e-commerce websites. Only report MISSING critical consumer protection elements. + +CRITICAL (must report): +- NO pricing shown on product pages +- Checkout with NO shipping cost disclosed before payment +- NO return/refund policy anywhere findable +- NO contact information (email, phone, address) +- Payment page over HTTP (not HTTPS) + +IGNORE (standard practices): +- Return policies in footer links (standard) +- Shipping costs shown at checkout (acceptable) +- Privacy/terms in footer (normal placement) +- Contact page vs contact on every page +- Modern e-commerce checkout flows + +Major retailers (Amazon, Target, etc.) set the standard. Only flag what's ACTUALLY MISSING. + +Severity: +- "critical": Required consumer protection element absent +- "warning": Hard to find but exists + +Return JSON: [{"severity": "...", "standard": "...", "description": "...", "recommendation": "..."}] +If standard e-commerce elements are present, return: []`, + }; + + return ( + prompts[industry.toLowerCase()] || + `You are a QA expert. Only report CRITICAL missing regulatory elements for this industry. +Focus on what's ACTUALLY MISSING that creates legal risk. Ignore standard industry practices. + +Return JSON: [{"severity": "...", "standard": "...", "description": "...", "recommendation": "..."}] +If no clear violations, return: []` + ); +} + +// ============================================================================ +// Compliance Checking +// ============================================================================ + +async function performComplianceChecks( + page: Page, + url: string, + visionProvider: VisionProvider, + checks: { + accessibility?: boolean; + legal?: boolean; + brand?: boolean; + regulatory?: boolean; + }, + context?: { industry?: string; brandGuidelines?: string } +): Promise { + const issues: QaIssue[] = []; + + // Scroll through page to load all lazy-loaded images + await scrollAndLoadImages(page); + + const screenshot = await page.screenshot({ fullPage: true }); + + console.log("Performing compliance checks..."); + + // Accessibility Compliance Check + if (checks.accessibility) { + console.log(" Checking accessibility (WCAG 2.1 AA)..."); + const accessibilityPrompt = `You are an accessibility QA expert. Only report ACTUAL violations that would fail WCAG 2.1 AA compliance testing. + +CRITICAL ISSUES ONLY (must report): +- Text contrast BELOW 4.5:1 (3:1 for 18pt+ text) - must be clearly unreadable +- Images with actual content but NO alt text visible (ignore decorative images, icons with adjacent text) +- Form inputs with NO label AND NO placeholder visible +- Interactive elements completely invisible or unreachable + +IGNORE these common practices (NOT violations): +- Small font sizes if readable +- Cookie banners, privacy notices (standard UX patterns) +- Decorative images, background images, icons with text labels +- Modern design patterns like cards, hero sections +- "Learn more" or "Shop now" buttons (acceptable with context) +- Hamburger menus, dropdown menus (standard patterns) + +Be VERY conservative. Only report issues that would genuinely block users with disabilities. + +Severity levels: +- "critical": Completely unusable for users with disabilities (missing alt text on content images, text completely unreadable) +- "warning": Difficult but not impossible to use (borderline contrast, missing labels with placeholders) + +Return JSON array: [{"severity": "...", "standard": "...", "description": "...", "location": "...", "recommendation": "..."}] +If NO CLEAR violations found, return: []`; + + try { + const response = await visionProvider.analyzeScreenshot(screenshot, accessibilityPrompt); + const parsed = parseAIResponse(response); + + for (const issue of parsed) { + issues.push({ + severity: issue.severity || "info", + category: "compliance", + complianceType: "accessibility", + standard: issue.standard, + description: issue.description, + page: url, + location: issue.location, + recommendation: issue.recommendation, + }); + } + + console.log(` Found ${parsed.length} accessibility issues`); + } catch (error) { + console.error(" Error in accessibility analysis:", error); + } + } + + // Legal Compliance Check + if (checks.legal) { + console.log(" Checking legal compliance..."); + const legalPrompt = `You are a legal compliance QA expert. Only report MISSING required legal elements that would create actual legal risk. + +CRITICAL ISSUES (must report): +- NO privacy policy link anywhere visible (footer, header, menu) +- NO cookie consent mechanism when cookies are clearly being used +- NO terms of service for sites collecting user data or processing transactions +- Financial/health sites with NO disclaimers or regulatory notices +- E-commerce with NO refund/return policy information + +IGNORE these common patterns (NOT violations): +- Small footer links (standard practice - "Privacy", "Terms", etc.) +- Cookie banners that appear after page load (common delay) +- Copyright notice in small footer text (completely normal) +- Privacy links not on homepage if in footer/menu +- Modern minimal footer designs (as long as legal links exist) +- "Learn more" links that lead to full policies + +Most major company websites (Apple, Google, Amazon, etc.) are compliant even if policies aren't prominently displayed. + +Only report if something is COMPLETELY MISSING or would create ACTUAL legal exposure. + +Severity: +- "critical": Required element completely missing (no privacy policy at all) +- "warning": Hard to find but exists somewhere + +Return JSON array: [{"severity": "...", "standard": "...", "description": "...", "recommendation": "..."}] +If legal elements exist (even if small), return: []`; + + try { + const response = await visionProvider.analyzeScreenshot(screenshot, legalPrompt); + const parsed = parseAIResponse(response); + + for (const issue of parsed) { + issues.push({ + severity: issue.severity || "warning", + category: "compliance", + complianceType: "legal", + standard: issue.standard, + description: issue.description, + page: url, + recommendation: issue.recommendation, + }); + } + + console.log(` Found ${parsed.length} legal compliance issues`); + } catch (error) { + console.error(" Error in legal compliance analysis:", error); + } + } + + // Brand Guidelines Check + if (checks.brand && context?.brandGuidelines) { + console.log(" Checking brand guidelines compliance..."); + const brandPrompt = `You are a brand QA expert. Check if this website violates these SPECIFIC brand guidelines: + +=== BRAND GUIDELINES TO ENFORCE === +${context.brandGuidelines} +=== END GUIDELINES === + +Your job: Find any elements that VIOLATE the guidelines listed above. + +Be specific: +- Cite WHICH guideline is being violated +- Describe HOW it's being violated +- Only flag clear violations of the stated rules + +For each violation: +- severity: "warning" (clear violation of stated guideline) | "info" (minor deviation) +- description: WHICH guideline violated and HOW (be specific) +- location: where on the page +- recommendation: how to fix to match the guideline + +Return JSON array: [{"severity": "...", "description": "...", "location": "...", "recommendation": "..."}] +If the page follows the guidelines above, return: []`; + + try { + const response = await visionProvider.analyzeScreenshot(screenshot, brandPrompt); + const parsed = parseAIResponse(response); + + for (const issue of parsed) { + issues.push({ + severity: issue.severity || "info", + category: "compliance", + complianceType: "brand", + description: issue.description, + page: url, + location: issue.location, + recommendation: issue.recommendation, + }); + } + + console.log(` Found ${parsed.length} brand guideline violations`); + } catch (error) { + console.error(" Error in brand compliance analysis:", error); + } + } + + // Regulatory Compliance (Industry-Specific) + if (checks.regulatory && context?.industry) { + console.log(` Checking ${context.industry} regulatory compliance...`); + const regulatoryPrompt = getIndustrySpecificPrompt(context.industry); + + try { + const response = await visionProvider.analyzeScreenshot(screenshot, regulatoryPrompt); + const parsed = parseAIResponse(response); + + for (const issue of parsed) { + issues.push({ + severity: issue.severity || "warning", + category: "compliance", + complianceType: "regulatory", + standard: issue.standard, + description: issue.description, + page: url, + location: issue.location, + recommendation: issue.recommendation, + }); + } + + console.log(` Found ${parsed.length} regulatory compliance issues`); + } catch (error) { + console.error(" Error in regulatory compliance analysis:", error); + } + } + + return issues; +} + +// ============================================================================ +// Policy Violation Detection +// ============================================================================ + +async function detectPolicyViolations( + page: Page, + url: string, + visionProvider: VisionProvider, + checks: { content?: boolean; security?: boolean }, + customPolicies?: string +): Promise { + const violations: QaIssue[] = []; + + // Scroll through page to load all lazy-loaded images + await scrollAndLoadImages(page); + + const screenshot = await page.screenshot({ fullPage: true }); + + console.log("Detecting policy violations..."); + + // Content Policy Violations + if (checks.content) { + console.log(" Checking content policy..."); + + let contentPrompt; + if (customPolicies && customPolicies.trim()) { + // Custom policies provided - make them the PRIMARY focus + contentPrompt = `You are a content policy enforcement expert. You must check for violations of these SPECIFIC CUSTOM POLICIES: + +=== CUSTOM POLICIES (PRIMARY CHECK) === +${customPolicies} +=== END CUSTOM POLICIES === + +Your job is to find ANY content on the page that violates the policies listed above. Be thorough and specific. + +For each violation found: +- riskLevel: "high" (clear violation of stated policy) | "medium" (borderline/unclear) | "low" (minor concern) +- description: WHICH SPECIFIC POLICY is violated and HOW +- location: where on the page + +Return JSON array: [{"riskLevel": "...", "description": "...", "location": "..."}] +If no violations of the CUSTOM POLICIES above, return: []`; + } else { + // No custom policies - use standard checks + contentPrompt = `You are a content moderation expert. Check for common policy violations (be conservative - only flag clear issues): + +Check for: +- Inappropriate or offensive content (hate speech, discriminatory language) +- Misleading claims or false advertising (unsubstantiated claims, fake testimonials) +- Unverified health/medical claims (unapproved treatments, miracle cures) +- Deceptive practices (hidden fees, fake urgency, bait-and-switch) + +For each violation found: +- riskLevel: "high" (immediate action required) | "medium" (review needed) | "low" (minor concern) +- description: what violates policy and why +- location: where on the page + +Return JSON array: [{"riskLevel": "...", "description": "...", "location": "..."}] +If no violations found, return: []`; + } + + try { + const response = await visionProvider.analyzeScreenshot(screenshot, contentPrompt); + const parsed = parseAIResponse(response); + + for (const violation of parsed) { + const severity = + violation.riskLevel === "high" + ? "critical" + : violation.riskLevel === "medium" + ? "warning" + : "info"; + + violations.push({ + severity, + category: "policy", + violationType: "content", + riskLevel: violation.riskLevel, + description: violation.description, + page: url, + location: violation.location, + }); + } + + console.log(` Found ${parsed.length} content policy violations`); + } catch (error) { + console.error(" Error in content policy analysis:", error); + } + } + + // Security Issues + if (checks.security) { + console.log(" Checking security issues..."); + const securityPrompt = `You are a security expert analyzing a website for visible security issues. + +Check for: +- Exposed personal data: Email addresses, phone numbers, or personal info in plain text where it shouldn't be +- Missing HTTPS indicators: Forms collecting sensitive data without visible security indicators +- Insecure payment displays: Credit card numbers or payment info visible +- Exposed API keys or tokens: Any visible credentials, keys, or tokens in the interface +- Weak password requirements: Password fields showing very weak requirements (e.g., "password", "123456") +- Missing security badges: Checkout or payment pages without trust indicators +- Suspicious external links: Links to unverified or suspicious domains +- Data exposure: Session IDs, user IDs, or internal data visible to users +- Insecure file uploads: Upload forms without file type restrictions visible + +For each security issue found: +- riskLevel: "high" (data exposure, immediate security risk) | "medium" (security concern, should be addressed) | "low" (best practice improvement) +- description: what the security issue is and the potential risk +- location: where the issue is visible on the page + +Return JSON array: [{"riskLevel": "...", "description": "...", "location": "..."}] +If no issues found, return: []`; + + try { + const response = await visionProvider.analyzeScreenshot(screenshot, securityPrompt); + const parsed = parseAIResponse(response); + + for (const issue of parsed) { + const severity = + issue.riskLevel === "high" + ? "critical" + : issue.riskLevel === "medium" + ? "warning" + : "info"; + + violations.push({ + severity, + category: "policy", + violationType: "security", + riskLevel: issue.riskLevel, + description: issue.description, + page: url, + location: issue.location, + }); + } + + console.log(` Found ${parsed.length} security issues`); + } catch (error) { + console.error(" Error in security analysis:", error); + } + } + + return violations; +} + +// ============================================================================ +// Visual QA Checks +// ============================================================================ + +async function performVisualChecks( + page: Page, + url: string, + visionProvider: VisionProvider +): Promise { + const issues: QaIssue[] = []; + + console.log(`Performing visual checks on ${url}...`); + + // Scroll through page to load all lazy-loaded images + await scrollAndLoadImages(page); + + // Capture full page screenshot + const screenshot = await page.screenshot({ fullPage: true }); + const screenshotBase64 = screenshot.toString("base64"); + + // Visual Analysis Prompt + const visualPrompt = `You are a UI/UX QA expert analyzing this website for visual and design issues. Report BOTH broken functionality AND poor design quality. + +CRITICAL ISSUES (completely broken): +- Broken images (404 icons, missing images) +- Text overlapping and unreadable +- Content overflowing containers causing horizontal scroll +- Buttons or links that appear broken or non-functional +- Elements positioned incorrectly (overlapping, off-screen) +- Completely broken or chaotic layouts + +WARNING ISSUES (poor design/UX): +- Excessive visual clutter or overwhelming layouts +- Very poor color choices (clashing colors, eye-straining combinations) +- Inconsistent typography (random font sizes, too many fonts) +- Poor spacing and alignment (cramped, uneven, messy) +- Confusing navigation or unclear information hierarchy +- Text readability issues (too small, poor contrast, bad line height) +- Unprofessional appearance (amateurish, outdated 1990s style) +- Too many competing visual elements +- Garish or ugly color schemes +- Layout chaos (everything everywhere, no structure) + +INFO ISSUES (minor problems): +- Small inconsistencies in spacing or alignment +- Minor typography issues +- Could benefit from better visual hierarchy + +Be honest and critical. If the website looks unprofessional, cluttered, or poorly designed, SAY SO. Don't hold back on ugly or amateurish designs. + +Severity guidelines: +- "critical": Broken functionality or completely unusable +- "warning": Poor design quality, unprofessional appearance, bad UX +- "info": Minor polish issues + +Return JSON array: +[{"severity": "...", "description": "...", "location": "..."}] + +If the page is well-designed and professional, return: [] +If the page is ugly or poorly designed, report the specific issues.`; + + try { + const response = await visionProvider.analyzeScreenshot(screenshot, visualPrompt); + + // Parse the AI response + const jsonMatch = response.match(/\[[\s\S]*\]/); + if (jsonMatch) { + const parsedIssues = JSON.parse(jsonMatch[0]); + + for (const issue of parsedIssues) { + issues.push({ + severity: issue.severity || "info", + category: "visual", + description: issue.description, + page: url, + location: issue.location, + screenshot: screenshotBase64, + }); + } + } + } catch (error) { + console.error("Error in visual analysis:", error); + issues.push({ + severity: "warning", + category: "visual", + description: `Visual analysis failed: ${error instanceof Error ? error.message : String(error)}`, + page: url, + }); + } + + return issues; +} + +// ============================================================================ +// Functional QA Checks +// ============================================================================ + +async function performFunctionalChecks( + page: Page, + url: string, + visionProvider: VisionProvider +): Promise { + const issues: QaIssue[] = []; + + console.log(`Performing functional checks on ${url}...`); + + // Check for JavaScript errors + const jsErrors: string[] = []; + page.on("pageerror", (error) => { + jsErrors.push(error.message); + }); + + // Check for console errors + const consoleErrors: string[] = []; + page.on("console", (msg) => { + if (msg.type() === "error") { + consoleErrors.push(msg.text()); + } + }); + + // Wait a moment to catch any immediate errors + await page.waitForTimeout(2000); + + // Report JavaScript errors + if (jsErrors.length > 0) { + issues.push({ + severity: "critical", + category: "functional", + description: `JavaScript errors detected: ${jsErrors.slice(0, 3).join("; ")}${jsErrors.length > 3 ? ` (and ${jsErrors.length - 3} more)` : ""}`, + page: url, + }); + } + + // Report console errors (filter out common non-critical ones) + const significantConsoleErrors = consoleErrors.filter( + (err) => !err.includes("favicon") && !err.includes("analytics") + ); + if (significantConsoleErrors.length > 0) { + issues.push({ + severity: "warning", + category: "functional", + description: `Console errors: ${significantConsoleErrors.slice(0, 2).join("; ")}`, + page: url, + }); + } + + // Check for broken images + const brokenImages = await page.evaluate(() => { + const images = Array.from(document.querySelectorAll("img")); + return images + .filter((img) => !img.complete || img.naturalHeight === 0) + .map((img) => img.src || img.alt || "unknown") + .slice(0, 5); + }); + + if (brokenImages.length > 0) { + issues.push({ + severity: "critical", + category: "functional", + description: `Broken images detected: ${brokenImages.join(", ")}`, + page: url, + }); + } + + // Scroll through page to load all lazy-loaded images + await scrollAndLoadImages(page); + + // Analyze interactive elements with AI + const screenshot = await page.screenshot({ fullPage: true }); + + const functionalPrompt = `You are a QA engineer analyzing interactive elements on a website. Look at this screenshot and identify potential functional issues. + +Check for: +1. Buttons that appear non-clickable or broken +2. Form elements that look disabled or malformed +3. Links that appear broken or improperly styled +4. Interactive elements with unclear purpose +5. Accessibility issues (missing labels, poor focus indicators) + +For each issue found, provide: +- Severity: critical, warning, or info +- Description: What's wrong and why it matters +- Location: Where the element is located + +Format as JSON array: +[ + { + "severity": "critical|warning|info", + "description": "Brief description", + "location": "Element location" + } +] + +If no issues, return: []`; + + try { + const response = await visionProvider.analyzeScreenshot(screenshot, functionalPrompt); + + const jsonMatch = response.match(/\[[\s\S]*\]/); + if (jsonMatch) { + const parsedIssues = JSON.parse(jsonMatch[0]); + + for (const issue of parsedIssues) { + issues.push({ + severity: issue.severity || "info", + category: "functional", + description: issue.description, + page: url, + location: issue.location, + }); + } + } + } catch (error) { + console.error("Error in functional analysis:", error); + } + + return issues; +} + +// ============================================================================ +// Report Generation +// ============================================================================ + +function generateJsonReport(issues: QaIssue[], metadata: { url: string; model: string; timestamp: Date }): string { + return JSON.stringify( + { + metadata: { + url: metadata.url, + model: metadata.model, + timestamp: metadata.timestamp.toISOString(), + generatedBy: "Kernel QA Agent", + }, + summary: { + totalIssues: issues.length, + critical: issues.filter((i) => i.severity === "critical").length, + warnings: issues.filter((i) => i.severity === "warning").length, + info: issues.filter((i) => i.severity === "info").length, + }, + issuesByCategory: { + visual: issues.filter((i) => i.category === "visual").length, + functional: issues.filter((i) => i.category === "functional").length, + accessibility: issues.filter((i) => i.category === "accessibility").length, + }, + issues: issues.map((issue) => ({ + severity: issue.severity, + category: issue.category, + description: issue.description, + page: issue.page, + location: issue.location, + hasScreenshot: !!issue.screenshot, + })), + }, + null, + 2 + ); +} + +function generateHtmlReport(issues: QaIssue[], metadata: { url: string; model: string; timestamp: Date }): string { + const criticalCount = issues.filter((i) => i.severity === "critical").length; + const warningCount = issues.filter((i) => i.severity === "warning").length; + const infoCount = issues.filter((i) => i.severity === "info").length; + + const issuesByCategory = { + compliance: issues.filter((i) => i.category === "compliance"), + policy: issues.filter((i) => i.category === "policy"), + visual: issues.filter((i) => i.category === "visual"), + functional: issues.filter((i) => i.category === "functional"), + accessibility: issues.filter((i) => i.category === "accessibility"), + }; + + const renderIssue = (issue: QaIssue, index: number) => ` +
+
+ ${issue.severity.toUpperCase()} + ${issue.riskLevel ? `RISK: ${issue.riskLevel.toUpperCase()}` : ''} + ${issue.standard ? `${escapeHtml(issue.standard)}` : ''} +
+

${escapeHtml(issue.description)}

+ ${issue.location ? `

Location: ${escapeHtml(issue.location)}

` : ""} + ${issue.recommendation ? `

Recommendation: ${escapeHtml(issue.recommendation)}

` : ""} + ${issue.page ? `

Page: ${escapeHtml(issue.page)}

` : ""} + ${issue.screenshot ? ` +
+ View Screenshot + Issue screenshot +
+ ` : ""} +
+ `; + + return ` + + + + + QA Report - ${escapeHtml(metadata.url)} + + + +
+

QA Report

+ + +
+
+
${issues.length}
+
Total Issues
+
+
+
${criticalCount}
+
Critical
+
+
+
${warningCount}
+
Warnings
+
+
+
${infoCount}
+
Info
+
+
+ + ${issues.length === 0 ? '
✓ No issues found!
' : ''} + + ${issuesByCategory.compliance.length > 0 ? ` +
+

Compliance Issues (${issuesByCategory.compliance.length})

+ + ${issuesByCategory.compliance.filter(i => i.complianceType === 'accessibility').length > 0 ? ` +

Accessibility Compliance

+ ${issuesByCategory.compliance.filter(i => i.complianceType === 'accessibility').map(renderIssue).join('')} + ` : ''} + + ${issuesByCategory.compliance.filter(i => i.complianceType === 'legal').length > 0 ? ` +

Legal Compliance

+ ${issuesByCategory.compliance.filter(i => i.complianceType === 'legal').map(renderIssue).join('')} + ` : ''} + + ${issuesByCategory.compliance.filter(i => i.complianceType === 'brand').length > 0 ? ` +

Brand Guidelines

+ ${issuesByCategory.compliance.filter(i => i.complianceType === 'brand').map(renderIssue).join('')} + ` : ''} + + ${issuesByCategory.compliance.filter(i => i.complianceType === 'regulatory').length > 0 ? ` +

Regulatory Compliance

+ ${issuesByCategory.compliance.filter(i => i.complianceType === 'regulatory').map(renderIssue).join('')} + ` : ''} +
+ ` : ''} + + ${issuesByCategory.policy.length > 0 ? ` +
+

Policy Violations (${issuesByCategory.policy.length})

+ + ${issuesByCategory.policy.filter(i => i.violationType === 'content').length > 0 ? ` +

Content Policy

+ ${issuesByCategory.policy.filter(i => i.violationType === 'content').map(renderIssue).join('')} + ` : ''} + + ${issuesByCategory.policy.filter(i => i.violationType === 'security').length > 0 ? ` +

Security Issues

+ ${issuesByCategory.policy.filter(i => i.violationType === 'security').map(renderIssue).join('')} + ` : ''} +
+ ` : ''} + + ${issuesByCategory.visual.length > 0 ? ` +
+

Broken UI Issues (${issuesByCategory.visual.length})

+ ${issuesByCategory.visual.map(renderIssue).join('')} +
+ ` : ''} +
+ +`; +} + +function escapeHtml(text: string): string { + const map: Record = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + }; + return text.replace(/[&<>"']/g, (m) => map[m] || m); +} + +// ============================================================================ +// Main QA Task Handler +// ============================================================================ + +async function runQaTask( + invocationId: string | undefined, + input: QaTaskInput, + progressCallback?: (step: string, message: string) => void +): Promise { + const progress = progressCallback || ((step, msg) => console.log(`[${step}] ${msg}`)); + const url = input.url; + const model = input.model || "claude"; + + console.log(`Starting QA analysis for: ${url}`); + console.log(`Model: ${model}`); + + if (input.checks?.compliance) { + console.log(`Compliance checks: Accessibility=${!!input.checks.compliance.accessibility}, Legal=${!!input.checks.compliance.legal}, Brand=${!!input.checks.compliance.brand}, Regulatory=${!!input.checks.compliance.regulatory}`); + } + if (input.checks?.policyViolations) { + console.log(`Policy checks: Content=${!!input.checks.policyViolations.content}, Security=${!!input.checks.policyViolations.security}`); + } + if (input.checks?.brokenUI) { + console.log(`UI checks: Broken UI=${!!input.checks.brokenUI}`); + } + + // Create vision provider + progress('model', `Using ${model.toUpperCase()} model for analysis`); + const visionProvider = createVisionProvider(model); + console.log(`Using ${visionProvider.name} for analysis`); + + // Create Kernel browser + progress('browser', 'Launching browser...'); + const kernelBrowser = await kernel.browsers.create({ + invocation_id: invocationId, + stealth: true, + viewport: { + width: 1440, + height: 900, + }, + }); + + console.log("Kernel browser live view url:", kernelBrowser.browser_live_view_url); + + const browser = await chromium.connectOverCDP(kernelBrowser.cdp_ws_url); + + try { + const context = browser.contexts()[0] || (await browser.newContext()); + const page = context.pages()[0] || (await context.newPage()); + + // Navigate to the target URL + progress('navigate', `Navigating to ${url}...`); + console.log(`\nNavigating to ${url}...`); + await page.goto(url, { waitUntil: "load", timeout: 60000 }); + + // Wait for page to settle and dynamic content to load + await page.waitForTimeout(3000); + console.log("Page loaded successfully"); + + // Dismiss popups if enabled (before scrolling) + if (input.dismissPopups) { + progress('popups', 'Dismissing popups and overlays...'); + console.log('\nDismissing popups and overlays...'); + await dismissPopups(page); + } + + // Scroll through page to load all lazy-loaded images + progress('scroll', 'Loading all page content...'); + console.log(`\nScrolling through page to load all images...`); + await scrollAndLoadImages(page); + + // Dismiss popups again (some appear after scroll) + if (input.dismissPopups) { + console.log('Dismissing any popups that appeared after scrolling...'); + await dismissPopups(page); + } + + const allIssues: QaIssue[] = []; + + // Compliance Checks + if (input.checks?.compliance) { + const checkTypes = Object.entries(input.checks.compliance || {}) + .filter(([_, enabled]) => enabled) + .map(([type]) => type); + if (checkTypes.length > 0) { + progress('compliance', `Running compliance checks (${checkTypes.join(', ')})...`); + } + const complianceIssues = await performComplianceChecks( + page, + url, + visionProvider, + input.checks.compliance, + input.context + ); + allIssues.push(...complianceIssues); + console.log(`\nCompliance analysis: Found ${complianceIssues.length} issues`); + } + + // Policy Violation Checks + if (input.checks?.policyViolations) { + const checkTypes = Object.entries(input.checks.policyViolations || {}) + .filter(([_, enabled]) => enabled) + .map(([type]) => type); + if (checkTypes.length > 0) { + progress('policy', `Detecting policy violations (${checkTypes.join(', ')})...`); + } + const policyViolations = await detectPolicyViolations( + page, + url, + visionProvider, + input.checks.policyViolations, + input.context?.customPolicies + ); + allIssues.push(...policyViolations); + console.log(`\nPolicy analysis: Found ${policyViolations.length} violations`); + } + + // Broken UI Check + if (input.checks?.brokenUI) { + progress('ui', 'Checking for broken UI elements...'); + const uiIssues = await performVisualChecks(page, url, visionProvider); + allIssues.push(...uiIssues); + console.log(`\nUI analysis: Found ${uiIssues.length} UI issues`); + } + + // Generate reports + progress('generating', 'Generating reports...'); + const metadata = { + url, + model: visionProvider.name, + timestamp: new Date(), + }; + + const jsonReport = generateJsonReport(allIssues, metadata); + const htmlReport = generateHtmlReport(allIssues, metadata); + + progress('complete', `Analysis complete! Found ${allIssues.length} issues.`); + console.log(`\nQA Analysis Complete!`); + console.log(`Total issues found: ${allIssues.length}`); + console.log(`- Critical: ${allIssues.filter((i) => i.severity === "critical").length}`); + console.log(`- Warnings: ${allIssues.filter((i) => i.severity === "warning").length}`); + console.log(`- Info: ${allIssues.filter((i) => i.severity === "info").length}`); + + return { + success: true, + summary: { + totalIssues: allIssues.length, + criticalIssues: allIssues.filter((i) => i.severity === "critical").length, + warnings: allIssues.filter((i) => i.severity === "warning").length, + infos: allIssues.filter((i) => i.severity === "info").length, + }, + issues: allIssues, + jsonReport, + htmlReport, + }; + } catch (error) { + console.error("QA analysis failed:", error); + throw error; + } finally { + await browser.close(); + await kernel.browsers.deleteByID(kernelBrowser.session_id); + } +} + +// ============================================================================ +// Kernel Action Registration +// ============================================================================ + +app.action( + "qa-test", + async (ctx: KernelContext, payload?: QaTaskInput): Promise => { + if (!payload?.url) { + throw new Error("URL is required"); + } + + // Normalize URL + let url = payload.url; + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = `https://${url}`; + } + + // Validate URL + try { + new URL(url); + } catch { + throw new Error(`Invalid URL: ${url}`); + } + + return runQaTask(ctx.invocation_id, { ...payload, url }); + } +); + +// ============================================================================ +// Export for UI Server +// ============================================================================ + +export default runQaTask; + +// ============================================================================ +// Local Execution Support +// ============================================================================ + +if (import.meta.url === `file://${process.argv[1]}`) { + const testUrl = process.argv[2] || "https://cash.app"; + const testModel = (process.argv[3] as "claude" | "gpt4o" | "gemini") || "claude"; + + console.log("Running QA Agent locally..."); + + runQaTask(undefined, { + url: testUrl, + model: testModel, + }) + .then((result) => { + console.log("\n" + "=".repeat(80)); + console.log("JSON REPORT:"); + console.log("=".repeat(80)); + console.log(result.jsonReport); + + console.log("\n" + "=".repeat(80)); + console.log("HTML REPORT:"); + console.log("=".repeat(80)); + console.log("HTML report generated (view in browser)"); + console.log("=".repeat(80)); + + process.exit(0); + }) + .catch((error) => { + console.error("Local execution failed:", error); + process.exit(1); + }); +} diff --git a/pkg/templates/typescript/qa-agent/package.json b/pkg/templates/typescript/qa-agent/package.json new file mode 100644 index 0000000..6496bc3 --- /dev/null +++ b/pkg/templates/typescript/qa-agent/package.json @@ -0,0 +1,26 @@ +{ + "name": "ts-qa-agent", + "module": "index.ts", + "type": "module", + "private": true, + "scripts": { + "ui": "tsx ui/server.js", + "dev": "tsx --watch ui/server.js" + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.30.1", + "@google/generative-ai": "^0.21.0", + "@onkernel/sdk": "^0.23.0", + "dotenv": "^17.2.3", + "express": "^4.22.1", + "openai": "^4.73.0", + "playwright-core": "^1.49.1", + "zod": "^4.2.0" + }, + "devDependencies": { + "@types/express": "^4.17.25", + "@types/node": "^22.15.17", + "tsx": "^4.21.0", + "typescript": "^5.9.3" + } +} diff --git a/pkg/templates/typescript/qa-agent/pnpm-lock.yaml b/pkg/templates/typescript/qa-agent/pnpm-lock.yaml new file mode 100644 index 0000000..7e248ae --- /dev/null +++ b/pkg/templates/typescript/qa-agent/pnpm-lock.yaml @@ -0,0 +1,1238 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@anthropic-ai/sdk': + specifier: ^0.30.1 + version: 0.30.1 + '@google/generative-ai': + specifier: ^0.21.0 + version: 0.21.0 + '@onkernel/sdk': + specifier: ^0.23.0 + version: 0.23.0 + dotenv: + specifier: ^17.2.3 + version: 17.2.3 + express: + specifier: ^4.22.1 + version: 4.22.1 + openai: + specifier: ^4.73.0 + version: 4.104.0(zod@4.3.5) + playwright-core: + specifier: ^1.49.1 + version: 1.57.0 + zod: + specifier: ^4.2.0 + version: 4.3.5 + devDependencies: + '@types/express': + specifier: ^4.17.25 + version: 4.17.25 + '@types/node': + specifier: ^22.15.17 + version: 22.19.7 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + +packages: + + '@anthropic-ai/sdk@0.30.1': + resolution: {integrity: sha512-nuKvp7wOIz6BFei8WrTdhmSsx5mwnArYyJgh4+vYu3V4J0Ltb8Xm3odPm51n1aSI0XxNCrDl7O88cxCtUdAkaw==} + + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@google/generative-ai@0.21.0': + resolution: {integrity: sha512-7XhUbtnlkSEZK15kN3t+tzIMxsbKm/dSkKBFalj+20NvPKe1kBY7mR2P7vuijEn+f06z5+A8bVGKO0v39cr6Wg==} + engines: {node: '>=18.0.0'} + + '@onkernel/sdk@0.23.0': + resolution: {integrity: sha512-P/ez6HU8sO2QvqWATkvC+Wdv+fgto4KfBCHLl2T6EUpoU3LhgOZ/sJP2ZRf/vh5Vh7QR2Vf05RgMaFcIGBGD9Q==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/express-serve-static-core@4.19.8': + resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} + + '@types/express@4.17.25': + resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/node-fetch@2.6.13': + resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + + '@types/node@22.19.7': + resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==} + + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@0.17.6': + resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@1.15.10': + resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + body-parser@1.20.4: + resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + express@4.22.1: + resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} + engines: {node: '>= 0.10.0'} + + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + + form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + openai@4.104.0: + resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + playwright-core@1.57.0: + resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} + engines: {node: '>=18'} + hasBin: true + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + zod@4.3.5: + resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} + +snapshots: + + '@anthropic-ai/sdk@0.30.1': + dependencies: + '@types/node': 18.19.130 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + '@esbuild/aix-ppc64@0.27.2': + optional: true + + '@esbuild/android-arm64@0.27.2': + optional: true + + '@esbuild/android-arm@0.27.2': + optional: true + + '@esbuild/android-x64@0.27.2': + optional: true + + '@esbuild/darwin-arm64@0.27.2': + optional: true + + '@esbuild/darwin-x64@0.27.2': + optional: true + + '@esbuild/freebsd-arm64@0.27.2': + optional: true + + '@esbuild/freebsd-x64@0.27.2': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + + '@esbuild/win32-x64@0.27.2': + optional: true + + '@google/generative-ai@0.21.0': {} + + '@onkernel/sdk@0.23.0': {} + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.19.7 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.19.7 + + '@types/express-serve-static-core@4.19.8': + dependencies: + '@types/node': 22.19.7 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@4.17.25': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.8 + '@types/qs': 6.14.0 + '@types/serve-static': 1.15.10 + + '@types/http-errors@2.0.5': {} + + '@types/mime@1.3.5': {} + + '@types/node-fetch@2.6.13': + dependencies: + '@types/node': 22.19.7 + form-data: 4.0.5 + + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + + '@types/node@22.19.7': + dependencies: + undici-types: 6.21.0 + + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@0.17.6': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 22.19.7 + + '@types/send@1.2.1': + dependencies: + '@types/node': 22.19.7 + + '@types/serve-static@1.15.10': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 22.19.7 + '@types/send': 0.17.6 + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + array-flatten@1.1.1: {} + + asynckit@0.4.0: {} + + body-parser@1.20.4: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.14.1 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-signature@1.0.7: {} + + cookie@0.7.2: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + + destroy@1.2.0: {} + + dotenv@17.2.3: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + encodeurl@2.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + event-target-shim@5.0.1: {} + + express@4.22.1: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.4 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2 + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.14.1 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.2 + serve-static: 1.16.3 + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + finalhandler@1.3.2: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + form-data-encoder@1.7.2: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + + forwarded@0.2.0: {} + + fresh@0.5.2: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + 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 + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + merge-descriptors@1.0.3: {} + + methods@1.1.2: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + negotiator@0.6.3: {} + + node-domexception@1.0.0: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + openai@4.104.0(zod@4.3.5): + dependencies: + '@types/node': 18.19.130 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + optionalDependencies: + zod: 4.3.5 + transitivePeerDependencies: + - encoding + + parseurl@1.3.3: {} + + path-to-regexp@0.1.12: {} + + playwright-core@1.57.0: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.14.1: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + resolve-pkg-maps@1.0.0: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + send@0.19.2: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + statuses@2.0.2: {} + + toidentifier@1.0.1: {} + + tr46@0.0.3: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.2 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + typescript@5.9.3: {} + + undici-types@5.26.5: {} + + undici-types@6.21.0: {} + + unpipe@1.0.0: {} + + utils-merge@1.0.1: {} + + vary@1.1.2: {} + + web-streams-polyfill@4.0.0-beta.3: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + zod@4.3.5: {} diff --git a/pkg/templates/typescript/qa-agent/tsconfig.json b/pkg/templates/typescript/qa-agent/tsconfig.json new file mode 100644 index 0000000..e709276 --- /dev/null +++ b/pkg/templates/typescript/qa-agent/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + }, + "include": ["./**/*.ts", "./**/*.tsx"], + "exclude": ["node_modules", "dist"] +} diff --git a/pkg/templates/typescript/qa-agent/ui/app.js b/pkg/templates/typescript/qa-agent/ui/app.js new file mode 100644 index 0000000..a750155 --- /dev/null +++ b/pkg/templates/typescript/qa-agent/ui/app.js @@ -0,0 +1,324 @@ +// Form submission handler +document.getElementById('qaForm').addEventListener('submit', async (e) => { + e.preventDefault(); + + const submitBtn = document.getElementById('submitBtn'); + const resultsPanel = document.getElementById('resultsPanel'); + const errorPanel = document.getElementById('errorPanel'); + const progressPanel = document.getElementById('progressPanel'); + const progressSteps = document.getElementById('progressSteps'); + + // Disable submit button and show loading + submitBtn.disabled = true; + submitBtn.querySelector('.btn-text').style.display = 'none'; + submitBtn.querySelector('.btn-loader').style.display = 'inline-flex'; + + // Hide previous results/errors, show progress + resultsPanel.style.display = 'none'; + errorPanel.style.display = 'none'; + progressPanel.style.display = 'block'; + progressSteps.innerHTML = '
Initializing...
'; + + try { + // Collect form data + const formData = new FormData(e.target); + const payload = buildPayload(formData); + + console.log('Sending payload:', payload); + + // Send request to server to get session ID + const response = await fetch('/api/run-qa', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Analysis failed'); + } + + const { sessionId } = await response.json(); + + // Connect to SSE for progress updates + const eventSource = new EventSource(`/api/progress/${sessionId}`); + + eventSource.onmessage = (event) => { + const data = JSON.parse(event.data); + + if (data.type === 'status') { + addProgressStep(data.step, data.message); + } else if (data.type === 'complete') { + eventSource.close(); + progressPanel.style.display = 'none'; + displayResults(data.result); + resultsPanel.style.display = 'block'; + resultsPanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + + // Re-enable submit button + submitBtn.disabled = false; + submitBtn.querySelector('.btn-text').style.display = 'inline'; + submitBtn.querySelector('.btn-loader').style.display = 'none'; + } else if (data.type === 'error') { + eventSource.close(); + throw new Error(data.error); + } + }; + + eventSource.onerror = (error) => { + console.error('SSE Error:', error); + eventSource.close(); + }; + + } catch (error) { + console.error('Error:', error); + progressPanel.style.display = 'none'; + document.getElementById('errorMessage').textContent = error.message; + errorPanel.style.display = 'block'; + errorPanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + + // Re-enable submit button + submitBtn.disabled = false; + submitBtn.querySelector('.btn-text').style.display = 'inline'; + submitBtn.querySelector('.btn-loader').style.display = 'none'; + } +}); + +// Add progress step to UI +function addProgressStep(step, message) { + const progressSteps = document.getElementById('progressSteps'); + + // Mark previous step as complete + const activeSteps = progressSteps.querySelectorAll('.progress-step.active'); + activeSteps.forEach(step => { + step.classList.remove('active'); + step.classList.add('complete'); + step.querySelector('.spinner')?.remove(); + const checkmark = document.createElement('span'); + checkmark.className = 'checkmark'; + checkmark.textContent = '✓'; + step.insertBefore(checkmark, step.firstChild); + }); + + // Add new step + const stepDiv = document.createElement('div'); + stepDiv.className = 'progress-step active'; + stepDiv.innerHTML = ` ${escapeHtml(message)}`; + progressSteps.appendChild(stepDiv); + + // Auto-scroll to bottom + progressSteps.scrollTop = progressSteps.scrollHeight; +} + +// Build payload from form data +function buildPayload(formData) { + const payload = { + url: formData.get('url'), + model: formData.get('model'), + checks: {}, + context: {}, + }; + + // Compliance checks + const compliance = {}; + if (formData.get('compliance.accessibility')) compliance.accessibility = true; + if (formData.get('compliance.legal')) compliance.legal = true; + if (formData.get('compliance.brand')) compliance.brand = true; + if (formData.get('compliance.regulatory')) compliance.regulatory = true; + if (Object.keys(compliance).length > 0) { + payload.checks.compliance = compliance; + } + + // Policy violations + const policyViolations = {}; + if (formData.get('policyViolations.content')) policyViolations.content = true; + if (formData.get('policyViolations.security')) policyViolations.security = true; + if (Object.keys(policyViolations).length > 0) { + payload.checks.policyViolations = policyViolations; + } + + // Broken UI + if (formData.get('brokenUI')) { + payload.checks.brokenUI = true; + } + + // Dismiss Popups + if (formData.get('dismissPopups')) { + payload.dismissPopups = true; + } + + // Context + const industry = formData.get('context.industry'); + if (industry) payload.context.industry = industry; + + const brandGuidelines = formData.get('context.brandGuidelines'); + if (brandGuidelines && brandGuidelines.trim()) { + payload.context.brandGuidelines = brandGuidelines.trim(); + } + + const customPolicies = formData.get('context.customPolicies'); + if (customPolicies && customPolicies.trim()) { + payload.context.customPolicies = customPolicies.trim(); + } + + return payload; +} + +// Display results +function displayResults(result) { + const container = document.getElementById('resultsContent'); + + // Create summary cards + const summary = result.summary; + const summaryHTML = ` +
+
+
${summary.totalIssues}
+
Total Issues
+
+
+
${summary.criticalIssues}
+
Critical
+
+
+
${summary.warnings}
+
Warnings
+
+
+
${summary.infos}
+
Info
+
+
+ `; + + // Group issues by category + const issues = result.issues; + const complianceIssues = issues.filter(i => i.category === 'compliance'); + const policyIssues = issues.filter(i => i.category === 'policy'); + const uiIssues = issues.filter(i => i.category === 'visual'); + + let issuesHTML = ''; + + // Compliance Issues + if (complianceIssues.length > 0) { + const byType = { + accessibility: complianceIssues.filter(i => i.complianceType === 'accessibility'), + legal: complianceIssues.filter(i => i.complianceType === 'legal'), + brand: complianceIssues.filter(i => i.complianceType === 'brand'), + regulatory: complianceIssues.filter(i => i.complianceType === 'regulatory'), + }; + + issuesHTML += '

Compliance Issues (' + complianceIssues.length + ')

'; + + if (byType.accessibility.length > 0) { + issuesHTML += '

Accessibility

'; + issuesHTML += byType.accessibility.map(renderIssue).join(''); + } + if (byType.legal.length > 0) { + issuesHTML += '

Legal

'; + issuesHTML += byType.legal.map(renderIssue).join(''); + } + if (byType.brand.length > 0) { + issuesHTML += '

Brand

'; + issuesHTML += byType.brand.map(renderIssue).join(''); + } + if (byType.regulatory.length > 0) { + issuesHTML += '

Regulatory

'; + issuesHTML += byType.regulatory.map(renderIssue).join(''); + } + + issuesHTML += '
'; + } + + // Policy Violations + if (policyIssues.length > 0) { + const byType = { + content: policyIssues.filter(i => i.violationType === 'content'), + security: policyIssues.filter(i => i.violationType === 'security'), + }; + + issuesHTML += '

Policy Violations (' + policyIssues.length + ')

'; + + if (byType.content.length > 0) { + issuesHTML += '

Content Policy

'; + issuesHTML += byType.content.map(renderIssue).join(''); + } + if (byType.security.length > 0) { + issuesHTML += '

Security

'; + issuesHTML += byType.security.map(renderIssue).join(''); + } + + issuesHTML += '
'; + } + + // UI Issues + if (uiIssues.length > 0) { + issuesHTML += '

Broken UI Issues (' + uiIssues.length + ')

'; + issuesHTML += uiIssues.map(renderIssue).join(''); + issuesHTML += '
'; + } + + if (issues.length === 0) { + issuesHTML = '
✓ No issues found! The website passed all checks.
'; + } + + container.innerHTML = summaryHTML + issuesHTML; + + // Store HTML report for export + window.currentHtmlReport = result.htmlReport; +} + +// Render individual issue +function renderIssue(issue) { + let badges = `${issue.severity.toUpperCase()}`; + + if (issue.standard) { + badges += `${escapeHtml(issue.standard)}`; + } + if (issue.riskLevel) { + badges += `RISK: ${issue.riskLevel.toUpperCase()}`; + } + + let meta = ''; + if (issue.location) { + meta += `
Location: ${escapeHtml(issue.location)}
`; + } + + let recommendation = ''; + if (issue.recommendation) { + recommendation = `
Recommendation: ${escapeHtml(issue.recommendation)}
`; + } + + return ` +
+
${badges}
+
${escapeHtml(issue.description)}
+ ${meta} + ${recommendation} +
+ `; +} + +// Export HTML report +document.getElementById('exportBtn').addEventListener('click', () => { + if (!window.currentHtmlReport) return; + + const blob = new Blob([window.currentHtmlReport], { type: 'text/html' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `qa-report-${Date.now()}.html`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +}); + +// Utility: Escape HTML +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} diff --git a/pkg/templates/typescript/qa-agent/ui/index.html b/pkg/templates/typescript/qa-agent/ui/index.html new file mode 100644 index 0000000..846fccd --- /dev/null +++ b/pkg/templates/typescript/qa-agent/ui/index.html @@ -0,0 +1,195 @@ + + + + + + + QA Agent + + + + +
+
+

🔍 QA Agent

+

AI-powered quality assurance testing using vision models

+
+ +
+
+
+ +
+

Target Website

+
+ + +
+
+ + +
+

Vision Model

+
+ +
+ + + +
+
+
+ + +
+

Compliance Checks

+
+ + + + +
+
+ + +
+

Policy Violations

+
+ + +
+
+ + +
+

Additional Checks

+
+ + +
+
+ + +
+

Context (Optional)

+
+ + +
+ +
+ + +
+ +
+ + +
+
+ + + +
+
+ + + + + + + + + +
+
+ + + + + \ No newline at end of file diff --git a/pkg/templates/typescript/qa-agent/ui/server.js b/pkg/templates/typescript/qa-agent/ui/server.js new file mode 100644 index 0000000..6764b8d --- /dev/null +++ b/pkg/templates/typescript/qa-agent/ui/server.js @@ -0,0 +1,206 @@ +import express from 'express'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; +import { chromium } from 'playwright-core'; +import { Kernel } from '@onkernel/sdk'; +import Anthropic from '@anthropic-ai/sdk'; +import OpenAI from 'openai'; +import { GoogleGenerativeAI } from '@google/generative-ai'; +import { config } from 'dotenv'; + +// Load environment variables +config(); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Middleware +app.use(express.json()); +app.use(express.static(__dirname)); + +// Import the QA agent logic (we'll need to export the functions from index.ts) +// For now, we'll duplicate the necessary code here + +// Vision Provider implementations +class ClaudeVisionProvider { + constructor(apiKey) { + this.name = "Claude (Anthropic)"; + this.client = new Anthropic({ apiKey }); + } + + async analyzeScreenshot(screenshot, prompt) { + const base64Image = screenshot.toString("base64"); + const response = await this.client.messages.create({ + model: "claude-3-5-sonnet-20241022", + max_tokens: 2048, + messages: [{ + role: "user", + content: [ + { type: "image", source: { type: "base64", media_type: "image/png", data: base64Image } }, + { type: "text", text: prompt }, + ], + }], + }); + const textContent = response.content.find((block) => block.type === "text"); + return textContent && textContent.type === "text" ? textContent.text : ""; + } +} + +class GPT4oVisionProvider { + constructor(apiKey) { + this.name = "GPT-4o (OpenAI)"; + this.client = new OpenAI({ apiKey }); + } + + async analyzeScreenshot(screenshot, prompt) { + const base64Image = screenshot.toString("base64"); + const response = await this.client.chat.completions.create({ + model: "gpt-4o", + max_tokens: 2048, + messages: [{ + role: "user", + content: [ + { type: "image_url", image_url: { url: `data:image/png;base64,${base64Image}` } }, + { type: "text", text: prompt }, + ], + }], + }); + return response.choices[0]?.message?.content || ""; + } +} + +class GeminiVisionProvider { + constructor(apiKey) { + this.name = "Gemini (Google)"; + this.client = new GoogleGenerativeAI(apiKey); + } + + async analyzeScreenshot(screenshot, prompt) { + const model = this.client.getGenerativeModel({ model: "gemini-2.0-flash-exp" }); + const result = await model.generateContent([ + { inlineData: { mimeType: "image/png", data: screenshot.toString("base64") } }, + prompt, + ]); + return result.response.text(); + } +} + +function createVisionProvider(model) { + switch (model) { + case "claude": + if (!process.env.ANTHROPIC_API_KEY) throw new Error("ANTHROPIC_API_KEY is required"); + return new ClaudeVisionProvider(process.env.ANTHROPIC_API_KEY); + case "gpt4o": + if (!process.env.OPENAI_API_KEY) throw new Error("OPENAI_API_KEY is required"); + return new GPT4oVisionProvider(process.env.OPENAI_API_KEY); + case "gemini": + if (!process.env.GOOGLE_API_KEY) throw new Error("GOOGLE_API_KEY is required"); + return new GeminiVisionProvider(process.env.GOOGLE_API_KEY); + default: + throw new Error(`Unknown model: ${model}`); + } +} + +// Store active SSE connections +const sseConnections = new Map(); + +// SSE endpoint for progress updates +app.get('/api/progress/:sessionId', (req, res) => { + const sessionId = req.params.sessionId; + + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + + // Store this connection + sseConnections.set(sessionId, res); + + // Send initial connection message + res.write(`data: ${JSON.stringify({ type: 'connected' })}\n\n`); + + // Clean up on close + req.on('close', () => { + sseConnections.delete(sessionId); + }); +}); + +// Helper to send progress updates +function sendProgress(sessionId, data) { + const connection = sseConnections.get(sessionId); + if (connection) { + connection.write(`data: ${JSON.stringify(data)}\n\n`); + } +} + +// API endpoint to run QA analysis +app.post('/api/run-qa', async (req, res) => { + const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + try { + const input = req.body; + + console.log('Received request:', JSON.stringify(input, null, 2)); + + // Validate input + if (!input.url) { + return res.status(400).json({ error: 'URL is required' }); + } + + // Send session ID immediately + res.json({ sessionId }); + + // Import and run the QA task from index.ts + const { default: runQA } = await import('../index.ts'); + + // Send progress: Starting + sendProgress(sessionId, { + type: 'status', + step: 'starting', + message: `Starting analysis of ${input.url}...` + }); + + // Create a wrapper that sends progress updates + const progressCallback = (step, message) => { + sendProgress(sessionId, { type: 'status', step, message }); + }; + + // Run the QA analysis with progress callback + const result = await runQA(undefined, input, progressCallback); + + // Send final result + sendProgress(sessionId, { + type: 'complete', + result + }); + + } catch (error) { + console.error('Error running QA:', error); + sendProgress(sessionId, { + type: 'error', + error: error.message || 'Failed to run QA analysis' + }); + } finally { + // Close SSE connection after a delay + setTimeout(() => { + const connection = sseConnections.get(sessionId); + if (connection) { + connection.end(); + sseConnections.delete(sessionId); + } + }, 1000); + } +}); + +// Health check endpoint +app.get('/api/health', (req, res) => { + res.json({ status: 'ok', message: 'QA Agent UI Server is running' }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`\n🚀 QA Agent UI Server running at http://localhost:${PORT}`); + console.log(`📊 Open http://localhost:${PORT} in your browser to use the QA Agent\n`); +}); diff --git a/pkg/templates/typescript/qa-agent/ui/styles.css b/pkg/templates/typescript/qa-agent/ui/styles.css new file mode 100644 index 0000000..aef962a --- /dev/null +++ b/pkg/templates/typescript/qa-agent/ui/styles.css @@ -0,0 +1,470 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + padding: 20px; + line-height: 1.6; +} + +.container { + max-width: 1400px; + margin: 0 auto; +} + +header { + text-align: center; + color: white; + margin-bottom: 30px; +} + +header h1 { + font-size: 2.5rem; + margin-bottom: 10px; + font-weight: 700; +} + +.subtitle { + font-size: 1.1rem; + opacity: 0.9; +} + +.content { + display: grid; + grid-template-columns: 500px 1fr; + gap: 20px; + align-items: start; +} + +@media (max-width: 1200px) { + .content { + grid-template-columns: 1fr; + } +} + +.form-panel { + background: white; + border-radius: 12px; + padding: 30px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); +} + +.form-section { + margin-bottom: 30px; +} + +.form-section h2 { + font-size: 1.2rem; + color: #2c3e50; + margin-bottom: 15px; + padding-bottom: 8px; + border-bottom: 2px solid #ecf0f1; +} + +.form-group { + margin-bottom: 15px; +} + +.form-group label { + display: block; + font-weight: 600; + color: #34495e; + margin-bottom: 8px; + font-size: 0.95rem; +} + +.form-group input[type="url"], +.form-group select, +.form-group textarea { + width: 100%; + padding: 12px; + border: 2px solid #e0e0e0; + border-radius: 8px; + font-size: 1rem; + font-family: inherit; + transition: border-color 0.3s; +} + +.form-group input[type="url"]:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: #667eea; +} + +.form-group textarea { + resize: vertical; +} + +.radio-group { + display: flex; + flex-direction: column; + gap: 10px; +} + +.radio-label { + display: flex; + align-items: center; + padding: 12px; + border: 2px solid #e0e0e0; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s; +} + +.radio-label:hover { + border-color: #667eea; + background: #f8f9ff; +} + +.radio-label input[type="radio"] { + margin-right: 10px; + cursor: pointer; +} + +.radio-label input[type="radio"]:checked + span { + color: #667eea; + font-weight: 600; +} + +.checkbox-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; +} + +@media (max-width: 600px) { + .checkbox-grid { + grid-template-columns: 1fr; + } +} + +.checkbox-label { + display: flex; + align-items: start; + padding: 15px; + border: 2px solid #e0e0e0; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s; +} + +.checkbox-label:hover { + border-color: #667eea; + background: #f8f9ff; +} + +.checkbox-label input[type="checkbox"] { + margin-right: 12px; + margin-top: 4px; + cursor: pointer; +} + +.checkbox-content { + display: flex; + flex-direction: column; + gap: 4px; +} + +.checkbox-content strong { + color: #2c3e50; + font-size: 0.95rem; +} + +.checkbox-content span { + color: #7f8c8d; + font-size: 0.85rem; +} + +.checkbox-label input[type="checkbox"]:checked ~ .checkbox-content strong { + color: #667eea; +} + +.submit-btn { + width: 100%; + padding: 16px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 8px; + font-size: 1.1rem; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); +} + +.submit-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6); +} + +.submit-btn:active { + transform: translateY(0); +} + +.submit-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.spinner { + display: inline-block; + width: 16px; + height: 16px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: white; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.progress-panel, +.results-panel, +.error-panel { + background: white; + border-radius: 12px; + padding: 30px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); +} + +.results-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 2px solid #ecf0f1; +} + +.results-header h2 { + font-size: 1.5rem; + color: #2c3e50; +} + +.export-btn { + padding: 10px 20px; + background: #27ae60; + color: white; + border: none; + border-radius: 6px; + font-weight: 600; + cursor: pointer; + transition: background 0.3s; +} + +.export-btn:hover { + background: #229954; +} + +#resultsContent { + color: #2c3e50; +} + +.error-panel { + background: #fff5f5; + border: 2px solid #e74c3c; +} + +.error-panel h3 { + color: #c0392b; + margin-bottom: 10px; +} + +.error-panel p { + color: #e74c3c; +} + +/* Summary Cards */ +.summary-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; + margin-bottom: 30px; +} + +.summary-card { + padding: 20px; + border-radius: 8px; + text-align: center; + color: white; +} + +.summary-card.total { background: linear-gradient(135deg, #3498db, #2980b9); } +.summary-card.critical { background: linear-gradient(135deg, #e74c3c, #c0392b); } +.summary-card.warning { background: linear-gradient(135deg, #f39c12, #d68910); } +.summary-card.info { background: linear-gradient(135deg, #1abc9c, #16a085); } + +.summary-card .number { + font-size: 2.5rem; + font-weight: bold; + margin-bottom: 5px; +} + +.summary-card .label { + font-size: 0.9rem; + opacity: 0.9; + text-transform: uppercase; + letter-spacing: 1px; +} + +/* Issue List */ +.issue-section { + margin-bottom: 30px; +} + +.issue-section h3 { + color: #2c3e50; + font-size: 1.3rem; + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 2px solid #ecf0f1; +} + +.issue-item { + background: #f8f9fa; + border-left: 4px solid #3498db; + padding: 15px; + margin-bottom: 12px; + border-radius: 6px; +} + +.issue-item.critical { + border-left-color: #e74c3c; + background: #fef5f5; +} + +.issue-item.warning { + border-left-color: #f39c12; + background: #fffbf5; +} + +.issue-item.info { + border-left-color: #1abc9c; + background: #f0fffe; +} + +.issue-badges { + display: flex; + gap: 8px; + margin-bottom: 10px; + flex-wrap: wrap; +} + +.badge { + display: inline-block; + padding: 4px 10px; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.badge.critical { background: #e74c3c; color: white; } +.badge.warning { background: #f39c12; color: white; } +.badge.info { background: #1abc9c; color: white; } +.badge.standard { background: #95a5a6; color: white; } +.badge.risk { background: #e67e22; color: white; } + +.issue-description { + font-size: 1rem; + color: #2c3e50; + margin-bottom: 8px; + line-height: 1.5; +} + +.issue-meta { + font-size: 0.9rem; + color: #7f8c8d; + margin-top: 8px; +} + +.issue-meta strong { + color: #34495e; +} + +.recommendation { + margin-top: 10px; + padding: 10px; + background: #e8f8f5; + border-left: 3px solid #27ae60; + border-radius: 4px; + font-size: 0.9rem; + color: #16a085; +} + +.recommendation strong { + color: #27ae60; +} + +/* Progress Panel */ +.progress-panel { + background: white; +} + +.progress-header { + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 2px solid #ecf0f1; +} + +.progress-header h2 { + font-size: 1.5rem; + color: #2c3e50; + margin: 0; +} + +.progress-steps { + max-height: 400px; + overflow-y: auto; +} + +.progress-step { + display: flex; + align-items: center; + padding: 12px 15px; + margin-bottom: 8px; + border-radius: 8px; + font-size: 0.95rem; + transition: all 0.3s; +} + +.progress-step.active { + background: #f0f7ff; + border-left: 3px solid #667eea; + color: #2c3e50; + font-weight: 500; +} + +.progress-step.complete { + background: #f0fdf4; + border-left: 3px solid #27ae60; + color: #6b7280; +} + +.progress-step .spinner { + margin-right: 12px; +} + +.progress-step .checkmark { + display: inline-block; + width: 20px; + height: 20px; + margin-right: 12px; + background: #27ae60; + color: white; + border-radius: 50%; + text-align: center; + line-height: 20px; + font-size: 14px; + flex-shrink: 0; +}