Skip to main content

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.