Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/accessibility-optional-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@accessibility-devkit/assist': minor
'@accessibility-devkit/mcp': minor
---

Recover contextual drafting and provider adapters from the archived prototype,
and expose deterministic checks through a genuine stdio MCP server. Drafts retain
explicit human-review boundaries and remain separate from deterministic reports.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,25 @@ Run these marketplace commands in Claude Code, then use the same first prompt:
/plugin install accessibility@accessibility-devkit
```

## Optional drafting and agent tools

Two additional packages are available from this repository's source. They are separate
from the ten published 1.1.2 packages and are not included in the installed 1.1.2 plugin:

- [`assist`](./packages/assist): contextual alt-text drafts, accessibility review suggestions,
and selectable word suggestions through an explicitly chosen provider and model. It
includes a CLI. Generated text is always a draft for human review.
- [`mcp`](./packages/mcp): a local stdio MCP server exposing source scanning, contrast,
readability, and timing checks to compatible agents. It uses the existing deterministic
functions and does not read local files, fetch URLs, or contact model providers.

These packages recover useful capabilities from the archived
[`accessibility-devkit-llm`](https://github.com/lukeslp/accessibility-devkit-llm) prototype.
See the [migration map](./docs/04-prototype-migration.md) for each old API's disposition
and the package READMEs for source build and usage instructions. The separate
[`intentional-ux`](https://github.com/actually-useful-ai/intentional-ux) plugin remains
the companion for goals, decisions, navigation, and interaction cost.

## Review lenses for different products

The general `accessibility` skill starts with the task, evidence, repair, and verification. It can route a review to a specialist when the product has a clear shape.
Expand Down
4 changes: 3 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
"tsup",
"TypeScript",
"unminified",
"WCAG"
"WCAG",
"Ollama",
"huggingface"
]
}
37 changes: 37 additions & 0 deletions docs/04-prototype-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Prototype migration

Accessibility Devkit is the maintained home for accessibility development tools
and review skills. Intentional UX remains a separate companion for interaction
design. The original language-model prototype remains archived for provenance;
the old Devkit mirror is now
[`accessibility-old`](https://github.com/actually-useful-ai/accessibility-old).

The migration starts from
[`accessibility-devkit-llm` at 241f1a3](https://github.com/lukeslp/accessibility-devkit-llm/tree/241f1a332af8dbe3a5aef4fcdc0ec33b5d619e87).
Its MIT-licensed source and history are preserved. These are adapted capabilities,
not binary-compatible replacements or a claim that old installations still work.

| Prototype surface | Maintained disposition |
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `llm-prompts` alt-text prompts | Contextual `altTextPrompt`; image purpose is explicit, including decorative and functional use |
| `llm-prompts` ARIA and WCAG prompts | `accessibilityReviewPrompt`; suggestions and verification tasks rather than pass/fail verdicts |
| `llm-apis` | `assist.createProvider`: explicit model, OpenAI-compatible, Anthropic, Ollama and Hugging Face transports; bounded requests and cancellation |
| `llm-tools` alt-text CLI/API | `assist.draftAltText` and `accessibility-assist alt-text`; explicit image bytes/MIME and context |
| `llm-tools` WCAG CLI/API | `assist.suggestAccessibilityReview` and `accessibility-assist review`; separate from deterministic reports |
| AAC word prediction | `assist.suggestWords` and `accessibility-assist suggest-words`; a person selects or edits each suggestion |
| Diagnosis-based AAC clinical planning | Retained only in archive; not a supported development-tool capability |
| Agent skill registry | The canonical general Accessibility skill and four specialist skills |
| Flask service called `llm-mcp` | Replaced in purpose by genuine stdio MCP tools over existing deterministic checks; no HTTP route compatibility |
| Orchestration and chat history | Caller-managed composition of explicit inputs; no implicit history collection or provider fallback |
| Internal gateway adapter | A caller-configured compatible API root where supported; no embedded service address or credential |

The ten deterministic npm packages and Python package remain at their published
1.1.2 release. `assist` and `mcp` are new optional packages at 0.1.0 in source;
their addition does not publish them or update installed plugins automatically.
Use their package READMEs to build and test the source.

Generation outputs retain provider/model provenance, draft status and manual
verification tasks. They do not populate `AccessibilityReport.findings`, alter its
evidence categories or change the Python/Node report contract. Network adapters
are tested with fixture responses; live provider compatibility remains dependent
on the selected model and account.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
},
"homepage": "https://github.com/actually-useful-ai/accessibility-devkit#readme",
"scripts": {
"build": "pnpm --filter @accessibility-devkit/core build && pnpm --filter=\"./packages/*\" --filter=\"!@accessibility-devkit/core\" --parallel build",
"build": "pnpm --filter @accessibility-devkit/core build && pnpm --filter @accessibility-devkit/cli build && pnpm --filter=\"./packages/*\" --filter=\"!@accessibility-devkit/core\" --filter=\"!@accessibility-devkit/cli\" --parallel build",
"lint": "eslint . --ext .ts,.tsx",
"format": "prettier --write .",
"format:check": "prettier --check .",
Expand Down
21 changes: 21 additions & 0 deletions packages/assist/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Luke Steuber

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
84 changes: 84 additions & 0 deletions packages/assist/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Optional accessibility drafting

`@accessibility-devkit/assist` recovers the useful generation workflows from the
archived language-model prototype. This new package is available from source;
it is separate from the published Devkit 1.1.2 packages.

It provides contextual alt-text drafts, source-review suggestions, and selectable
word suggestions. Every result has `status: "draft"`, `humanReviewRequired: true`,
provider/model provenance and verification steps. Generated suggestions never
become deterministic findings or a conformance report.

## Build and run

From the repository root:

```sh
pnpm install --frozen-lockfile
pnpm build
node packages/assist/dist/cli.mjs --help
```

Choose the provider and an appropriate model explicitly. A local Ollama example:

```sh
node packages/assist/dist/cli.mjs alt-text ./photo.png \
--provider ollama --model YOUR_VISION_MODEL --send \
--media-type image/png --purpose informative \
--context 'The photo accompanies a description of the garden.'
```

For `openai`, `anthropic` or `huggingface`, supply credentials through
`ACCESSIBILITY_API_KEY` or the provider's `*_API_KEY` environment variable.
Do not put credentials on the command line. `--base-url` selects a compatible
API root when needed; HTTPS is required except for loopback HTTP.
The Hugging Face adapter uses its OpenAI-compatible router; choose a model that
supports the requested text or image input. Provider availability and permissions
depend on the selected account and model. HTTP adapters have fixture coverage;
this migration does not claim every live model has been exercised.

```sh
node packages/assist/dist/cli.mjs review ./checkout.html \
--provider openai --model YOUR_MODEL --send \
--context 'Complete checkout using a keyboard.'

node packages/assist/dist/cli.mjs suggest-words ./prefix.txt \
--provider ollama --model YOUR_TEXT_MODEL --send \
--context 'Ordering lunch in my own words.'
```

`--send` permits transmitting only the named file and supplied context to the
chosen provider. URL inputs are not supported. Files are limited to 6 MB for
images and 100 KB for text; prompts and provider responses have additional limits.
No file is edited, suggestion selected, or text spoken automatically.

For a decorative image, `alt-text unused --purpose decorative --context 'Border'`
returns an empty alternative for review without reading the file or making a
provider request. No `--send`, credentials or model are needed for that decision.

## Programmatic API

```ts
import { createProvider, draftAltText } from '@accessibility-devkit/assist';

const provider = createProvider({ kind: 'ollama', model: 'YOUR_VISION_MODEL' });
const draft = await draftAltText(provider, {
image: { mediaType: 'image/png', base64: suppliedImageBytes },
purpose: 'functional',
context: 'This image is the only content of a link to the account page.',
});
```

`draftAltText`, `suggestAccessibilityReview`, and `suggestWords` accept an optional
`AbortSignal`. `createProvider` accepts an explicit model, API root, timeout and
an injectable `fetch`. No default model, fallback provider, automatic retry or
conversation history is selected. Calls time out after 30 seconds by default,
including response reading. Error messages omit provider bodies and credentials.

The exported `altTextPrompt` and `accessibilityReviewPrompt` can also be used with
an existing provider integration. A custom provider implements `DraftProvider`.
Treat all generated content as untrusted plain text; never insert it into HTML
without escaping. Verify visible text, names, purpose and uncertainties before use.

Word suggestions support personal communication. They do not diagnose a condition,
recommend a device, produce a clinical plan or replace the person's choices.
60 changes: 60 additions & 0 deletions packages/assist/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"name": "@accessibility-devkit/assist",
"version": "0.1.0",
"description": "Optional model-assisted accessibility drafts with explicit inputs and review boundaries.",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"bin": {
"accessibility-assist": "dist/cli.mjs"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
},
"files": [
"dist",
"LICENSE",
"README.md"
],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest run"
},
"keywords": [
"accessibility",
"a11y",
"cli",
"scanner",
"wcag"
],
"author": {
"name": "Luke Steuber",
"email": "luke@lukesteuber.com"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/actually-useful-ai/accessibility-devkit.git",
"directory": "packages/assist"
},
"bugs": {
"url": "https://github.com/actually-useful-ai/accessibility-devkit/issues"
},
"homepage": "https://github.com/actually-useful-ai/accessibility-devkit/tree/master/packages/assist#readme",
"publishConfig": {
"access": "public",
"provenance": true
},
"engines": {
"node": ">=22"
},
"dependencies": {},
"devDependencies": {
"@types/node": "^22.0.0"
}
}
15 changes: 15 additions & 0 deletions packages/assist/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { runAssist } from './command';

runAssist(process.argv.slice(2))
.then((result) => {
process.stdout.write(result + '\n');
})
.catch((error) => {
// Provider errors are sanitized by the adapter; filesystem errors can contain paths.
const message =
error instanceof Error && !('code' in error)
? error.message
: 'Could not read the supplied local file.';
process.stderr.write(message + '\n');
process.exitCode = 1;
});
112 changes: 112 additions & 0 deletions packages/assist/src/command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { readFileSync, statSync } from 'node:fs';
import {
createProvider,
draftAltText,
suggestAccessibilityReview,
suggestWords,
type ProviderKind,
type ImageType,
} from './index';

const help = `accessibility-assist <alt-text|review|suggest-words> <file> --send
--provider openai|anthropic|ollama|huggingface --model NAME
--context TEXT [--purpose informative|functional|complex|decorative]
[--media-type image/png|image/jpeg|image/webp] [--language en]
[--base-url URL]

Reads only the named local file. --send explicitly permits sending it and context
to the chosen provider. Credentials come from ACCESSIBILITY_API_KEY or the
provider's *_API_KEY environment variable. Output is a JSON draft for review.
For review, --context describes the task; for suggest-words, the file is a prefix.
Decorative alt text returns an empty draft without reading the file or contacting
a provider. Provider/model and --send are unnecessary for that local decision.
No URL inputs, automatic edits, clinical plans, or conformance verdicts.
`;

function readBounded(path: string, maximum: number): Buffer {
if (/^https?:/i.test(path)) throw new TypeError('Supply a local file, not a URL.');
const stat = statSync(path);
if (!stat.isFile() || stat.size > maximum)
throw new TypeError('Input must be a regular file within the size limit.');
const value = readFileSync(path);
if (value.length > maximum) throw new TypeError('Input exceeds the size limit.');
return value;
}

export async function runAssist(
args: string[],
env: NodeJS.ProcessEnv = process.env,
): Promise<string> {
if (args.length === 0 || args.includes('--help')) return help;
const [command, path, ...rest] = args;
if (!['alt-text', 'review', 'suggest-words'].includes(command) || !path || path.startsWith('--'))
throw new TypeError('Specify a command and local file. Use --help.');
const flags: Record<string, string> = {};
const accepted = new Set([
'--provider',
'--model',
'--context',
'--purpose',
'--media-type',
'--language',
'--base-url',
]);
let send = false;
for (let i = 0; i < rest.length; i++) {
const key = rest[i];
if (key === '--send') {
send = true;
continue;
}
if (!accepted.has(key) || !rest[i + 1] || rest[i + 1].startsWith('--') || key in flags)
throw new TypeError('Unknown, duplicate or incomplete option. Use --help.');
flags[key] = rest[++i];
}
const context = flags['--context'] ?? '';
const purpose = flags['--purpose'] as 'informative' | 'functional' | 'complex' | 'decorative';
if (command === 'alt-text' && purpose === 'decorative') {
const unused = {
kind: 'ollama' as const,
model: 'unused',
generate: async () => {
throw new Error('Unexpected provider request.');
},
};
return JSON.stringify(
await draftAltText(unused, { context, purpose, language: flags['--language'] }),
null,
2,
);
}
if (!send)
throw new TypeError(
'Use --send to permit sending the supplied content to the chosen provider.',
);
const kind = flags['--provider'] as ProviderKind;
const provider = createProvider({
kind,
model: flags['--model'],
apiKey: env.ACCESSIBILITY_API_KEY ?? env[`${kind?.toUpperCase()}_API_KEY`],
baseUrl: flags['--base-url'],
});
let result;
if (command === 'alt-text') {
const image = {
mediaType: flags['--media-type'] as ImageType,
base64: readBounded(path, 6_000_000).toString('base64'),
};
result = await draftAltText(provider, {
image,
context,
purpose,
language: flags['--language'],
});
} else {
const source = readBounded(path, 100_000).toString('utf8');
result =
command === 'review'
? await suggestAccessibilityReview(provider, { source, task: context })
: await suggestWords(provider, { prefix: source, context });
}
return JSON.stringify(result, null, 2);
}
Loading