wcag-a11y 0.3.6 → 0.4.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.
package/README.md CHANGED
@@ -56,7 +56,7 @@ Generating AI fixes for 7 violations...
56
56
 
57
57
  The **prompt** at the end of each fix is what you copy into Cursor, Copilot, or Claude. It includes the affected selectors, the WCAG rule, and exactly what needs to change — no rewriting needed.
58
58
 
59
- With `--report`, the full output is also saved to `a11y-report.md`.
59
+ The full output is saved to `a11y-report.md` by default. Pass `--no-report` to skip.
60
60
 
61
61
  ---
62
62
 
@@ -69,17 +69,22 @@ npm install -g wcag-a11y
69
69
  ## Setup
70
70
 
71
71
  ```bash
72
- wcag-a11y init # Gemini (free, default)
73
- wcag-a11y init --provider openai # OpenAI
74
- wcag-a11y init --provider ollama # Local — no API key needed
72
+ wcag-a11y init # Gemini (free, default)
73
+ wcag-a11y init --provider openai # OpenAI
74
+ wcag-a11y init --provider anthropic # Anthropic Claude
75
+ wcag-a11y init --provider mistral # Mistral
76
+ wcag-a11y init --provider groq # Groq (fast inference)
77
+ wcag-a11y init --provider cohere # Cohere
78
+ wcag-a11y init --provider xai # xAI Grok
79
+ wcag-a11y init --provider deepseek # DeepSeek
80
+ wcag-a11y init --provider together # Together AI (open-source models)
81
+ wcag-a11y init --provider perplexity # Perplexity
82
+ wcag-a11y init --provider azure-openai # Azure OpenAI
83
+ wcag-a11y init --provider ollama # Local — no API key needed
75
84
  ```
76
85
 
77
86
  Each command creates an `a11y.config.json` pre-wired for that provider. Fill in your API key, then scan.
78
87
 
79
- **Get a free Gemini key:** https://aistudio.google.com
80
- **Get an OpenAI key:** https://platform.openai.com/api-keys
81
- **Ollama (local):** install from https://ollama.com, then run `ollama serve`
82
-
83
88
  ---
84
89
 
85
90
  ## Commands
@@ -91,13 +96,13 @@ Scan a built-in page with 10 intentional violations. No dev server or config req
91
96
  ```bash
92
97
  wcag-a11y demo # violations + AI fixes (default, requires config)
93
98
  wcag-a11y demo --no-ai # violations only, no AI — faster
94
- wcag-a11y demo --report # + save a11y-report.md
99
+ wcag-a11y demo --no-report # skip saving a11y-report.md
95
100
  ```
96
101
 
97
102
  | Flag | Description |
98
103
  |---|---|
99
104
  | `--no-ai` | Skip AI fix generation — prints violations only, no prompts |
100
- | `-r, --report` | Save the full report to `a11y-report.md` in the current directory |
105
+ | `--no-report` | Skip saving report to `a11y-report.md` (report is saved by default) |
101
106
 
102
107
  ---
103
108
 
@@ -113,7 +118,7 @@ wcag-a11y init --provider ollama # Ollama (local)
113
118
 
114
119
  | Flag | Description |
115
120
  |---|---|
116
- | `--provider <name>` | Which provider to configure: `gemini` (default), `openai`, or `ollama`. Determines which fields are written to the config file. |
121
+ | `--provider <name>` | Which provider to configure. Valid values: `gemini` (default), `openai`, `anthropic`, `mistral`, `groq`, `cohere`, `xai`, `deepseek`, `together`, `perplexity`, `azure-openai`, `ollama`. Determines which fields are written to the config file. |
117
122
 
118
123
  ---
119
124
 
@@ -124,8 +129,9 @@ Scan a running dev server for accessibility violations.
124
129
  ```bash
125
130
  wcag-a11y scan -u http://localhost:3000
126
131
  wcag-a11y scan -u http://localhost:3000 --pages / /about /contact
127
- wcag-a11y scan -u http://localhost:3000 --crawl --report
132
+ wcag-a11y scan -u http://localhost:3000 --crawl
128
133
  wcag-a11y scan -u http://localhost:3000 --no-ai --ci
134
+ wcag-a11y scan -u http://localhost:3000 --terminal --fast-mode
129
135
  ```
130
136
 
131
137
  | Flag | Default | Description |
@@ -133,12 +139,14 @@ wcag-a11y scan -u http://localhost:3000 --no-ai --ci
133
139
  | `-u, --url <url>` | required | Base URL of your running dev server |
134
140
  | `-p, --pages <pages...>` | `/` | One or more paths to scan. Separate with spaces: `--pages / /about /contact` |
135
141
  | `-c, --crawl` | off | Follow same-origin links and scan all reachable pages automatically |
136
- | `-r, --report` | off | Save the full scan output to `a11y-report.md` |
142
+ | `--no-report` | on | Skip saving scan output to `a11y-report.md` (report is saved by default) |
137
143
  | `--no-ai` | on | Skip AI fix generation — scan runs faster and prints violations only |
138
- | `--no-explain` | off | Print only the ready-to-paste prompt for each fix, without the AI explanation |
144
+ | `--no-explain` | on | Print only the ready-to-paste prompt for each fix, without the AI explanation |
145
+ | `--terminal` | off | Print violations summary and AI fix prompts to terminal |
146
+ | `--fast-mode` | off | Output only AI fix prompts — no summaries, explanations, or progress messages |
139
147
  | `--group <strategy>` | `rule` | `rule` (default) groups all violations of the same type into one fix prompt. `none` produces a separate prompt per element. Use `none` when violations of the same rule need different fixes |
140
148
  | `--ci` | off | Exit with code `1` if any violations are found. Use this to fail a CI pipeline |
141
- | `--provider <name>` | from config | Override the AI provider for this run: `gemini`, `openai`, or `ollama`. Does not modify the config file |
149
+ | `--provider <name>` | from config | Override the AI provider for this run. See [AI Providers](#ai-providers) for valid names. Does not modify the config file |
142
150
 
143
151
  ---
144
152
 
@@ -195,33 +203,46 @@ src/components/Navbar.jsx — 2 violation(s)
195
203
  | `-c, --crawl` | off | Auto-discover pages by following same-origin links |
196
204
  | `--from-report [path]` | `a11y-report.md` | Load violations from an existing report instead of scanning. Useful when you already ran `scan --report` and just want to apply fixes |
197
205
  | `--apply` | off | Write patched files to disk (dry-run without this flag) |
198
- | `--provider <name>` | from config | Override AI provider for this run: `gemini`, `openai`, or `ollama` |
206
+ | `--provider <name>` | from config | Override AI provider for this run. See [AI Providers](#ai-providers) for valid names |
199
207
 
200
208
  > **Tip:** Always run without `--apply` first to review the diff. The dry-run is safe — nothing is written to disk.
201
209
 
202
210
  **Common workflow:** run `scan --report` to generate a report for review, then run `fix --from-report --apply` to patch the files — no second browser crawl needed.
203
211
 
204
212
  ```bash
205
- wcag-a11y scan -u http://localhost:3000 --report # review a11y-report.md
206
- wcag-a11y fix --from-report --apply # patch files from that report
213
+ wcag-a11y scan -u http://localhost:3000 # generates a11y-report.md automatically
214
+ wcag-a11y fix --from-report --apply # patch files from that report
207
215
  ```
208
216
 
209
217
  ---
210
218
 
211
219
  ## AI Providers
212
220
 
213
- | Provider | Model | Cost | API Key |
214
- |---|---|---|---|
215
- | `gemini` (default) | `gemini-2.5-flash` | Free tier | [aistudio.google.com](https://aistudio.google.com) |
216
- | `openai` | `gpt-4o-mini` | Pay-per-use | [platform.openai.com](https://platform.openai.com/api-keys) |
217
- | `ollama` | `llama3` | Free (local) | None — run `ollama serve` |
221
+ 12 providers are supported. Set your provider in `a11y.config.json` or override per-run with `--provider <name>`.
218
222
 
219
- Set your provider in `a11y.config.json` or override it per-run with `--provider`. If the AI response is unparseable, the tool generates a fix prompt directly from the violation data so you always get something actionable.
223
+ | Provider | `--provider` name | Default model | API key source |
224
+ |---|---|---|---|
225
+ | Google Gemini | `gemini` *(default)* | `gemini-2.5-flash` | [aistudio.google.com](https://aistudio.google.com) — free tier |
226
+ | OpenAI | `openai` | `gpt-4o-mini` | [platform.openai.com/api-keys](https://platform.openai.com/api-keys) |
227
+ | Anthropic | `anthropic` | `claude-sonnet-4-6` | [console.anthropic.com](https://console.anthropic.com) |
228
+ | Mistral | `mistral` | `mistral-large-latest` | [console.mistral.ai](https://console.mistral.ai) |
229
+ | Groq | `groq` | `llama-3.3-70b-versatile` | [console.groq.com](https://console.groq.com) |
230
+ | Cohere | `cohere` | `command-r-plus` | [dashboard.cohere.com](https://dashboard.cohere.com) |
231
+ | xAI | `xai` | `grok-2` | [console.x.ai](https://console.x.ai) |
232
+ | DeepSeek | `deepseek` | `deepseek-chat` | [platform.deepseek.com](https://platform.deepseek.com) |
233
+ | Together AI | `together` | `meta-llama/Llama-3-70b-chat-hf` | [api.together.xyz](https://api.together.xyz) |
234
+ | Perplexity | `perplexity` | `llama-3.1-sonar-large-128k-online` | [perplexity.ai/settings/api](https://www.perplexity.ai/settings/api) |
235
+ | Azure OpenAI | `azure-openai` | *(your deployment)* | [portal.azure.com](https://portal.azure.com) |
236
+ | Ollama | `ollama` | `llama3` | None — run `ollama serve` locally |
237
+
238
+ All models are configurable. Change the model field in `a11y.config.json` to use any model your API key has access to. If the AI response is unparseable, the tool generates a fix prompt directly from the violation data so you always get something actionable.
220
239
 
221
240
  ---
222
241
 
223
242
  ## Config (`a11y.config.json`)
224
243
 
244
+ Only the fields for your active `provider` are required. This file is gitignored by default.
245
+
225
246
  ```json
226
247
  {
227
248
  "provider": "gemini",
@@ -232,13 +253,40 @@ Set your provider in `a11y.config.json` or override it per-run with `--provider`
232
253
  "openaiApiKey": "YOUR_OPENAI_API_KEY",
233
254
  "openaiModel": "gpt-4o-mini",
234
255
 
256
+ "anthropicApiKey": "YOUR_ANTHROPIC_API_KEY",
257
+ "anthropicModel": "claude-sonnet-4-6",
258
+
259
+ "mistralApiKey": "YOUR_MISTRAL_API_KEY",
260
+ "mistralModel": "mistral-large-latest",
261
+
262
+ "groqApiKey": "YOUR_GROQ_API_KEY",
263
+ "groqModel": "llama-3.3-70b-versatile",
264
+
265
+ "cohereApiKey": "YOUR_COHERE_API_KEY",
266
+ "cohereModel": "command-r-plus",
267
+
268
+ "xaiApiKey": "YOUR_XAI_API_KEY",
269
+ "xaiModel": "grok-2",
270
+
271
+ "deepseekApiKey": "YOUR_DEEPSEEK_API_KEY",
272
+ "deepseekModel": "deepseek-chat",
273
+
274
+ "togetherApiKey": "YOUR_TOGETHER_API_KEY",
275
+ "togetherModel": "meta-llama/Llama-3-70b-chat-hf",
276
+
277
+ "perplexityApiKey": "YOUR_PERPLEXITY_API_KEY",
278
+ "perplexityModel": "llama-3.1-sonar-large-128k-online",
279
+
280
+ "azureOpenaiApiKey": "YOUR_AZURE_KEY",
281
+ "azureOpenaiEndpoint": "https://YOUR_RESOURCE.openai.azure.com",
282
+ "azureOpenaiDeployment": "YOUR_DEPLOYMENT_NAME",
283
+ "azureOpenaiApiVersion": "2024-10-01-preview",
284
+
235
285
  "ollamaBaseUrl": "http://localhost:11434",
236
286
  "ollamaModel": "llama3"
237
287
  }
238
288
  ```
239
289
 
240
- Only the fields for your active provider are required. This file is gitignored by default.
241
-
242
290
  ---
243
291
 
244
292
  ## What it checks
@@ -0,0 +1,59 @@
1
+ import { buildPrompt } from './prompt.js';
2
+ import { buildPatchPrompt } from './patch-prompt.js';
3
+ import { groupViolations } from './group.js';
4
+ import { BaseAIProvider } from './base.js';
5
+ export class AnthropicProvider extends BaseAIProvider {
6
+ apiKey;
7
+ model;
8
+ constructor(apiKey, model = 'claude-sonnet-4-6') {
9
+ super();
10
+ this.apiKey = apiKey;
11
+ this.model = model;
12
+ }
13
+ get headers() {
14
+ return {
15
+ 'Content-Type': 'application/json',
16
+ 'x-api-key': this.apiKey,
17
+ 'anthropic-version': '2023-06-01',
18
+ };
19
+ }
20
+ async generateFixes(violations, strategy = 'rule', framework) {
21
+ if (violations.length === 0)
22
+ return [];
23
+ const groups = groupViolations(violations, strategy);
24
+ const prompt = buildPrompt(groups, framework);
25
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
26
+ method: 'POST',
27
+ headers: this.headers,
28
+ body: JSON.stringify({
29
+ model: this.model,
30
+ max_tokens: 8192,
31
+ temperature: 0.2,
32
+ messages: [{ role: 'user', content: prompt }],
33
+ }),
34
+ });
35
+ if (!response.ok) {
36
+ throw new Error(`Anthropic API error: ${response.status} ${await response.text()}`);
37
+ }
38
+ const data = await response.json();
39
+ const text = data.content?.[0]?.text ?? '[]';
40
+ return this.parse(text, groups, framework);
41
+ }
42
+ async generateFilePatch(fileContent, violations, filePath, framework) {
43
+ const prompt = buildPatchPrompt(fileContent, violations, filePath, framework);
44
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
45
+ method: 'POST',
46
+ headers: this.headers,
47
+ body: JSON.stringify({
48
+ model: this.model,
49
+ max_tokens: 16384,
50
+ temperature: 0.1,
51
+ messages: [{ role: 'user', content: prompt }],
52
+ }),
53
+ });
54
+ if (!response.ok)
55
+ throw new Error(`Anthropic API error: ${response.status} ${await response.text()}`);
56
+ const data = await response.json();
57
+ return data.content?.[0]?.text ?? '';
58
+ }
59
+ }
@@ -0,0 +1,17 @@
1
+ import { OpenAICompatProvider } from './openai-compat.js';
2
+ export class AzureOpenAIProvider extends OpenAICompatProvider {
3
+ apiVersion;
4
+ constructor(endpoint, apiKey, deployment, apiVersion = '2024-10-01-preview') {
5
+ super(`${endpoint}/openai/deployments/${deployment}`, apiKey, deployment, 'Azure OpenAI');
6
+ this.apiVersion = apiVersion;
7
+ }
8
+ buildUrl(path) {
9
+ return `${this.baseUrl}${path}?api-version=${this.apiVersion}`;
10
+ }
11
+ buildHeaders() {
12
+ return {
13
+ 'Content-Type': 'application/json',
14
+ 'api-key': this.apiKey,
15
+ };
16
+ }
17
+ }
@@ -0,0 +1,39 @@
1
+ import { fallbackExplanation } from './fallback-explanation.js';
2
+ export class BaseAIProvider {
3
+ parse(text, groups, framework) {
4
+ try {
5
+ const jsonMatch = text.match(/\[[\s\S]*\]/);
6
+ const fixes = jsonMatch ? JSON.parse(jsonMatch[0]) : [];
7
+ return groups.map((g) => {
8
+ const found = fixes.find((f) => f.ruleId === g.ruleId);
9
+ return found
10
+ ? { ...found, selectors: g.selectors, instanceCount: g.count }
11
+ : this.fallbackFix(g, framework);
12
+ });
13
+ }
14
+ catch {
15
+ return groups.map((g) => this.fallbackFix(g, framework));
16
+ }
17
+ }
18
+ fallback(groups, framework) {
19
+ return groups.map((g) => this.fallbackFix(g, framework));
20
+ }
21
+ fallbackFix(g, framework) {
22
+ const v = g.representative;
23
+ const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
24
+ const explanation = fallbackExplanation(g.ruleId, g.description, g.wcag, g.level);
25
+ const fwNote = framework ? `This project uses ${framework}. ` : '';
26
+ const prompt = g.count > 1
27
+ ? `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected elements (${g.count} instances):\n${selectorList}\n\nRepresentative HTML:\n\`${v.html.slice(0, 300)}\`\n\nApply the fix to all ${g.count} instances in the codebase to comply with WCAG 2.1 SC ${g.wcag}.`
28
+ : `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected element:\n- Selector: \`${g.selectors[0]}\`\n- HTML: \`${v.html.slice(0, 300)}\`\n\nApply the fix to comply with WCAG 2.1 SC ${g.wcag}.`;
29
+ return {
30
+ ruleId: g.ruleId,
31
+ selectors: g.selectors,
32
+ instanceCount: g.count,
33
+ explanation,
34
+ fixedCode: v.html,
35
+ wcagReference: `WCAG 2.1 SC ${g.wcag}`,
36
+ optimalPrompt: prompt,
37
+ };
38
+ }
39
+ }
@@ -0,0 +1,58 @@
1
+ import { buildPrompt } from './prompt.js';
2
+ import { buildPatchPrompt } from './patch-prompt.js';
3
+ import { groupViolations } from './group.js';
4
+ import { BaseAIProvider } from './base.js';
5
+ export class CohereProvider extends BaseAIProvider {
6
+ apiKey;
7
+ model;
8
+ constructor(apiKey, model = 'command-r-plus') {
9
+ super();
10
+ this.apiKey = apiKey;
11
+ this.model = model;
12
+ }
13
+ get headers() {
14
+ return {
15
+ 'Content-Type': 'application/json',
16
+ 'Authorization': `Bearer ${this.apiKey}`,
17
+ };
18
+ }
19
+ async generateFixes(violations, strategy = 'rule', framework) {
20
+ if (violations.length === 0)
21
+ return [];
22
+ const groups = groupViolations(violations, strategy);
23
+ const prompt = buildPrompt(groups, framework);
24
+ const response = await fetch('https://api.cohere.com/v2/chat', {
25
+ method: 'POST',
26
+ headers: this.headers,
27
+ body: JSON.stringify({
28
+ model: this.model,
29
+ messages: [{ role: 'user', content: prompt }],
30
+ max_tokens: 8192,
31
+ temperature: 0.2,
32
+ }),
33
+ });
34
+ if (!response.ok) {
35
+ throw new Error(`Cohere API error: ${response.status} ${await response.text()}`);
36
+ }
37
+ const data = await response.json();
38
+ const text = data.message?.content?.[0]?.text ?? '[]';
39
+ return this.parse(text, groups, framework);
40
+ }
41
+ async generateFilePatch(fileContent, violations, filePath, framework) {
42
+ const prompt = buildPatchPrompt(fileContent, violations, filePath, framework);
43
+ const response = await fetch('https://api.cohere.com/v2/chat', {
44
+ method: 'POST',
45
+ headers: this.headers,
46
+ body: JSON.stringify({
47
+ model: this.model,
48
+ messages: [{ role: 'user', content: prompt }],
49
+ max_tokens: 16384,
50
+ temperature: 0.1,
51
+ }),
52
+ });
53
+ if (!response.ok)
54
+ throw new Error(`Cohere API error: ${response.status} ${await response.text()}`);
55
+ const data = await response.json();
56
+ return data.message?.content?.[0]?.text ?? '';
57
+ }
58
+ }
@@ -0,0 +1,6 @@
1
+ import { OpenAICompatProvider } from './openai-compat.js';
2
+ export class DeepSeekProvider extends OpenAICompatProvider {
3
+ constructor(apiKey, model = 'deepseek-chat') {
4
+ super('https://api.deepseek.com/v1', apiKey, model, 'DeepSeek');
5
+ }
6
+ }
package/dist/ai/gemini.js CHANGED
@@ -1,21 +1,24 @@
1
1
  import { buildPrompt } from './prompt.js';
2
2
  import { buildPatchPrompt } from './patch-prompt.js';
3
3
  import { groupViolations } from './group.js';
4
- import { fallbackExplanation } from './fallback-explanation.js';
5
- export class GeminiProvider {
4
+ import { BaseAIProvider } from './base.js';
5
+ export class GeminiProvider extends BaseAIProvider {
6
6
  apiKey;
7
7
  model;
8
8
  constructor(apiKey, model = 'gemini-2.5-flash') {
9
+ super();
9
10
  this.apiKey = apiKey;
10
11
  this.model = model;
11
12
  }
13
+ buildUrl(model) {
14
+ return `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${this.apiKey}`;
15
+ }
12
16
  async generateFixes(violations, strategy = 'rule', framework) {
13
17
  if (violations.length === 0)
14
18
  return [];
15
19
  const groups = groupViolations(violations, strategy);
16
20
  const prompt = buildPrompt(groups, framework);
17
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${this.model}:generateContent?key=${this.apiKey}`;
18
- const response = await fetch(url, {
21
+ const response = await fetch(this.buildUrl(this.model), {
19
22
  method: 'POST',
20
23
  headers: { 'Content-Type': 'application/json' },
21
24
  body: JSON.stringify({
@@ -32,8 +35,7 @@ export class GeminiProvider {
32
35
  }
33
36
  async generateFilePatch(fileContent, violations, filePath, framework) {
34
37
  const prompt = buildPatchPrompt(fileContent, violations, filePath, framework);
35
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${this.model}:generateContent?key=${this.apiKey}`;
36
- const response = await fetch(url, {
38
+ const response = await fetch(this.buildUrl(this.model), {
37
39
  method: 'POST',
38
40
  headers: { 'Content-Type': 'application/json' },
39
41
  body: JSON.stringify({
@@ -46,37 +48,4 @@ export class GeminiProvider {
46
48
  const data = await response.json();
47
49
  return data.candidates?.[0]?.content?.parts?.[0]?.text ?? '';
48
50
  }
49
- parse(text, groups, framework) {
50
- try {
51
- const jsonMatch = text.match(/\[[\s\S]*\]/);
52
- const fixes = jsonMatch ? JSON.parse(jsonMatch[0]) : [];
53
- return groups.map((g) => {
54
- const found = fixes.find((f) => f.ruleId === g.ruleId);
55
- return found
56
- ? { ...found, selectors: g.selectors, instanceCount: g.count }
57
- : this.fallbackFix(g, framework);
58
- });
59
- }
60
- catch {
61
- return groups.map((g) => this.fallbackFix(g, framework));
62
- }
63
- }
64
- fallbackFix(g, framework) {
65
- const v = g.representative;
66
- const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
67
- const explanation = fallbackExplanation(g.ruleId, g.description, g.wcag, g.level);
68
- const fwNote = framework ? `This project uses ${framework}. ` : '';
69
- const prompt = g.count > 1
70
- ? `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected elements (${g.count} instances):\n${selectorList}\n\nRepresentative HTML:\n\`${v.html.slice(0, 300)}\`\n\nApply the fix to all ${g.count} instances in the codebase to comply with WCAG 2.1 SC ${g.wcag}.`
71
- : `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected element:\n- Selector: \`${g.selectors[0]}\`\n- HTML: \`${v.html.slice(0, 300)}\`\n\nApply the fix to comply with WCAG 2.1 SC ${g.wcag}.`;
72
- return {
73
- ruleId: g.ruleId,
74
- selectors: g.selectors,
75
- instanceCount: g.count,
76
- explanation,
77
- fixedCode: v.html,
78
- wcagReference: `WCAG 2.1 SC ${g.wcag}`,
79
- optimalPrompt: prompt,
80
- };
81
- }
82
51
  }
@@ -0,0 +1,6 @@
1
+ import { OpenAICompatProvider } from './openai-compat.js';
2
+ export class GroqProvider extends OpenAICompatProvider {
3
+ constructor(apiKey, model = 'llama-3.3-70b-versatile') {
4
+ super('https://api.groq.com/openai/v1', apiKey, model, 'Groq');
5
+ }
6
+ }
package/dist/ai/index.js CHANGED
@@ -1,20 +1,84 @@
1
1
  import { GeminiProvider } from './gemini.js';
2
2
  import { OllamaProvider } from './ollama.js';
3
3
  import { OpenAIProvider } from './openai.js';
4
+ import { AnthropicProvider } from './anthropic.js';
5
+ import { MistralProvider } from './mistral.js';
6
+ import { GroqProvider } from './groq.js';
7
+ import { CohereProvider } from './cohere.js';
8
+ import { XAIProvider } from './xai.js';
9
+ import { DeepSeekProvider } from './deepseek.js';
10
+ import { TogetherProvider } from './together.js';
11
+ import { PerplexityProvider } from './perplexity.js';
12
+ import { AzureOpenAIProvider } from './azure-openai.js';
4
13
  export function createAIProvider(config) {
5
- if (config.provider === 'ollama') {
6
- return new OllamaProvider(config.ollamaBaseUrl, config.ollamaModel);
14
+ switch (config.provider) {
15
+ case 'ollama':
16
+ return new OllamaProvider(config.ollamaBaseUrl, config.ollamaModel);
17
+ case 'openai':
18
+ if (!config.openaiApiKey) {
19
+ console.error('No OpenAI API key found. Add "openaiApiKey" to a11y.config.json.');
20
+ process.exit(1);
21
+ }
22
+ return new OpenAIProvider(config.openaiApiKey, config.openaiModel);
23
+ case 'anthropic':
24
+ if (!config.anthropicApiKey) {
25
+ console.error('No Anthropic API key found. Add "anthropicApiKey" to a11y.config.json.');
26
+ process.exit(1);
27
+ }
28
+ return new AnthropicProvider(config.anthropicApiKey, config.anthropicModel);
29
+ case 'mistral':
30
+ if (!config.mistralApiKey) {
31
+ console.error('No Mistral API key found. Add "mistralApiKey" to a11y.config.json.');
32
+ process.exit(1);
33
+ }
34
+ return new MistralProvider(config.mistralApiKey, config.mistralModel);
35
+ case 'groq':
36
+ if (!config.groqApiKey) {
37
+ console.error('No Groq API key found. Add "groqApiKey" to a11y.config.json.');
38
+ process.exit(1);
39
+ }
40
+ return new GroqProvider(config.groqApiKey, config.groqModel);
41
+ case 'cohere':
42
+ if (!config.cohereApiKey) {
43
+ console.error('No Cohere API key found. Add "cohereApiKey" to a11y.config.json.');
44
+ process.exit(1);
45
+ }
46
+ return new CohereProvider(config.cohereApiKey, config.cohereModel);
47
+ case 'xai':
48
+ if (!config.xaiApiKey) {
49
+ console.error('No xAI API key found. Add "xaiApiKey" to a11y.config.json.');
50
+ process.exit(1);
51
+ }
52
+ return new XAIProvider(config.xaiApiKey, config.xaiModel);
53
+ case 'deepseek':
54
+ if (!config.deepseekApiKey) {
55
+ console.error('No DeepSeek API key found. Add "deepseekApiKey" to a11y.config.json.');
56
+ process.exit(1);
57
+ }
58
+ return new DeepSeekProvider(config.deepseekApiKey, config.deepseekModel);
59
+ case 'together':
60
+ if (!config.togetherApiKey) {
61
+ console.error('No Together AI API key found. Add "togetherApiKey" to a11y.config.json.');
62
+ process.exit(1);
63
+ }
64
+ return new TogetherProvider(config.togetherApiKey, config.togetherModel);
65
+ case 'perplexity':
66
+ if (!config.perplexityApiKey) {
67
+ console.error('No Perplexity API key found. Add "perplexityApiKey" to a11y.config.json.');
68
+ process.exit(1);
69
+ }
70
+ return new PerplexityProvider(config.perplexityApiKey, config.perplexityModel);
71
+ case 'azure-openai':
72
+ if (!config.azureOpenaiApiKey || !config.azureOpenaiEndpoint || !config.azureOpenaiDeployment) {
73
+ console.error('Azure OpenAI requires "azureOpenaiApiKey", "azureOpenaiEndpoint", and "azureOpenaiDeployment" in a11y.config.json.');
74
+ process.exit(1);
75
+ }
76
+ return new AzureOpenAIProvider(config.azureOpenaiEndpoint, config.azureOpenaiApiKey, config.azureOpenaiDeployment, config.azureOpenaiApiVersion);
77
+ default:
78
+ if (!config.apiKey) {
79
+ console.error('No API key found. Run: wcag-a11y init');
80
+ process.exit(1);
81
+ }
82
+ return new GeminiProvider(config.apiKey, config.model);
7
83
  }
8
- if (config.provider === 'openai') {
9
- if (!config.openaiApiKey) {
10
- console.error('No OpenAI API key found. Add "openaiApiKey" to a11y.config.json.');
11
- process.exit(1);
12
- }
13
- return new OpenAIProvider(config.openaiApiKey, config.openaiModel);
14
- }
15
- if (!config.apiKey) {
16
- console.error('No API key found. Run: wcag-a11y init');
17
- process.exit(1);
18
- }
19
- return new GeminiProvider(config.apiKey, config.model);
20
84
  }
@@ -0,0 +1,6 @@
1
+ import { OpenAICompatProvider } from './openai-compat.js';
2
+ export class MistralProvider extends OpenAICompatProvider {
3
+ constructor(apiKey, model = 'mistral-large-latest') {
4
+ super('https://api.mistral.ai/v1', apiKey, model, 'Mistral');
5
+ }
6
+ }
package/dist/ai/ollama.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { buildPrompt } from './prompt.js';
2
2
  import { buildPatchPrompt } from './patch-prompt.js';
3
3
  import { groupViolations } from './group.js';
4
- import { fallbackExplanation } from './fallback-explanation.js';
5
- export class OllamaProvider {
4
+ import { BaseAIProvider } from './base.js';
5
+ export class OllamaProvider extends BaseAIProvider {
6
6
  baseUrl;
7
7
  model;
8
8
  constructor(baseUrl = 'http://localhost:11434', model = 'llama3') {
9
+ super();
9
10
  this.baseUrl = baseUrl;
10
11
  this.model = model;
11
12
  }
@@ -52,25 +53,4 @@ export class OllamaProvider {
52
53
  const data = await response.json();
53
54
  return data.response ?? '';
54
55
  }
55
- fallback(groups, framework) {
56
- return groups.map((g) => this.fallbackFix(g, framework));
57
- }
58
- fallbackFix(g, framework) {
59
- const v = g.representative;
60
- const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
61
- const explanation = fallbackExplanation(g.ruleId, g.description, g.wcag, g.level);
62
- const fwNote = framework ? `This project uses ${framework}. ` : '';
63
- const prompt = g.count > 1
64
- ? `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected elements (${g.count} instances):\n${selectorList}\n\nRepresentative HTML:\n\`${v.html.slice(0, 300)}\`\n\nApply the fix to all ${g.count} instances in the codebase to comply with WCAG 2.1 SC ${g.wcag}.`
65
- : `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected element:\n- Selector: \`${g.selectors[0]}\`\n- HTML: \`${v.html.slice(0, 300)}\`\n\nApply the fix to comply with WCAG 2.1 SC ${g.wcag}.`;
66
- return {
67
- ruleId: g.ruleId,
68
- selectors: g.selectors,
69
- instanceCount: g.count,
70
- explanation,
71
- fixedCode: v.html,
72
- wcagReference: `WCAG 2.1 SC ${g.wcag}`,
73
- optimalPrompt: prompt,
74
- };
75
- }
76
56
  }
@@ -0,0 +1,65 @@
1
+ import { buildPrompt } from './prompt.js';
2
+ import { buildPatchPrompt } from './patch-prompt.js';
3
+ import { groupViolations } from './group.js';
4
+ import { BaseAIProvider } from './base.js';
5
+ export class OpenAICompatProvider extends BaseAIProvider {
6
+ baseUrl;
7
+ apiKey;
8
+ model;
9
+ providerName;
10
+ constructor(baseUrl, apiKey, model, providerName = 'API') {
11
+ super();
12
+ this.baseUrl = baseUrl;
13
+ this.apiKey = apiKey;
14
+ this.model = model;
15
+ this.providerName = providerName;
16
+ }
17
+ buildUrl(path) {
18
+ return `${this.baseUrl}${path}`;
19
+ }
20
+ buildHeaders() {
21
+ return {
22
+ 'Content-Type': 'application/json',
23
+ 'Authorization': `Bearer ${this.apiKey}`,
24
+ };
25
+ }
26
+ async generateFixes(violations, strategy = 'rule', framework) {
27
+ if (violations.length === 0)
28
+ return [];
29
+ const groups = groupViolations(violations, strategy);
30
+ const prompt = buildPrompt(groups, framework);
31
+ const response = await fetch(this.buildUrl('/chat/completions'), {
32
+ method: 'POST',
33
+ headers: this.buildHeaders(),
34
+ body: JSON.stringify({
35
+ model: this.model,
36
+ messages: [{ role: 'user', content: prompt }],
37
+ temperature: 0.2,
38
+ max_tokens: 8192,
39
+ }),
40
+ });
41
+ if (!response.ok) {
42
+ throw new Error(`${this.providerName} API error: ${response.status} ${await response.text()}`);
43
+ }
44
+ const data = await response.json();
45
+ const text = data.choices?.[0]?.message?.content ?? '[]';
46
+ return this.parse(text, groups, framework);
47
+ }
48
+ async generateFilePatch(fileContent, violations, filePath, framework) {
49
+ const prompt = buildPatchPrompt(fileContent, violations, filePath, framework);
50
+ const response = await fetch(this.buildUrl('/chat/completions'), {
51
+ method: 'POST',
52
+ headers: this.buildHeaders(),
53
+ body: JSON.stringify({
54
+ model: this.model,
55
+ messages: [{ role: 'user', content: prompt }],
56
+ temperature: 0.1,
57
+ max_tokens: 16384,
58
+ }),
59
+ });
60
+ if (!response.ok)
61
+ throw new Error(`${this.providerName} API error: ${response.status} ${await response.text()}`);
62
+ const data = await response.json();
63
+ return data.choices?.[0]?.message?.content ?? '';
64
+ }
65
+ }
package/dist/ai/openai.js CHANGED
@@ -1,87 +1,6 @@
1
- import { buildPrompt } from './prompt.js';
2
- import { buildPatchPrompt } from './patch-prompt.js';
3
- import { groupViolations } from './group.js';
4
- import { fallbackExplanation } from './fallback-explanation.js';
5
- export class OpenAIProvider {
6
- apiKey;
7
- model;
1
+ import { OpenAICompatProvider } from './openai-compat.js';
2
+ export class OpenAIProvider extends OpenAICompatProvider {
8
3
  constructor(apiKey, model = 'gpt-4o-mini') {
9
- this.apiKey = apiKey;
10
- this.model = model;
11
- }
12
- async generateFixes(violations, strategy = 'rule', framework) {
13
- if (violations.length === 0)
14
- return [];
15
- const groups = groupViolations(violations, strategy);
16
- const prompt = buildPrompt(groups, framework);
17
- const response = await fetch('https://api.openai.com/v1/chat/completions', {
18
- method: 'POST',
19
- headers: {
20
- 'Content-Type': 'application/json',
21
- 'Authorization': `Bearer ${this.apiKey}`,
22
- },
23
- body: JSON.stringify({
24
- model: this.model,
25
- messages: [{ role: 'user', content: prompt }],
26
- temperature: 0.2,
27
- max_tokens: 8192,
28
- }),
29
- });
30
- if (!response.ok) {
31
- throw new Error(`OpenAI API error: ${response.status} ${await response.text()}`);
32
- }
33
- const data = await response.json();
34
- const text = data.choices?.[0]?.message?.content ?? '[]';
35
- return this.parse(text, groups, framework);
36
- }
37
- async generateFilePatch(fileContent, violations, filePath, framework) {
38
- const prompt = buildPatchPrompt(fileContent, violations, filePath, framework);
39
- const response = await fetch('https://api.openai.com/v1/chat/completions', {
40
- method: 'POST',
41
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.apiKey}` },
42
- body: JSON.stringify({
43
- model: this.model,
44
- messages: [{ role: 'user', content: prompt }],
45
- temperature: 0.1,
46
- max_tokens: 16384,
47
- }),
48
- });
49
- if (!response.ok)
50
- throw new Error(`OpenAI API error: ${response.status} ${await response.text()}`);
51
- const data = await response.json();
52
- return data.choices?.[0]?.message?.content ?? '';
53
- }
54
- parse(text, groups, framework) {
55
- try {
56
- const jsonMatch = text.match(/\[[\s\S]*\]/);
57
- const fixes = jsonMatch ? JSON.parse(jsonMatch[0]) : [];
58
- return groups.map((g) => {
59
- const found = fixes.find((f) => f.ruleId === g.ruleId);
60
- return found
61
- ? { ...found, selectors: g.selectors, instanceCount: g.count }
62
- : this.fallbackFix(g, framework);
63
- });
64
- }
65
- catch {
66
- return groups.map((g) => this.fallbackFix(g, framework));
67
- }
68
- }
69
- fallbackFix(g, framework) {
70
- const v = g.representative;
71
- const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
72
- const explanation = fallbackExplanation(g.ruleId, g.description, g.wcag, g.level);
73
- const fwNote = framework ? `This project uses ${framework}. ` : '';
74
- const prompt = g.count > 1
75
- ? `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected elements (${g.count} instances):\n${selectorList}\n\nRepresentative HTML:\n\`${v.html.slice(0, 300)}\`\n\nApply the fix to all ${g.count} instances in the codebase to comply with WCAG 2.1 SC ${g.wcag}.`
76
- : `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected element:\n- Selector: \`${g.selectors[0]}\`\n- HTML: \`${v.html.slice(0, 300)}\`\n\nApply the fix to comply with WCAG 2.1 SC ${g.wcag}.`;
77
- return {
78
- ruleId: g.ruleId,
79
- selectors: g.selectors,
80
- instanceCount: g.count,
81
- explanation,
82
- fixedCode: v.html,
83
- wcagReference: `WCAG 2.1 SC ${g.wcag}`,
84
- optimalPrompt: prompt,
85
- };
4
+ super('https://api.openai.com/v1', apiKey, model, 'OpenAI');
86
5
  }
87
6
  }
@@ -0,0 +1,6 @@
1
+ import { OpenAICompatProvider } from './openai-compat.js';
2
+ export class PerplexityProvider extends OpenAICompatProvider {
3
+ constructor(apiKey, model = 'llama-3.1-sonar-large-128k-online') {
4
+ super('https://api.perplexity.ai', apiKey, model, 'Perplexity');
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ import { OpenAICompatProvider } from './openai-compat.js';
2
+ export class TogetherProvider extends OpenAICompatProvider {
3
+ constructor(apiKey, model = 'meta-llama/Llama-3-70b-chat-hf') {
4
+ super('https://api.together.xyz/v1', apiKey, model, 'Together AI');
5
+ }
6
+ }
package/dist/ai/xai.js ADDED
@@ -0,0 +1,6 @@
1
+ import { OpenAICompatProvider } from './openai-compat.js';
2
+ export class XAIProvider extends OpenAICompatProvider {
3
+ constructor(apiKey, model = 'grok-2') {
4
+ super('https://api.x.ai/v1', apiKey, model, 'xAI');
5
+ }
6
+ }
package/dist/cli.js CHANGED
@@ -13,11 +13,11 @@ const program = new Command();
13
13
  program
14
14
  .name('wcag-a11y')
15
15
  .description('WCAG 2.1/2.2 accessibility auditor with AI-powered fixes')
16
- .version('0.3.6');
16
+ .version('0.4.0');
17
17
  program
18
18
  .command('init')
19
19
  .description('Create a11y.config.json in the current directory')
20
- .option('--provider <name>', 'AI provider to configure (gemini|openai|ollama)', 'gemini')
20
+ .option('--provider <name>', 'AI provider to configure (gemini|openai|ollama|anthropic|mistral|groq|cohere|xai|deepseek|together|perplexity|azure-openai)', 'gemini')
21
21
  .action((opts) => {
22
22
  initConfig(opts.provider);
23
23
  });
@@ -34,7 +34,7 @@ program
34
34
  .option('--fast-mode', 'Output only AI fix prompts — no summaries or explanations', false)
35
35
  .option('--group <strategy>', 'Group violations by rule or show individually (rule|none)', 'rule')
36
36
  .option('--ci', 'Exit with code 1 if any violations are found (for CI/CD pipelines)', false)
37
- .option('--provider <name>', 'Override the AI provider from config (gemini|openai|ollama)')
37
+ .option('--provider <name>', 'Override the AI provider from config (gemini|openai|ollama|anthropic|mistral|groq|cohere|xai|deepseek|together|perplexity|azure-openai)')
38
38
  .action(async (opts) => {
39
39
  try {
40
40
  console.log(`\nScanning ${opts.url}...`);
@@ -82,7 +82,7 @@ program
82
82
  .option('-c, --crawl', 'Auto-discover pages by following same-origin links', false)
83
83
  .option('--from-report [path]', 'Use an existing report instead of scanning (default: a11y-report.md)')
84
84
  .option('--apply', 'Write fixes to source files (default: dry-run, shows diff only)', false)
85
- .option('--provider <name>', 'Override the AI provider from config (gemini|openai|ollama)')
85
+ .option('--provider <name>', 'Override the AI provider from config (gemini|openai|ollama|anthropic|mistral|groq|cohere|xai|deepseek|together|perplexity|azure-openai)')
86
86
  .action(async (opts) => {
87
87
  if (!opts.url && !opts.fromReport) {
88
88
  console.error('\nError: provide --url <url> to scan, or --from-report [path] to load an existing report.');
package/dist/config.js CHANGED
@@ -7,6 +7,15 @@ const DEFAULTS = {
7
7
  ollamaBaseUrl: 'http://localhost:11434',
8
8
  ollamaModel: 'llama3',
9
9
  openaiModel: 'gpt-4o-mini',
10
+ anthropicModel: 'claude-sonnet-4-6',
11
+ mistralModel: 'mistral-large-latest',
12
+ groqModel: 'llama-3.3-70b-versatile',
13
+ cohereModel: 'command-r-plus',
14
+ xaiModel: 'grok-2',
15
+ deepseekModel: 'deepseek-chat',
16
+ togetherModel: 'meta-llama/Llama-3-70b-chat-hf',
17
+ perplexityModel: 'llama-3.1-sonar-large-128k-online',
18
+ azureOpenaiApiVersion: '2024-10-01-preview',
10
19
  };
11
20
  export function loadConfig() {
12
21
  const configPath = join(process.cwd(), CONFIG_FILE);
@@ -30,6 +39,48 @@ const STARTER_CONFIGS = {
30
39
  { provider: 'ollama', ollamaBaseUrl: 'http://localhost:11434', ollamaModel: 'llama3' },
31
40
  `Created ${CONFIG_FILE} — run \`ollama serve\` to start the local model server`,
32
41
  ],
42
+ anthropic: [
43
+ { provider: 'anthropic', anthropicApiKey: 'YOUR_ANTHROPIC_API_KEY', anthropicModel: 'claude-sonnet-4-6' },
44
+ `Created ${CONFIG_FILE} — add your Anthropic API key from https://console.anthropic.com`,
45
+ ],
46
+ mistral: [
47
+ { provider: 'mistral', mistralApiKey: 'YOUR_MISTRAL_API_KEY', mistralModel: 'mistral-large-latest' },
48
+ `Created ${CONFIG_FILE} — add your Mistral API key from https://console.mistral.ai`,
49
+ ],
50
+ groq: [
51
+ { provider: 'groq', groqApiKey: 'YOUR_GROQ_API_KEY', groqModel: 'llama-3.3-70b-versatile' },
52
+ `Created ${CONFIG_FILE} — add your Groq API key from https://console.groq.com`,
53
+ ],
54
+ cohere: [
55
+ { provider: 'cohere', cohereApiKey: 'YOUR_COHERE_API_KEY', cohereModel: 'command-r-plus' },
56
+ `Created ${CONFIG_FILE} — add your Cohere API key from https://dashboard.cohere.com`,
57
+ ],
58
+ xai: [
59
+ { provider: 'xai', xaiApiKey: 'YOUR_XAI_API_KEY', xaiModel: 'grok-2' },
60
+ `Created ${CONFIG_FILE} — add your xAI API key from https://console.x.ai`,
61
+ ],
62
+ deepseek: [
63
+ { provider: 'deepseek', deepseekApiKey: 'YOUR_DEEPSEEK_API_KEY', deepseekModel: 'deepseek-chat' },
64
+ `Created ${CONFIG_FILE} — add your DeepSeek API key from https://platform.deepseek.com`,
65
+ ],
66
+ together: [
67
+ { provider: 'together', togetherApiKey: 'YOUR_TOGETHER_API_KEY', togetherModel: 'meta-llama/Llama-3-70b-chat-hf' },
68
+ `Created ${CONFIG_FILE} — add your Together AI API key from https://api.together.xyz`,
69
+ ],
70
+ perplexity: [
71
+ { provider: 'perplexity', perplexityApiKey: 'YOUR_PERPLEXITY_API_KEY', perplexityModel: 'llama-3.1-sonar-large-128k-online' },
72
+ `Created ${CONFIG_FILE} — add your Perplexity API key from https://www.perplexity.ai/settings/api`,
73
+ ],
74
+ 'azure-openai': [
75
+ {
76
+ provider: 'azure-openai',
77
+ azureOpenaiApiKey: 'YOUR_AZURE_OPENAI_API_KEY',
78
+ azureOpenaiEndpoint: 'https://YOUR_RESOURCE.openai.azure.com',
79
+ azureOpenaiDeployment: 'YOUR_DEPLOYMENT_NAME',
80
+ azureOpenaiApiVersion: '2024-10-01-preview',
81
+ },
82
+ `Created ${CONFIG_FILE} — fill in your Azure OpenAI endpoint, deployment, and API key from https://portal.azure.com`,
83
+ ],
33
84
  };
34
85
  export function initConfig(provider = 'gemini') {
35
86
  const configPath = join(process.cwd(), CONFIG_FILE);
package/package.json CHANGED
@@ -1,7 +1,25 @@
1
1
  {
2
2
  "name": "wcag-a11y",
3
- "version": "0.3.6",
4
- "description": "WCAG 2.1/2.2 accessibility auditor with AI-powered fixes",
3
+ "version": "0.4.0",
4
+ "description": "WCAG 2.1/2.2 accessibility auditor with AI-powered fixes. Crawls your dev server with Playwright, runs 40+ checks, and uses AI (12 providers) to generate fix prompts or patch source files directly.",
5
+ "keywords": [
6
+ "wcag",
7
+ "accessibility",
8
+ "a11y",
9
+ "wcag2",
10
+ "wcag21",
11
+ "wcag22",
12
+ "aria",
13
+ "playwright",
14
+ "ai",
15
+ "audit",
16
+ "fix",
17
+ "cli",
18
+ "gemini",
19
+ "openai",
20
+ "anthropic",
21
+ "screen-reader"
22
+ ],
5
23
  "type": "module",
6
24
  "bin": {
7
25
  "wcag-a11y": "dist/cli.js"