Merge hooks and custom-tools into unified extensions system (#454)

Breaking changes:
- Settings: 'hooks' and 'customTools' arrays replaced with 'extensions'
- CLI: '--hook' and '--tool' flags replaced with '--extension' / '-e'
- API: HookMessage renamed to CustomMessage, role 'hookMessage' to 'custom'
- API: FileSlashCommand renamed to PromptTemplate
- API: discoverSlashCommands() renamed to discoverPromptTemplates()
- Directories: commands/ renamed to prompts/ for prompt templates

Migration:
- Session version bumped to 3 (auto-migrates v2 sessions)
- Old 'hookMessage' role entries converted to 'custom'

Structural changes:
- src/core/hooks/ and src/core/custom-tools/ merged into src/core/extensions/
- src/core/slash-commands.ts renamed to src/core/prompt-templates.ts
- examples/hooks/ and examples/custom-tools/ merged into examples/extensions/
- docs/hooks.md and docs/custom-tools.md merged into docs/extensions.md

New test coverage:
- test/extensions-runner.test.ts (10 tests)
- test/extensions-discovery.test.ts (26 tests)
- test/prompt-templates.test.ts
This commit is contained in:
Mario Zechner 2026-01-05 01:43:35 +01:00
parent 9794868b38
commit c6fc084534
112 changed files with 2842 additions and 6747 deletions

View file

@ -1,5 +1,5 @@
/**
* Extension system - unified hooks and custom tools.
* Extension system for lifecycle events and custom tools.
*/
export { discoverAndLoadExtensions, loadExtensions } from "./loader.js";

View file

@ -4,7 +4,7 @@
* Extensions are TypeScript modules that can:
* - Subscribe to agent lifecycle events
* - Register LLM-callable tools
* - Register slash commands, keyboard shortcuts, and CLI flags
* - Register commands, keyboard shortcuts, and CLI flags
* - Interact with the user via UI primitives
*/
@ -16,7 +16,7 @@ import type { Theme } from "../../modes/interactive/theme/theme.js";
import type { CompactionPreparation, CompactionResult } from "../compaction/index.js";
import type { EventBus } from "../event-bus.js";
import type { ExecOptions, ExecResult } from "../exec.js";
import type { HookMessage } from "../messages.js";
import type { CustomMessage } from "../messages.js";
import type { ModelRegistry } from "../model-registry.js";
import type {
BranchSummaryEntry,
@ -119,7 +119,7 @@ export interface ExtensionContext {
}
/**
* Extended context for slash command handlers.
* Extended context for command handlers.
* Includes session control methods only safe in user-initiated commands.
*/
export interface ExtensionCommandContext extends ExtensionContext {
@ -228,7 +228,7 @@ export interface SessionBeforeCompactEvent {
export interface SessionCompactEvent {
type: "session_compact";
compactionEntry: CompactionEntry;
fromHook: boolean;
fromExtension: boolean;
}
/** Fired on process exit */
@ -258,7 +258,7 @@ export interface SessionTreeEvent {
newLeafId: string | null;
oldLeafId: string | null;
summaryEntry?: BranchSummaryEntry;
fromHook?: boolean;
fromExtension?: boolean;
}
export type SessionEvent =
@ -442,7 +442,7 @@ export interface ToolResultEventResult {
}
export interface BeforeAgentStartEventResult {
message?: Pick<HookMessage, "customType" | "content" | "display" | "details">;
message?: Pick<CustomMessage, "customType" | "content" | "display" | "details">;
systemPromptAppend?: string;
}
@ -477,7 +477,7 @@ export interface MessageRenderOptions {
}
export type MessageRenderer<T = unknown> = (
message: HookMessage<T>,
message: CustomMessage<T>,
options: MessageRenderOptions,
theme: Theme,
) => Component | undefined;
@ -547,7 +547,7 @@ export interface ExtensionAPI {
// Command, Shortcut, Flag Registration
// =========================================================================
/** Register a custom slash command. */
/** Register a custom command. */
registerCommand(name: string, options: { description?: string; handler: RegisteredCommand["handler"] }): void;
/** Register a keyboard shortcut. */
@ -576,7 +576,7 @@ export interface ExtensionAPI {
// Message Rendering
// =========================================================================
/** Register a custom renderer for HookMessageEntry. */
/** Register a custom renderer for CustomMessageEntry. */
registerMessageRenderer<T = unknown>(customType: string, renderer: MessageRenderer<T>): void;
// =========================================================================
@ -585,7 +585,7 @@ export interface ExtensionAPI {
/** Send a custom message to the session. */
sendMessage<T = unknown>(
message: Pick<HookMessage<T>, "customType" | "content" | "display" | "details">,
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
): void;
@ -638,7 +638,7 @@ export interface ExtensionShortcut {
type HandlerFn = (...args: unknown[]) => Promise<unknown>;
export type SendMessageHandler = <T = unknown>(
message: Pick<HookMessage<T>, "customType" | "content" | "display" | "details">,
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
) => void;