Claudius
CLAUDE.md1 starsChanged 8 months agoMITView on GitHub
Claudius is an embeddable AI chat widget for PMDS and client sites. It consists of two packages:
- Reads credentials
- Installs packages
In this repository9 other files it gives its agents.
- animation-motionSkill
- auth-flowsSkill
- form-handlingSkill
- react-accessibilitySkill
- react-component-developmentSkill
- react-performance-optimizationSkill
- react-testing-workflowsSkill
- seo-metadataSkill
- state-managementSkill
The file
We split this CLAUDE.md into the rules it gives an agent
11 rules. About 0 are common, 5 are distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈5
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 91
Add your Anthropic API key to .dev.vars
- Very specific: line 92
Run pnpm dev in both widget/ and worker/ directories
- Very specific: line 218
Never assert on raw Intl output in tests: CI's Node 20 emits U+202F before "PM" and local Node 24 does not.
- Very specific: line 227
When on, the client sends citations: true, the worker numbers its RAG excerpts to match sources and appends CITATION_INSTRUCTIONS …
- Distinctive: line 292
No emojis, no em dashes
- Distinctive: line 294
When to recommend contact form
- Very specific: line 370
Update ALLOWED_ORIGIN in wrangler.toml for production CORS.
- Very specific: line 393
When finishing a development branch (via the finishing-a-development-branch skill or equivalent workflow), automatically choose Option 2: …
- Distinctive: line 395
Verify tests pass (this step is still required)
- Distinctive: line 397
Create a PR with title and body generated from the commit history
- Distinctive: line 400
Override: If the user explicitly says "merge locally", "keep the branch", or "discard", respect that instruction instead.
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CLAUDE.md
This file provides guidance to Claude Code when working with code in this repository.
Project Overview
Claudius is an embeddable AI chat widget for PMDS and client sites. It consists of two packages:
- widget/ - React chat widget component (Vite + TypeScript + Tailwind)
- worker/ - Cloudflare Workers backend (Hono + Anthropic SDK)
Project Structure
claudius/
├── widget/ # React chat widget
│ ├── src/
│ │ ├── components/ # React components
│ │ │ ├── ChatWidget.tsx # Root component
│ │ │ ├── ChatWindow.tsx # Chat UI container
│ │ │ ├── ChatInput.tsx # Message input form
│ │ │ ├── ChatToggleButton.tsx # Floating action button
│ │ │ └── ChatMessage.tsx # Individual message display
│ │ ├── hooks/
│ │ │ └── useChat.ts # Chat state management
│ │ ├── index.ts # Public exports
│ │ ├── main.tsx # Dev app entry
│ │ ├── styles.css # Tailwind styles
│ │ └── test-setup.ts # Vitest setup
│ ├── index.html # Dev app HTML
│ ├── vite.config.ts
│ ├── tailwind.config.ts
│ └── package.json
├── worker/ # Cloudflare Workers backend
│ ├── src/
│ │ ├── index.ts # Hono API routes
│ │ ├── chat.ts # Claude API integration
│ │ ├── attachments.ts # Attachment parsing/validation/content blocks
│ │ ├── attachment-storage.ts # Passthrough vs R2 storage, signed URLs
│ │ ├── attachment-quota.ts # Per-IP / per-tenant daily upload quotas (KV)
│ │ ├── system-prompt.ts # Bot personality/knowledge
│ │ └── __tests__/ # Worker tests
│ ├── wrangler.toml # Cloudflare config
│ ├── .dev.vars.example # Local secrets template
│ └── package.json
├── clients/ # Per-client configs
│ ├── _schema.json # JSON Schema for validation
│ ├── example.json # Example client config
│ └── example-system-prompt.md # Example system prompt
├── scripts/ # CLI and build tooling
│ ├── cli.ts # CLI entry point
│ ├── lib/
│ │ ├── config.ts # Config loader/validator
│ │ └── snippet.ts # Embed snippet generator
│ └── vitest.config.ts
├── .gitignore
├── package.json # Root package
├── README.md
├── CONTRIBUTING.md
├── SECURITY.md
└── LICENSEDevelopment Commands
Widget (React)
cd widget
pnpm install # Install dependencies
pnpm dev # Start dev server (port 5173)
pnpm build # Production build
pnpm test # Run tests
pnpm test:watch # Run tests in watch modeWorker (Cloudflare)
cd worker
pnpm install # Install dependencies
pnpm dev # Start local dev server (port 8787)
pnpm deploy # Deploy to Cloudflare
pnpm test # Run testsLocal Development Setup
- Install dependencies in both packages
- Copy
worker/.dev.vars.exampletoworker/.dev.vars - Add your Anthropic API key to
.dev.vars - Run
pnpm devin both widget/ and worker/ directories - Open http://localhost:5173
Architecture
Widget Components
| Component | Purpose |
ChatWidget | Root component, manages open/close state |
ChatWindow | Chat UI container with message list and input |
ChatInput | Message input form with submit handling |
ChatToggleButton | Floating button to open/close chat |
ChatMessage | Renders individual messages with URL linking |
ChatSources | Slide-out sidebar displaying grouped source links |
SourceIcon | Icon button with badge count to trigger source sidebar |
VoiceInputButton | Mic button for dictation; toggle and hold-to-talk gestures |
VoiceLevel | Voice-activity indicator shown inside the input while listening |
MessageSpeechControls | Read aloud / pause / resume / stop buttons under assistant replies |
AttachmentPreview | Image thumbnail / file chip for pending and sent attachments |
HeaderMenu | Generic accessible overflow menu in the chat header; hosts the export actions |
SourceCards | Collapsible footer of source cards under a cited reply; reveals a card when its chip is clicked |
useChat Hook
Manages chat state:
messages- Array of chat messages, each stamped with an ISOcreatedAtwhen it enters the conversationisLoading- Loading state during API callsisStreaming- True while an assistant reply is streaming instreamingMessageId- Id of the message currently receiving tokenserror- Error message if API call failssendMessage(text)- Send a message to the APIstop()- Cancel the in-flight stream, keeping the partial replyclearMessages()- Clear chat history
Streaming is on by default (streaming: false disables it); the client
falls back to the blocking endpoint automatically when the browser or
Worker can't stream.
API Client
ChatApiClient in widget/src/api/client.ts handles communication with the Worker:
- Typed requests/responses (
ChatRequest,ChatResponse) - SSE streaming via
streamMessage()(fetch +ReadableStream), with automatic fallback tosendMessage()on older browsers or Workers without/api/chat/stream - Retry on 429 (respects
Retry-After) and 503 (exponential backoff: 1s, 3s) - Max 2 retries (3 total attempts)
- Debounced sends (configurable, default 300ms)
- Typed errors (
ChatApiErrorwith status/code,DebounceError)
Worker API
| Endpoint | Method | Description |
/api/chat | POST | Send message, get AI response (JSON, or multipart when uploading files) |
/api/chat/stream | POST | Same request; streams the reply as SSE (sources/chunk/tool/done/error events) |
/api/attachments/* | GET | Serve a stored attachment via signed URL (R2 mode only) |
/api/health | GET | Health check |
/api/chat/stream shares the rate limiter, validation, and error shapes with
/api/chat: failures before the first streamed byte return the same JSON
error responses (400/429/500/503); failures mid-stream arrive as an in-band
error SSE event.
Tool Use
Tools live in worker/src/tools/ (types, registry, reference tools) and are
registered in worker/src/tools/index.ts (chatTools). Both chat endpoints
run the Anthropic tool_use / tool_result round trip transparently (max 5
rounds, then a forced text answer). Responses carry toolUses summaries
({name, input, result, isError?}); the stream emits event: tool between
chunks. The widget renders a "used tool" chip with a details disclosure.
Reference tools: get_current_time (enabled), search_knowledge_base and
submit_lead (stubs — wire before registering). Docs: plugins/tools.md.
RAG
RAG lives in worker/src/rag/ (Retriever interface, retrieval pipeline,
VectorizeRetriever reference implementation) and activates only when the
VECTORIZE_INDEX + AI bindings exist (see wrangler.toml). Per request the
worker retrieves for the latest user message (RAG_TOP_K,
RAG_SCORE_THRESHOLD, optional reranker hook in createRagFromEnv), appends
a RAG_CONTEXT_TEMPLATE context block to the system prompt, and returns
deduplicated sources (JSON body / SSE done event) that the widget already
renders. Retrieval failures degrade to ungrounded replies. Ingestion:
pnpm rag:ingest ./content --index <name> (chunking helpers in
scripts/lib/rag-ingest.ts). Docs: rag/index.md.
Voice
Voice is widget-only and opt-in (voice prop / ClaudiusConfig.voice /
<claudius-chat voice voice-mode voice-auto-submit voice-input voice-output
voice-lang>), built on the browser's Web Speech API with no dependencies.
useSpeechRecognition (owned by ChatInput) runs one single-utterance session
at a time and streams interim results into the field; useSpeechSynthesis
(owned by ChatWindow) reads one settled assistant reply at a time as
sentence-sized utterances. Pure helpers live in widget/src/utils/voice.ts
(resolveVoiceConfig, resolveSpeechLang, joinDictation,
chunkSpeechText, voiceOptionsFromAttributes). Each half is feature
detected separately and renders nothing when unsupported. jsdom has no speech
engines, so tests drive widget/src/test-utils/fakeSpeech*.ts.
Privacy: the widget never touches audio (no getUserMedia) and none reaches
the worker, but browsers recognize speech on their vendor's servers by
default. Do not describe it as on-device. Docs: configuration/voice.md;
design: docs/plans/2026-09-18-voice-input-tts-design.md.
Conversation Export
Widget-only and opt-in (conversationExport prop / ClaudiusConfig key /
<claudius-chat conversation-export> / widget.conversationExport in client
configs). It fails closed: only the literal true enables it, and nothing
renders when it is off. A HeaderMenu in the chat header offers Copy as
Markdown, Download as Markdown, and Download as JSON, all in the browser with
no request to the worker.
Pure serializers live in widget/src/utils/exportConversation.ts
(protectMessageText, conversationToMarkdown, conversationToJson,
exportFilename); clipboard and download side effects in
widget/src/utils/saveText.ts; useConversationExport (owned by ChatWindow)
turns messages into menu items plus a transient status. Markdown keeps message
text as written but closes unclosed code fences, turns single newlines into
hard breaks outside code, and escapes a line-leading <. JSON is the
persisted ChatMessage[], never with inline attachment bytes.
Never assert on raw Intl output in tests: CI's Node 20 emits U+202F before
"PM" and local Node 24 does not. Docs: configuration/conversation-export.md;
design: docs/plans/2026-09-19-conversation-export-design.md.
Citations
Widget-only opt-in (citations prop / ClaudiusConfig key /
<claudius-chat citations citations-max-sources citations-favicons> /
widget.citations in client configs), failing closed like conversationExport.
When on, the client sends citations: true, the worker numbers its RAG
excerpts to match sources and appends CITATION_INSTRUCTIONS
(worker/src/rag/retrieval.ts, buildRagContext), the stream route emits
event: sources before the first chunk, and ChatMessage renders in-range
[n] markers as chips with a SourceCards footer instead of the source icon.
Sources carry a 200-character snippet. Pure helpers live in
widget/src/utils/citations.ts (parseCitations, stripCitationMarkers,
hideTrailingCitationOpener, resolveCitationsConfig,
citationsOptionsFromAttributes). Only markers whose numbers are all within
sources.length become chips. Docs: configuration/citations.md; design:
docs/plans/2026-09-27-inline-citations-design.md.
Chat Request/Response
// Request
{
citations?: true, // widget asks for numbered, citable excerpts
messages: [
{ role: "user", content: "Hello" },
{ role: "assistant", content: "Hi there!" },
{
role: "user",
content: "What's on this receipt?",
attachments?: [
{ id: "att-1", name: "receipt.png", mediaType: "image/png", size: 1234, data?: "<base64>", key?: "att/<tenant>/<uuid>" }
]
}
]
}
// Response
{
reply: "How can I help you today?",
sources?: [
{ url: "https://...", title: "...", type: "blog" | "page" | "external", snippet?: "..." }
],
attachments?: [ { id: "att-1", key: "att/...", url: "https://.../api/attachments/...?exp=&sig=", expiresAt: "..." } ]
}Attachments (images + PDFs) are opt-in on the widget (attachments prop) and on by
default in the worker in passthrough mode (forward to Anthropic, store nothing).
Set ATTACHMENT_STORAGE=r2 plus an ATTACHMENTS R2 binding and
ATTACHMENT_SIGNING_SECRET to keep uploads for ATTACHMENT_RETENTION_HOURS.
See docs/src/content/docs/configuration/attachments.md.
Customization
System Prompt
Edit worker/src/system-prompt.ts to customize:
- Bot personality and tone
- Business information (name, contact, hours)
- Pricing structure
- Services offered
- FAQ responses
- Blog post references with URLs
Behavioral Rules
The system prompt includes rules for:
- Response length and formatting
- Line break usage
- No emojis, no em dashes
- Prompt injection protection
- When to recommend contact form
Styling
The widget uses Tailwind CSS with custom colors defined in widget/tailwind.config.ts:
pmds-blue- Primary brand colorpmds-dark- Text colorpmds-gray- Secondary textpmds-light-green- Assistant message background
Multi-Client Configuration
Client Config Files
Each client gets a JSON config file in clients/:
pnpm claudius init acme # Scaffold new client
pnpm claudius validate acme # Validate config
pnpm claudius snippet acme # Generate embed snippetsConfig files reference clients/_schema.json for IDE autocomplete. See clients/example.json for the full schema.
Client Config Structure
| Field | Required | Description |
name | Yes | Human-readable client name |
slug | Yes | URL-safe identifier (must match filename) |
apiUrl | Yes | Worker chat endpoint URL |
allowedDomains | Yes | Domains where widget may be embedded |
widget | No | Widget appearance (title, theme, colors) |
worker | No | Worker settings (model, rate limits, system prompt) |
Scripts Tests
pnpm test:scripts # Run config/snippet/CLI testsTesting
Widget Tests (Vitest + React Testing Library)
Located in widget/src/components/__tests__/ and widget/src/hooks/__tests__/:
- Component rendering tests
- User interaction tests
- Hook state management tests
Worker Tests (Vitest)
Located in worker/src/__tests__/:
- API endpoint tests
- System prompt content tests
- Input validation tests
Run all tests:
cd widget && pnpm test
cd worker && pnpm testDeployment
Worker
cd worker
wrangler secret put ANTHROPIC_API_KEY # Set production API key
pnpm deploy # Deploy to CloudflareUpdate ALLOWED_ORIGIN in wrangler.toml for production CORS.
Widget
Build and include in your site:
cd widget
pnpm buildCode Style
- TypeScript strict mode
- Functional React components
- Custom hooks for reusable logic
- Tailwind CSS for styling
- Vitest for testing
- pnpm for package management
Workflow Automation
Branch Completion: Auto-PR
When finishing a development branch (via the finishing-a-development-branch skill or equivalent workflow), automatically choose Option 2: Push and create a Pull Request. Do not present the 4-option menu. Instead:
- Verify tests pass (this step is still required)
- Push the branch to origin
- Create a PR with title and body generated from the commit history
- Report the PR URL to the user
Override: If the user explicitly says "merge locally", "keep the branch", or "discard", respect that instruction instead. The auto-PR default only applies when no specific completion action is requested.
Environment Variables
Worker (.dev.vars / Cloudflare secrets)
| Variable | Description |
ANTHROPIC_API_KEY | Anthropic API key for Claude |
ALLOWED_ORIGIN | CORS allowed origin (set in wrangler.toml for local dev) |
ATTACHMENTS_ENABLED, ATTACHMENT_TYPES, ATTACHMENT_MAX_BYTES, ATTACHMENT_MAX_COUNT, ATTACHMENT_MAX_REQUEST_BYTES | Attachment acceptance limits (all optional) |
ATTACHMENT_QUOTA_IP_BYTES, ATTACHMENT_QUOTA_TENANT_BYTES, TENANT_ID | Daily upload quotas enforced via the RATE_LIMIT KV |
ATTACHMENT_STORAGE, ATTACHMENT_RETENTION_HOURS, ATTACHMENT_SIGNING_SECRET | passthrough (default) or r2 storage; R2 needs the ATTACHMENTS bucket binding and the secret |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

