Extensions
Extensions are middleware. They install once, hook into the message pipeline and lifecycle, and can register slash commands. They live in user code — there's no separate package.
Lifecycle
interface IExtension {
readonly name: string;
readonly version: string;
install(context: ExtensionContext): void;
uninstall?(): void;
}
interface ExtensionContext {
onBeforeSend(handler: (msg: OutgoingMessage) => OutgoingMessage | null): void;
onAfterReceive(handler: (msg: IncomingMessage) => IncomingMessage | null): void;
onWidgetOpen(handler: () => void): void;
onWidgetClose(handler: () => void): void;
registerCommand(command: ISlashCommand): void;
}
Returning null from onBeforeSend cancels the send. Returning null from onAfterReceive drops the incoming message before it's stored.
Install
import { ExtensionRegistry } from "@chativa/core";
ExtensionRegistry.install(new AnalyticsExtension({ trackingId: "UA-..." }));
Hooks fire in install order. uninstall() is called only if you explicitly remove the extension.
Example — analytics
import type { IExtension, ExtensionContext } from "@chativa/core";
export class AnalyticsExtension implements IExtension {
readonly name = "analytics";
readonly version = "1.0.0";
constructor(private readonly opts: { trackingId: string }) {}
install(ctx: ExtensionContext): void {
ctx.onBeforeSend((msg) => {
window.gtag?.("event", "chat_message_sent", {
tracking_id: this.opts.trackingId,
type: msg.type,
});
return msg; // return msg to keep, null to cancel
});
ctx.onAfterReceive((msg) => {
if ((msg.data as { text?: string }).text === "[redacted]") return null;
return msg;
});
ctx.onWidgetOpen(() => window.gtag?.("event", "chat_opened"));
}
}
Example — register a slash command from an extension
import type { IExtension, ExtensionContext } from "@chativa/core";
export class HelpExtension implements IExtension {
readonly name = "help";
readonly version = "1.0.0";
install(ctx: ExtensionContext): void {
ctx.registerCommand({
name: "help",
description: () => "Show available commands",
execute({ args }) {
console.log("Help requested with args:", args);
},
});
}
}
Tip: prefer EventBus for read-only analytics
If you only want to observe events (not transform messages), the typed EventBus is lighter — no extension lifecycle, just a subscribe:
import { EventBus } from "@chativa/core";
EventBus.on("message_sent", (msg) => track("sent", msg));
EventBus.on("widget_opened", () => track("opened"));
EventBus.on("genui_stream_completed", ({ streamId }) => track("genui_done", { streamId }));
Full event payload map: packages/core/src/application/EventBus.ts.
Tip — use the scaffolder
The /new-extension slash command scaffolds the extension class, test, and a sandbox-ready install snippet. See .claude/commands/new-extension.md.