Skip to content

Commit 3d0466a

Browse files
PAMulliganclaude
andcommitted
feat(plugins): message-middleware plugin SDK for widget and worker
Add a ClaudiusPlugin middleware API so consumers can run logic around chat messages without forking — inject context, redact PII, route to different models, log analytics, or answer canned intents. Client and server expose equivalent lifecycle hooks (onBeforeSend / onAfterReceive / onError) that may be async and may modify, replace, or short-circuit messages. Client (widget): - ClaudiusPlugin interface + contexts (respondWith / abort short-circuit on send, respondWith recovery on error) and a runner with per-hook error isolation. - ChatWidget accepts a `plugins` prop; hooks run in array order in useChat — onBeforeSend before the send (transform, canned short-circuit, or abort), onAfterReceive on the reply, onError recovery. - Reference plugins pluginAnalytics, pluginRedactPII, pluginCannedResponses, exported from claudius-chat-widget. Server (worker): - ClaudiusServerPlugin with equivalent hooks over {role, content}, a runner, and a `chatPlugins` Hono middleware (short-circuit, request transform via c.get("chatRequest"), reply rewrite on afterReceive). - The same three reference plugins, adapted. Wired into index.ts as an opt-in extension point — empty by default, so behavior is unchanged. Docs: rewrite the plugins page with copy-paste client + server examples and add the `plugins` prop to the widget options table. Also fixes a latent noImplicitAny error in the worker CORS origin callback, surfaced now that the new code is type-checked (worker CI runs tests only, which strip types). Tested: runner, reference plugins, and useChat integration (client); runner, reference, isolated middleware, and a real-app success path (server). Full suites green — widget 287 unit + 10 e2e, worker 39 — plus widget lint/format/typecheck/docs:api/build/check:exports. Closes #45 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2144690 commit 3d0466a

23 files changed

Lines changed: 2171 additions & 26 deletions

File tree

docs/src/content/docs/configuration/widget.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ attributes on the `<claudius-chat>` web component.
2525
| `locale` | `"en" \| "es" \| "fr" \| "de"` | auto-detected | UI language; see [Localization](/configuration/localization/) |
2626
| `translations` | `Partial<ClaudiusTranslations>` | built-in | Override individual UI strings |
2727
| `triggers` | `Trigger[]` | `undefined` | Proactive triggers; see [Proactive triggers](/configuration/triggers/) |
28+
| `plugins` | `ClaudiusPlugin[]` | `undefined` | Message middleware run around each send (`onBeforeSend` / `onAfterReceive` / `onError`); see [Plugins](/plugins/) |
2829

2930
## Web component attributes
3031

@@ -41,8 +42,10 @@ attributes on the `<claudius-chat>` web component.
4142
></claudius-chat>
4243
```
4344

44-
`locale`, `translations`, and `triggers` are not available as attributes — use
45-
`window.ClaudiusConfig` or the React component for those.
45+
`locale`, `translations`, `triggers`, and `plugins` are not available as
46+
attributes — use `window.ClaudiusConfig` or the React component for those.
47+
(`plugins` are functions, so set them in JS via `window.ClaudiusConfig` or as a
48+
React prop.)
4649

4750
## Checking the deployed version
4851

docs/src/content/docs/plugins/index.md

Lines changed: 213 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,233 @@
11
---
22
title: Plugins & Tools
3-
description: Extension points available today, and the planned plugin SDK and tool registry.
3+
description: The Claudius plugin SDK — client and server middleware for message transforms, PII redaction, canned responses, and analytics.
44
---
55

6-
## Extension points today
6+
Claudius plugins are small middleware that run around the chat message
7+
lifecycle. The same three lifecycle hooks exist on the **client** (the React
8+
widget) and the **server** (the Worker), so you can inject context, redact PII,
9+
route to different models, log analytics events, or answer locally — without
10+
forking the widget.
711

8-
Claudius doesn't have a plugin API yet, but several supported extension points
9-
cover most customization needs:
12+
## Lifecycle hooks
13+
14+
| Hook | Runs | Can |
15+
|------|------|-----|
16+
| `onBeforeSend` | before a message is sent | modify it, replace it, answer locally (`respondWith`), or cancel it (`abort`) |
17+
| `onAfterReceive` | after the reply arrives | modify or replace the reply |
18+
| `onError` | when a send fails | observe the error; on the client, recover with a fallback reply |
19+
20+
All hooks may be `async`. A hook that throws is caught and logged, so a single
21+
misbehaving plugin will not break the chat — write security-sensitive
22+
transforms (like redaction) defensively.
23+
24+
## Client plugins (widget)
25+
26+
Pass an array of plugins to `ChatWidget`. Hooks run in array order.
27+
28+
```tsx
29+
import { ChatWidget, pluginRedactPII, pluginAnalytics } from "claudius-chat-widget";
30+
31+
export function App() {
32+
return (
33+
<ChatWidget
34+
apiUrl="https://your-worker.workers.dev"
35+
plugins={[
36+
pluginRedactPII(),
37+
pluginAnalytics({ onEvent: (e) => console.log(e) }),
38+
]}
39+
/>
40+
);
41+
}
42+
```
43+
44+
`onBeforeSend` returns the (possibly modified) message — and the returned
45+
message is what is **both displayed and sent**, so a redaction is visible to the
46+
user.
47+
48+
### Short-circuiting
49+
50+
The `onBeforeSend` context can answer without a network call, or cancel the
51+
send entirely:
52+
53+
```ts
54+
import type { ClaudiusPlugin } from "claudius-chat-widget";
55+
56+
const slashCommands: ClaudiusPlugin = {
57+
name: "slash-commands",
58+
onBeforeSend(message, ctx) {
59+
if (message.content === "/clear") return ctx.abort();
60+
if (message.content.startsWith("/help")) {
61+
ctx.respondWith("Type a question and press Enter.");
62+
}
63+
},
64+
};
65+
```
66+
67+
- `ctx.respondWith(reply)` — render `reply` as the assistant message and skip
68+
the API. `reply` is a string or `{ content, sources }`.
69+
- `ctx.abort()` — drop the message and render nothing.
70+
71+
`onError` can recover the same way, replacing the error UI with a reply:
72+
73+
```ts
74+
const fallback: ClaudiusPlugin = {
75+
name: "fallback",
76+
onError: (_err, ctx) =>
77+
ctx.respondWith("We're offline right now — email help@example.com."),
78+
};
79+
```
80+
81+
### Reference plugins
82+
83+
Three plugins ship with `claudius-chat-widget`.
84+
85+
**`pluginAnalytics`** — emit a structured event for every sent message, reply,
86+
and error:
87+
88+
```ts
89+
import { pluginAnalytics } from "claudius-chat-widget";
90+
91+
pluginAnalytics({
92+
onEvent: (event) => window.gtag?.("event", event.type, event),
93+
includeContent: false, // record only character counts, not message text
94+
});
95+
```
96+
97+
**`pluginRedactPII`** — strip emails, phone numbers, SSNs, and card-like
98+
numbers before the message leaves the browser:
99+
100+
```ts
101+
import { pluginRedactPII } from "claudius-chat-widget";
102+
103+
pluginRedactPII(); // sensible defaults
104+
pluginRedactPII({ replacement: "***", redactReplies: true });
105+
```
106+
107+
**`pluginCannedResponses`** — answer matched intents locally, with no API call:
108+
109+
```ts
110+
import { pluginCannedResponses } from "claudius-chat-widget";
111+
112+
pluginCannedResponses({
113+
rules: [
114+
{ match: "hours", reply: "We're open 9–5, Mon–Fri." },
115+
{ match: /pricing|cost/i, reply: "See https://example.com/pricing." },
116+
],
117+
});
118+
```
119+
120+
### Writing your own
121+
122+
A plugin is an object with a `name` and any of the three hooks:
123+
124+
```ts
125+
import type { ClaudiusPlugin } from "claudius-chat-widget";
126+
127+
const pageContext: ClaudiusPlugin = {
128+
name: "page-context",
129+
onBeforeSend(message) {
130+
return { ...message, content: `[on ${location.pathname}] ${message.content}` };
131+
},
132+
};
133+
```
134+
135+
## Server plugins (Worker)
136+
137+
The Worker exposes the equivalent hooks as Hono middleware. Server hooks
138+
operate on the whole request (the messages array) and the reply string, rather
139+
than on a single widget message.
140+
141+
Register plugins in `worker/src/index.ts` — the file ships this block with an
142+
empty list, so just drop your plugins in:
143+
144+
```ts
145+
import { chatPlugins, pluginRedactPII } from "./plugins";
146+
147+
const serverPlugins = [pluginRedactPII({ redactReplies: true })];
148+
if (serverPlugins.length > 0) {
149+
app.use("/api/chat", chatPlugins(serverPlugins));
150+
}
151+
```
152+
153+
### Server hooks
154+
155+
```ts
156+
import type { ClaudiusServerPlugin } from "./plugins";
157+
158+
const example: ClaudiusServerPlugin = {
159+
name: "example",
160+
onBeforeSend(messages, ctx) {
161+
// Inspect ctx.env, transform messages, or short-circuit the model:
162+
// ctx.respondWith("a canned reply");
163+
return messages;
164+
},
165+
onAfterReceive(reply) {
166+
return reply.trim();
167+
},
168+
onError(error) {
169+
console.error("chat failed:", error.message);
170+
},
171+
};
172+
```
173+
174+
- `onBeforeSend(messages, ctx)` — return a new messages array, or call
175+
`ctx.respondWith(text)` to answer without calling Claude.
176+
- `onAfterReceive(reply, ctx)` — return a new reply string.
177+
- `onError(error, ctx)` — observe failures.
178+
179+
### Server reference plugins
180+
181+
The same three plugins, adapted for the request lifecycle:
182+
183+
```ts
184+
import {
185+
chatPlugins,
186+
pluginAnalytics,
187+
pluginRedactPII,
188+
pluginCannedResponses,
189+
} from "./plugins";
190+
191+
const serverPlugins = [
192+
pluginRedactPII(), // redact every message before it reaches Claude
193+
pluginCannedResponses({
194+
rules: [{ match: /refund/i, reply: "See our refund policy at /refunds." }],
195+
}),
196+
pluginAnalytics({ onEvent: (e) => console.log(e) }),
197+
];
198+
199+
app.use("/api/chat", chatPlugins(serverPlugins));
200+
```
201+
202+
Redacting server-side is defense in depth — it runs even for clients that don't
203+
ship the widget redactor. A canned (short-circuit) response skips the model, and
204+
with it the built-in D1 analytics for that turn.
205+
206+
## Notes
207+
208+
- Hooks run in array order; the first `respondWith` / `abort` wins and stops
209+
that hook's chain.
210+
- A throwing hook is caught and logged — it never breaks the chat.
211+
- The client and server interfaces are parallel but not identical: a widget
212+
message carries an `id` and `sources`; the server works on plain
213+
`{ role, content }` messages.
214+
215+
## Other extension points
216+
217+
These complement plugins and cover most customization without code:
10218

11219
| Extension point | What it controls |
12220
|-----------------|------------------|
13221
| [System prompt](/configuration/worker/#system-prompt) (`worker/src/system-prompt.ts`) | The bot's personality, knowledge, guardrails, and FAQ answers |
14222
| [Translations](/configuration/localization/) | Every user-facing UI string |
15-
| [Theming options](/configuration/theming/) and `widget/tailwind.config.ts` | Colors, fonts, radii |
223+
| [Theming](/configuration/theming/) and `widget/tailwind.config.ts` | Colors, fonts, radii |
16224
| [Proactive triggers](/configuration/triggers/) | When the widget opens or greets proactively |
17225
| Worker env vars (`CLAUDE_MODEL`, `MAX_TOKENS`, rate limits) | Model behavior and abuse protection |
18-
| Fork-level: `widget/src/` and `worker/src/` | Anything else — both packages are small, typed, and well-tested |
19-
20-
For testing integrations, the widget exports a `MockChatApiClient` test
21-
utility (`widget/src/test-utils/`) that fakes the API client without network
22-
calls.
23226

24227
## Planned
25228

26-
These are tracked on the [v1.3.0 and v2.0.0 milestones](https://github.com/PMDevSolutions/Claudius/milestones):
27-
28229
- **Anthropic tool-use / function calling** with a declarative tool registry,
29230
so the bot can call your APIs mid-conversation —
30231
[#51](https://github.com/PMDevSolutions/Claudius/issues/51)
31-
- **Plugin/hook SDK** for message middleware: pre-send and post-receive
32-
transforms — [#45](https://github.com/PMDevSolutions/Claudius/issues/45)
33232
- **Plugin SDK RFC for Claudius v2** — the longer-term plugin architecture —
34233
[#79](https://github.com/PMDevSolutions/Claudius/issues/79)
35-
36-
Nothing on this list is shipped yet. If you need one of these, comment on the
37-
issue — it helps prioritization.

widget/src/components/ChatWidget.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import { resolveTranslations, type LocaleCode } from "../locales";
1414
import { useTheme } from "../theme/useTheme";
1515
import type { ClaudiusThemeInput } from "../theme/types";
16+
import type { ClaudiusPlugin } from "../plugins/types";
1617

1718
/** Corner of the viewport the widget docks to. */
1819
export type WidgetPosition =
@@ -66,6 +67,12 @@ export interface ChatWidgetProps {
6667
translations?: Partial<ClaudiusTranslations>;
6768
/** Proactive open/greeting rules evaluated against the current page. */
6869
triggers?: Trigger[];
70+
/**
71+
* Middleware run around each message: `onBeforeSend`, `onAfterReceive`, and
72+
* `onError`. Hooks run in array order and may modify, replace, or
73+
* short-circuit messages. See {@link ClaudiusPlugin}.
74+
*/
75+
plugins?: ClaudiusPlugin[];
6976
}
7077

7178
function readDismissed(): boolean {
@@ -111,6 +118,7 @@ export function ChatWidget({
111118
locale,
112119
translations: translationOverrides,
113120
triggers,
121+
plugins,
114122
}: ChatWidgetProps) {
115123
const [isOpen, setIsOpen] = useState(false);
116124
const [greeting, setGreeting] = useState<string | null>(null);
@@ -129,6 +137,7 @@ export function ChatWidget({
129137
storageKeyPrefix,
130138
timeoutMs: requestTimeoutMs,
131139
translations,
140+
plugins,
132141
});
133142
const toggleRef = useRef<HTMLButtonElement>(null);
134143
const prevOpenRef = useRef(isOpen);

0 commit comments

Comments
 (0)