qai-cli 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,132 @@
1
+ const AnthropicProvider = require('./anthropic');
2
+ const OpenAIProvider = require('./openai');
3
+ const GeminiProvider = require('./gemini');
4
+ const OllamaProvider = require('./ollama');
5
+
6
+ const PROVIDERS = {
7
+ anthropic: AnthropicProvider,
8
+ openai: OpenAIProvider,
9
+ codex: OpenAIProvider, // Codex uses OpenAI API
10
+ gemini: GeminiProvider,
11
+ ollama: OllamaProvider,
12
+ };
13
+
14
+ /**
15
+ * Auto-detect provider from environment variables
16
+ * Supports both standard env vars (ANTHROPIC_API_KEY) and GitHub Actions format (INPUT_ANTHROPIC_API_KEY)
17
+ * @returns {{ provider: string, apiKey: string, options: Object } | null}
18
+ */
19
+ function detectProvider() {
20
+ const env = process.env;
21
+
22
+ // Check for explicit provider + api_key (both formats)
23
+ const provider = env.PROVIDER || env.INPUT_PROVIDER;
24
+ const apiKey = env.API_KEY || env.INPUT_API_KEY;
25
+ if (provider && apiKey) {
26
+ return {
27
+ provider: provider.toLowerCase(),
28
+ apiKey: apiKey,
29
+ options: {},
30
+ };
31
+ }
32
+
33
+ // Check for provider-specific keys (standard format first, then INPUT_ format)
34
+ const anthropicKey = env.ANTHROPIC_API_KEY || env.INPUT_ANTHROPIC_API_KEY;
35
+ if (anthropicKey) {
36
+ return {
37
+ provider: 'anthropic',
38
+ apiKey: anthropicKey,
39
+ options: {},
40
+ };
41
+ }
42
+
43
+ const openaiKey = env.OPENAI_API_KEY || env.INPUT_OPENAI_API_KEY;
44
+ if (openaiKey) {
45
+ return {
46
+ provider: 'openai',
47
+ apiKey: openaiKey,
48
+ options: {},
49
+ };
50
+ }
51
+
52
+ const codexKey = env.CODEX_API_KEY || env.INPUT_CODEX_API_KEY;
53
+ if (codexKey) {
54
+ return {
55
+ provider: 'codex',
56
+ apiKey: codexKey,
57
+ options: { model: 'codex-mini-latest' },
58
+ };
59
+ }
60
+
61
+ const geminiKey = env.GEMINI_API_KEY || env.INPUT_GEMINI_API_KEY;
62
+ if (geminiKey) {
63
+ return {
64
+ provider: 'gemini',
65
+ apiKey: geminiKey,
66
+ options: {},
67
+ };
68
+ }
69
+
70
+ // Ollama doesn't need an API key
71
+ if (provider === 'ollama') {
72
+ return {
73
+ provider: 'ollama',
74
+ apiKey: null,
75
+ options: {
76
+ baseUrl: env.OLLAMA_BASE_URL || env.INPUT_OLLAMA_BASE_URL || 'http://localhost:11434',
77
+ model: env.OLLAMA_MODEL || env.INPUT_OLLAMA_MODEL || 'llava',
78
+ },
79
+ };
80
+ }
81
+
82
+ return null;
83
+ }
84
+
85
+ /**
86
+ * Create a provider instance
87
+ * @param {string} providerName - Provider name
88
+ * @param {string} apiKey - API key
89
+ * @param {Object} options - Provider options
90
+ * @returns {BaseProvider}
91
+ */
92
+ function createProvider(providerName, apiKey, options = {}) {
93
+ const ProviderClass = PROVIDERS[providerName.toLowerCase()];
94
+
95
+ if (!ProviderClass) {
96
+ throw new Error(
97
+ `Unknown provider: ${providerName}. Supported: ${Object.keys(PROVIDERS).join(', ')}`,
98
+ );
99
+ }
100
+
101
+ return new ProviderClass(apiKey, options);
102
+ }
103
+
104
+ /**
105
+ * Get provider from environment (auto-detect or explicit)
106
+ * @returns {BaseProvider}
107
+ */
108
+ function getProvider() {
109
+ const detected = detectProvider();
110
+
111
+ if (!detected) {
112
+ throw new Error(
113
+ 'No API key provided. Set one of: ' +
114
+ 'ANTHROPIC_API_KEY, OPENAI_API_KEY, CODEX_API_KEY, GEMINI_API_KEY, ' +
115
+ 'or PROVIDER with API_KEY',
116
+ );
117
+ }
118
+
119
+ console.log(`Using provider: ${detected.provider}`);
120
+ return createProvider(detected.provider, detected.apiKey, detected.options);
121
+ }
122
+
123
+ module.exports = {
124
+ PROVIDERS,
125
+ detectProvider,
126
+ createProvider,
127
+ getProvider,
128
+ AnthropicProvider,
129
+ OpenAIProvider,
130
+ GeminiProvider,
131
+ OllamaProvider,
132
+ };
@@ -0,0 +1,49 @@
1
+ const BaseProvider = require('./base');
2
+
3
+ class OllamaProvider extends BaseProvider {
4
+ constructor(apiKey, options = {}) {
5
+ super(apiKey, options);
6
+ this.baseUrl = options.baseUrl || 'http://localhost:11434';
7
+ this.model = options.model || 'llava';
8
+ }
9
+
10
+ async analyze(captureData, options = {}) {
11
+ const prompt = this.buildPrompt(captureData, options);
12
+
13
+ // Ollama expects images as base64 strings in the images array
14
+ const images = captureData.screenshots.map((s) => s.buffer.toString('base64'));
15
+
16
+ // Build the full prompt with screenshot descriptions
17
+ let fullPrompt = '';
18
+ for (const screenshot of captureData.screenshots) {
19
+ const { viewport, width, height } = screenshot;
20
+ fullPrompt += `[Screenshot: ${viewport} - ${width}x${height}]\n`;
21
+ }
22
+ fullPrompt += '\n' + prompt;
23
+
24
+ const response = await fetch(`${this.baseUrl}/api/generate`, {
25
+ method: 'POST',
26
+ headers: {
27
+ 'Content-Type': 'application/json',
28
+ },
29
+ body: JSON.stringify({
30
+ model: this.model,
31
+ prompt: fullPrompt,
32
+ images,
33
+ stream: false,
34
+ options: {
35
+ temperature: 0.1,
36
+ },
37
+ }),
38
+ });
39
+
40
+ if (!response.ok) {
41
+ throw new Error(`Ollama request failed: ${response.status} ${response.statusText}`);
42
+ }
43
+
44
+ const data = await response.json();
45
+ return this.parseResponse(data.response || '');
46
+ }
47
+ }
48
+
49
+ module.exports = OllamaProvider;
@@ -0,0 +1,54 @@
1
+ const OpenAI = require('openai');
2
+ const BaseProvider = require('./base');
3
+
4
+ class OpenAIProvider extends BaseProvider {
5
+ constructor(apiKey, options = {}) {
6
+ super(apiKey, options);
7
+ this.client = new OpenAI({ apiKey });
8
+ this.model = options.model || 'gpt-4o';
9
+ }
10
+
11
+ async analyze(captureData, options = {}) {
12
+ const prompt = this.buildPrompt(captureData, options);
13
+
14
+ // Build content array with images
15
+ const content = [];
16
+
17
+ // Add screenshots as images
18
+ for (const screenshot of captureData.screenshots) {
19
+ content.push({
20
+ type: 'image_url',
21
+ image_url: {
22
+ url: `data:image/png;base64,${screenshot.buffer.toString('base64')}`,
23
+ detail: 'high',
24
+ },
25
+ });
26
+ content.push({
27
+ type: 'text',
28
+ text: `[Screenshot: ${screenshot.viewport} - ${screenshot.width}x${screenshot.height}]`,
29
+ });
30
+ }
31
+
32
+ // Add the analysis prompt
33
+ content.push({
34
+ type: 'text',
35
+ text: prompt,
36
+ });
37
+
38
+ const response = await this.client.chat.completions.create({
39
+ model: this.model,
40
+ max_tokens: 4096,
41
+ messages: [
42
+ {
43
+ role: 'user',
44
+ content,
45
+ },
46
+ ],
47
+ });
48
+
49
+ const responseText = response.choices[0]?.message?.content || '';
50
+ return this.parseResponse(responseText);
51
+ }
52
+ }
53
+
54
+ module.exports = OpenAIProvider;
package/src/types.d.ts ADDED
@@ -0,0 +1,148 @@
1
+ import type { Page, TestInfo } from '@playwright/test';
2
+
3
+ export interface AnalysisOptions {
4
+ /** Viewports to test (default: ['desktop', 'mobile']) */
5
+ viewports?: ('mobile' | 'tablet' | 'desktop')[];
6
+ /** Focus area for analysis */
7
+ focus?: 'all' | 'accessibility' | 'performance' | 'forms' | 'visual';
8
+ /** LLM provider to use */
9
+ provider?: 'anthropic' | 'openai' | 'gemini' | 'ollama' | 'codex';
10
+ /** API key (uses env var if not provided) */
11
+ apiKey?: string;
12
+ }
13
+
14
+ export interface Bug {
15
+ /** Bug title */
16
+ title: string;
17
+ /** Detailed description */
18
+ description: string;
19
+ /** Severity level */
20
+ severity: 'critical' | 'high' | 'medium' | 'low';
21
+ /** Bug category */
22
+ category: string;
23
+ /** Viewport where bug was found */
24
+ viewport?: string;
25
+ /** How to fix the bug */
26
+ recommendation?: string;
27
+ }
28
+
29
+ export interface Screenshot {
30
+ /** Screenshot name (e.g., "desktop-1920x1080") */
31
+ name: string;
32
+ /** Viewport name */
33
+ viewport: string;
34
+ /** Viewport width */
35
+ width: number;
36
+ /** Viewport height */
37
+ height: number;
38
+ /** Raw screenshot buffer */
39
+ buffer: Buffer;
40
+ /** Base64 encoded screenshot */
41
+ base64: string;
42
+ }
43
+
44
+ export interface NetworkError {
45
+ /** Request URL */
46
+ url: string;
47
+ /** HTTP method */
48
+ method: string;
49
+ /** HTTP status code (for response errors) */
50
+ status?: number;
51
+ /** Failure reason (for request failures) */
52
+ failure?: string;
53
+ }
54
+
55
+ export interface AnalysisReport {
56
+ /** Page URL */
57
+ url: string;
58
+ /** Page title */
59
+ title: string;
60
+ /** ISO timestamp */
61
+ timestamp: string;
62
+ /** Analysis duration */
63
+ duration: string;
64
+ /** QA score (0-100) */
65
+ score: number | null;
66
+ /** AI-generated summary */
67
+ summary: string;
68
+ /** All bugs found */
69
+ bugs: Bug[];
70
+ /** Only critical/high severity bugs (convenience property) */
71
+ criticalBugs: Bug[];
72
+ /** AI recommendations */
73
+ recommendations: string[];
74
+ /** Console errors captured */
75
+ consoleErrors: string[];
76
+ /** Network errors captured */
77
+ networkErrors: NetworkError[];
78
+ /** Screenshots taken */
79
+ screenshots: Screenshot[];
80
+ /** Viewports tested */
81
+ viewports: string[];
82
+ /** Focus area used */
83
+ focus: string;
84
+ }
85
+
86
+ export interface ViewportConfig {
87
+ width: number;
88
+ height: number;
89
+ name: string;
90
+ }
91
+
92
+ /**
93
+ * Analyze a page with AI
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * import { test, expect } from '@playwright/test';
98
+ * import { analyzeWithAI } from 'qaie';
99
+ *
100
+ * test('AI QA: homepage', async ({ page }) => {
101
+ * await page.goto('/');
102
+ * const report = await analyzeWithAI(page);
103
+ * expect(report.criticalBugs).toHaveLength(0);
104
+ * });
105
+ * ```
106
+ */
107
+ export function analyzeWithAI(page: Page, options?: AnalysisOptions): Promise<AnalysisReport>;
108
+
109
+ /**
110
+ * Create a configured analyzer with default options
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * const analyze = createAnalyzer({ viewports: ['desktop', 'mobile', 'tablet'] });
115
+ *
116
+ * test('homepage', async ({ page }) => {
117
+ * await page.goto('/');
118
+ * const report = await analyze(page);
119
+ * });
120
+ * ```
121
+ */
122
+ export function createAnalyzer(
123
+ defaultOptions?: AnalysisOptions,
124
+ ): (page: Page, options?: AnalysisOptions) => Promise<AnalysisReport>;
125
+
126
+ /**
127
+ * Attach all screenshots from a report to the Playwright test
128
+ *
129
+ * @example
130
+ * ```typescript
131
+ * test('AI QA', async ({ page }, testInfo) => {
132
+ * await page.goto('/');
133
+ * const report = await analyzeWithAI(page);
134
+ * await attachScreenshots(testInfo, report);
135
+ * });
136
+ * ```
137
+ */
138
+ export function attachScreenshots(testInfo: TestInfo, report: AnalysisReport): Promise<void>;
139
+
140
+ /**
141
+ * Attach the bug report as a JSON attachment
142
+ */
143
+ export function attachBugReport(testInfo: TestInfo, report: AnalysisReport): Promise<void>;
144
+
145
+ /**
146
+ * Viewport configurations
147
+ */
148
+ export const VIEWPORT_CONFIGS: Record<string, ViewportConfig>;