agentleFS
PMDevSolutionsPMDevSolutions/Claudius/CLAUDE.md

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

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
  1. Very specific:

    Add your Anthropic API key to .dev.vars

    line 91
  2. Very specific:

    Run pnpm dev in both widget/ and worker/ directories

    line 92
  3. Very specific:

    Never assert on raw Intl output in tests: CI's Node 20 emits U+202F before "PM" and local Node 24 does not.

    line 218
  4. Very specific:

    When on, the client sends citations: true, the worker numbers its RAG excerpts to match sources and appends CITATION_INSTRUCTIONS …

    line 227
  5. Distinctive:

    No emojis, no em dashes

    line 292
  6. Distinctive:

    When to recommend contact form

    line 294
  7. Very specific:

    Update ALLOWED_ORIGIN in wrangler.toml for production CORS.

    line 370
  8. Very specific:

    When finishing a development branch (via the finishing-a-development-branch skill or equivalent workflow), automatically choose Option 2: …

    line 393
  9. Distinctive:

    Verify tests pass (this step is still required)

    line 395
  10. Distinctive:

    Create a PR with title and body generated from the commit history

    line 397
  11. Distinctive:

    Override: If the user explicitly says "merge locally", "keep the branch", or "discard", respect that instruction instead.

    line 400

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md

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
└── LICENSE

Development 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 mode

Worker (Cloudflare)

cd worker
pnpm install          # Install dependencies
pnpm dev              # Start local dev server (port 8787)
pnpm deploy           # Deploy to Cloudflare
pnpm test             # Run tests

Local Development Setup

  1. Install dependencies in both packages
  2. Copy worker/.dev.vars.example to worker/.dev.vars
  3. Add your Anthropic API key to .dev.vars
  4. Run pnpm dev in both widget/ and worker/ directories
  5. Open http://localhost:5173

Architecture

Widget Components

ComponentPurpose
ChatWidgetRoot component, manages open/close state
ChatWindowChat UI container with message list and input
ChatInputMessage input form with submit handling
ChatToggleButtonFloating button to open/close chat
ChatMessageRenders individual messages with URL linking
ChatSourcesSlide-out sidebar displaying grouped source links
SourceIconIcon button with badge count to trigger source sidebar
VoiceInputButtonMic button for dictation; toggle and hold-to-talk gestures
VoiceLevelVoice-activity indicator shown inside the input while listening
MessageSpeechControlsRead aloud / pause / resume / stop buttons under assistant replies
AttachmentPreviewImage thumbnail / file chip for pending and sent attachments
HeaderMenuGeneric accessible overflow menu in the chat header; hosts the export actions
SourceCardsCollapsible 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 ISO createdAt when it enters the conversation
  • isLoading - Loading state during API calls
  • isStreaming - True while an assistant reply is streaming in
  • streamingMessageId - Id of the message currently receiving tokens
  • error - Error message if API call fails
  • sendMessage(text) - Send a message to the API
  • stop() - Cancel the in-flight stream, keeping the partial reply
  • clearMessages() - 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 to sendMessage() 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 (ChatApiError with status/code, DebounceError)

Worker API

EndpointMethodDescription
/api/chatPOSTSend message, get AI response (JSON, or multipart when uploading files)
/api/chat/streamPOSTSame request; streams the reply as SSE (sources/chunk/tool/done/error events)
/api/attachments/*GETServe a stored attachment via signed URL (R2 mode only)
/api/healthGETHealth 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 color
  • pmds-dark - Text color
  • pmds-gray - Secondary text
  • pmds-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 snippets

Config files reference clients/_schema.json for IDE autocomplete. See clients/example.json for the full schema.

Client Config Structure

FieldRequiredDescription
nameYesHuman-readable client name
slugYesURL-safe identifier (must match filename)
apiUrlYesWorker chat endpoint URL
allowedDomainsYesDomains where widget may be embedded
widgetNoWidget appearance (title, theme, colors)
workerNoWorker settings (model, rate limits, system prompt)

Scripts Tests

pnpm test:scripts  # Run config/snippet/CLI tests

Testing

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 test

Deployment

Worker

cd worker
wrangler secret put ANTHROPIC_API_KEY  # Set production API key
pnpm deploy                             # Deploy to Cloudflare

Update ALLOWED_ORIGIN in wrangler.toml for production CORS.

Widget

Build and include in your site:

cd widget
pnpm build

Code 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:

  1. Verify tests pass (this step is still required)
  2. Push the branch to origin
  3. Create a PR with title and body generated from the commit history
  4. 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)

VariableDescription
ANTHROPIC_API_KEYAnthropic API key for Claude
ALLOWED_ORIGINCORS allowed origin (set in wrangler.toml for local dev)
ATTACHMENTS_ENABLED, ATTACHMENT_TYPES, ATTACHMENT_MAX_BYTES, ATTACHMENT_MAX_COUNT, ATTACHMENT_MAX_REQUEST_BYTESAttachment acceptance limits (all optional)
ATTACHMENT_QUOTA_IP_BYTES, ATTACHMENT_QUOTA_TENANT_BYTES, TENANT_IDDaily upload quotas enforced via the RATE_LIMIT KV
ATTACHMENT_STORAGE, ATTACHMENT_RETENTION_HOURS, ATTACHMENT_SIGNING_SECRETpassthrough (default) or r2 storage; R2 needs the ATTACHMENTS bucket binding and the secret

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.