Skip to content

feat(plugins): message-middleware plugin SDK for widget and worker#134

Merged
PAMulligan merged 1 commit into
mainfrom
45-pluginhook-sdk-for-message-middleware-pre-send-and-post-receive-transforms
Jun 19, 2026
Merged

feat(plugins): message-middleware plugin SDK for widget and worker#134
PAMulligan merged 1 commit into
mainfrom
45-pluginhook-sdk-for-message-middleware-pre-send-and-post-receive-transforms

Conversation

@PAMulligan

Copy link
Copy Markdown
Contributor

Closes #45.

Introduces 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. Both the client (widget) and server (worker) expose equivalent lifecycle hooks.

Lifecycle hooks

Hook Runs Can
onBeforeSend(message, ctx) before a message is sent modify, replace, answer locally (ctx.respondWith), or cancel (ctx.abort)
onAfterReceive(message, ctx) after the reply arrives modify or replace the reply
onError(err, ctx) when a send fails observe; (client) recover with a fallback reply

Hooks may be async. A hook that throws is caught and logged — one bad plugin can't break the chat.

Client (widget)

import { ChatWidget, pluginRedactPII, pluginAnalytics } from "claudius-chat-widget";

<ChatWidget
  apiUrl="https://your-worker.workers.dev"
  plugins={[pluginRedactPII(), pluginAnalytics({ onEvent: (e) => gtag("event", e.type, e) })]}
/>;
  • ChatWidget accepts a plugins: ClaudiusPlugin[] prop; hooks run in array order, wired through useChat.
  • ctx.respondWith(reply) short-circuits the network (canned responses); ctx.abort() drops the message; onError can respondWith a fallback instead of the error UI.
  • Reference plugins exported from the package: pluginAnalytics, pluginRedactPII, pluginCannedResponses.

Server (worker)

The Worker exposes the equivalent hooks as Hono middleware over { role, content } messages:

import { chatPlugins, pluginRedactPII } from "./plugins";

const serverPlugins = [pluginRedactPII({ redactReplies: true })];
if (serverPlugins.length > 0) {
  app.use("/api/chat", chatPlugins(serverPlugins));
}

chatPlugins short-circuits via respondWith, hands the transformed request to the route via c.get("chatRequest"), and rewrites the reply on onAfterReceive. It's wired into worker/src/index.ts as an opt-in extension point — empty by default, so existing behavior is unchanged. The same three reference plugins ship server-side.

Design notes

  • Parallel, not shared. The widget message has id/sources; the worker uses { role, content }. The two interfaces mirror each other (matching the issue's "equivalent hooks" wording) without a fragile cross-package shared module.
  • Short-circuit semantics live on the context (respondWith / abort), keeping hook return types simple (return a transformed message, or nothing).
  • Server default is inert: with no plugins configured the middleware isn't registered and the handler reads the body exactly as before.

Incidental fix

Type-checking the new worker code surfaced a latent noImplicitAny error in the CORS origin callback (worker/src/index.ts) — the worker CI runs only vitest (esbuild strips types), so tsc was never run there. Fixed with a one-line annotation. Happy to split this out if preferred.

Docs

  • Rewrote docs/plugins with copy-paste client + server examples (the page previously listed this as "planned").
  • Added the plugins prop to the widget options table.
  • The 1.x/ archived docs snapshot is left to the starlight-versions tooling (not hand-edited). Docs build is validated by the docs.yml CI gate.

Verification

  • Widget: lint (0 errors), format:check, typecheck, docs:api:check, build, check:exports (attw + publint all green) — 287 unit tests + 10 e2e pass.
  • Worker: tsc --noEmit clean — 39 tests pass (runner, reference plugins, isolated chatPlugins middleware, and a real-app success path covering the stashed-request read).
  • New tests cover transform/short-circuit/abort/recovery on both sides.

🤖 Generated with Claude Code

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>
@PAMulligan PAMulligan linked an issue Jun 19, 2026 that may be closed by this pull request
6 tasks
@PAMulligan PAMulligan merged commit fc79f61 into main Jun 19, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plugin/hook SDK for message middleware (pre-send and post-receive transforms)

1 participant