= (t, s) => s.elements.Text({ children: t })
*/
export type ClientModule = (props: P, surface: ClientSurface) => RenderElement;
/**
* One pointer event over a `Client`'s region, as `surface.onPointer` hands
* it: region-relative cells, and the sub-cell position where known.
*
* After a `down` in the region the instance holds the pointer until the
* `up`: every `move` reaches it, past the edges too (negative, or beyond
* `columns`/`rows`), and the transcript neither selects nor scrolls.
*/
export type ClientPointerEvent = {
type: ClientPointerType;
/**
* The column under the pointer, 0 at the region's left edge; on `enter`
* and `leave`, the last column a move reported.
*/
x: number;
/**
* The row under the pointer, 0 at the region's top edge.
*/
y: number;
/**
* The same position with the fraction of a cell kept (`x: 12`, `fine.x:
* 12.375`), where the surface knows it: a terminal that reports pixels.
*
* kitty, Ghostty, iTerm2, WezTerm and foot do; tmux passes none through.
* Absent elsewhere and on `enter`/`leave`; present, a `move` comes within
* a cell too. What a module showing another program's pixels forwards.
*
* @example
* surface.onPointer(e => click((e.fine?.x ?? e.x + 0.5) * pixelsPerCell))
*/
fine?: {
x: number;
y: number;
};
/**
* Which button is down (`down`, `move` while held) or came up (`up`);
* absent on a hover `move`, `enter` and `leave`.
*/
button?: 'left' | 'middle' | 'right';
/**
* Modifier keys the terminal reported held, each present only when true.
*/
shift?: true;
alt?: true;
ctrl?: true;
};
/**
* What the pointer did over a `Client`'s region: a button went down, the
* pointer moved, the button came up, or the pointer crossed the region's edge.
*/
export type ClientPointerType = 'down' | 'move' | 'up' | 'enter' | 'leave';
/**
* The props of `Client`: which of the plugin's surface modules draws here,
* under what key, with what data, in how much room.
*/
export type ClientProps = {
/**
* The instance's address within the drawing: two `Client`s of one plugin in
* one tree take two keys. What `e.element` carries at `ui.message`.
*
* The engine keeps the instance (its local state, its timers) across the
* plugin's redraws while a `Client` under this key stays in the tree.
*/
key: string;
/**
* The surface module's path, a string literal relative to this file:
* `module: "./.tsx"` (or `.jsx`, `.ts`, `.js`, `.mjs`).
*
* Read off the source: a variable there is refused at load, as is a path
* outside the plugin or naming no file. Its default export draws, else
* its one PascalCase export; loaded, the tree carries the plugin's path.
*/
module: string;
/**
* Plain data (JsonValue) handed to the module function; a new value on a
* redraw reaches the running instance, its state kept.
*
* Bounded as a tree's text is; not a channel for closures.
* Typed `unknown` so a matcher over a tree stays shallow.
*/
props?: unknown;
/**
* Columns the instance's region takes: a count, or a percentage of the
* parent. Absent, the region is as wide as what the module draws.
*/
width?: number | string;
/**
* Rows the instance's region takes: a count, or a percentage of the parent.
* Absent, the region is as tall as what the module draws.
*/
height?: number | string;
/**
* How the region grows into free room along the parent's direction, as a
* Box's `flexGrow`.
*/
flexGrow?: number;
};
/**
* What a surface module's function receives as its second argument: its
* elements, the instance's local state, its region, input, clock and port.
*
* Called again (same `surface`, same `state`) on new props, after
* `setState`, and on a resize; what it returns is drawn in the region. No
* `$` here: the hooks module has it, and `post` is the way to reach it.
*/
export type ClientSurface = {
/**
* The surface's element table (ClientElements), the tags the module draws
* with: `const { Box, Text } = surface.elements`. No `Client` in it.
*/
readonly elements: ClientElements;
/**
* The instance's local state: `undefined` until the first `setState`.
* Kept across the plugin's redraws; dropped with the instance.
*/
readonly state: S | undefined;
/**
* Replaces the local state and schedules one more call of the function on
* the next frame; several calls before it coalesce into one redraw.
*
* A `setState` on each of three renders in a row with no key, pointer,
* tick, press or props between is a render loop: the instance unmounts.
*/
setState: (next: S) => void;
/**
* The region's width in cells, as last laid out (0 before the first
* layout).
*/
readonly columns: number;
/**
* The region's height in cells, as last laid out (0 before the first
* layout).
*/
readonly rows: number;
/**
* Calls `fn` every `ms` milliseconds on the surface's frame clock until
* the returned function is called or the instance unmounts.
*
* Start it once (while `state` is still undefined), not on every call.
*/
every: (ms: number, fn: () => void) => () => void;
/**
* Sets the instance's pointer listener (one; a later call replaces it)
* and returns what clears it. See ClientPointerEvent for capture.
*/
onPointer: (fn: (event: ClientPointerEvent) => void) => () => void;
/**
* Sets the instance's key listener (one; a later call replaces it),
* reached while a click has given it the focus; Escape returns that.
*/
onKey: (fn: (event: ClientKeyEvent) => void) => () => void;
/**
* Sends plain data to the plugin's hooks module: `e.data` of a
* `ui.message` only that plugin's hooks see, one per frame at most.
*
* A later post in the same frame replaces an undelivered one; a hook
* answering `{ props }` hands this instance its next props. At most
* 20,000 values, 32 deep, 100,000 characters, or the post is not sent.
*/
post: (data: JsonValue) => void;
};
/**
* The argument of the `$.clock` waits (`sleep`, `after`, `every`): how long,
* in milliseconds, before the dispatch resolves.
*/
type ClockWait = {
/**
* The wait, a non-negative number of milliseconds.
*/
ms: number;
};
/**
* The props of `Code`, source text every surface draws with the engine's own
* highlighter: coloured tokens, a line gutter on request, or a unified diff.
*
* A leaf: no children. `source` is the element's data as a string is a
* Text's, bounded and free of control characters the same way; the colour
* on screen is the engine's, never the plugin's.
*/
export type CodeProps = {
/**
* The text drawn: source code, or under `format: 'diff'` one or more
* unified-diff hunks.
*
* At most 10000 characters; tab and newline are the only control
* characters it may hold.
*/
source: string;
/**
* A highlighter language id or alias (`typescript`, `ts`, `py`), a
* plugin-contributed grammar's included.
*
* Absent, the language is inferred from `path`; when neither resolves,
* the text is drawn plain.
*/
language?: string;
/**
* A file path the language is inferred from when `language` is absent:
* its extension or name, else a shebang on the first line.
*
* Drawn nowhere and never read: nothing touches the disk.
*/
path?: string;
/**
* The 1-based number of the first line of `source`: present, a dim
* right-aligned gutter numbers the lines from it; absent, no gutter.
*
* Ignored under `format: 'diff'`, whose hunks carry their own numbers.
*/
startLine?: number;
/**
* `'source'` (the default) draws `source` as code; `'diff'` reads it as
* unified-diff hunks and draws gutters, markers, add and remove colouring.
*
* A hunk is `@@ -a,b +c,d @@` then lines starting ` `, `+` or `-`; a
* leading `---`/`+++` pair and `\ No newline at end of file` are read
* past. A source that does not parse as hunks is refused.
*/
format?: 'source' | 'diff';
/**
* What a line wider than the room does, in Text's `wrap` vocabulary:
* `'wrap'` (the default) continues it on rows under the gutter.
*
* `'truncate-end'` cuts it at the edge with an ellipsis; Text's other
* spellings are refused, since a gutter leaves them no sense here.
*/
wrap?: 'wrap' | 'truncate-end';
};
/**
* The input of `command.describe`: how one slash command presents in the
* typeahead and `/help`, at the moment the engine lists it.
*/
export type CommandDescribeInput = {
/**
* Names the command without its slash; the key a matcher narrows on. A
* rewrite is refused.
*/
command: string;
/**
* The one-line description the menu shows, as the command declares it.
*/
description: string;
/**
* The hint drawn dim after the name (`[name]`), when the command has one.
*/
argumentHint?: string;
/**
* Whether the typeahead and `/help` leave the command out; a hidden
* command still runs when typed in full.
*/
isHidden: boolean;
/**
* Whether the command declares it runs at once when typed mid-turn,
* instead of waiting for the turn to end.
*
* One that decides per invocation reads false. Read only: not part of
* what a hook answers, and a rewrite is refused.
*/
immediate: boolean;
/**
* Who provides this command: the plugin and its tier; `{ plugin: "engine",
* tier: "core" }` for a built-in. Pinned: a rewrite is refused.
*/
provider: Origin;
};
/**
* What a `command.describe` hook returns: the description, hint and hidden
* flag the menu uses; the name, `immediate` and `provider` stay as they were.
*/
export type CommandDescribeResult = Omit;
/**
* One slash command as `$.command.list()` returns it.
*/
export type CommandInfo = {
/**
* What the person runs it by, without the slash.
*/
name: string;
/**
* The one line the typeahead shows for it.
*/
description: string;
/**
* Where it comes from (CommandSource).
*/
source: CommandSource;
/**
* Which plugin added it, when `source` is `plugin` and the engine knows:
* the one that registered it, or whose manifest carries it.
*/
plugin?: string;
};
/**
* Where a command's answer will show: which of the terminal's two layouts
* the surface renders, and how wide it is when the command runs.
*
* A fact the engine stamps on `command.run`, so a command that draws (opens
* a pane, prints a wide table) can suit the room it has: the fullscreen
* layout docks a pane beside the transcript from 110 columns, the main
* screen places it inline above the prompt at any width.
*/
export type CommandPresentation = {
/**
* True under the fullscreen (alternate-screen) layout; false on the main
* screen (`CLAUDE_CODE_NO_FLICKER=0`, tmux by default) and headless.
*
* The fact `RenderViewport`'s `isFullscreen` carries on every terminal
* drawing, from the same source: the two agree.
*/
isFullscreen: boolean;
/**
* The terminal's width in cells as the command runs; 80 where no terminal
* has measured (headless with no tty).
*/
columns: number;
};
/**
* `command.run`'s input as a plugin's `$.command.run` takes it: `args` may
* be left out (`/command`, bare); `origin` and `presentation` the engine sets.
*/
export type CommandRunArgs = Omit & {
/**
* Everything after the name, as the person would type it; left out, `""`.
*/
args?: string;
};
/**
* The input of `command.run`: one slash command about to run, the way the
* person typed it (`/name args`), and where the run came from.
*/
export type CommandRunInput = {
/**
* Names the command without its slash (`compact`, `hello`), aliases and
* folds resolved; the key a matcher narrows on. A rewrite is refused.
*/
command: string;
/**
* Everything after the name, as typed (`""` when nothing was); a hook
* rewrites it with `next({ ...e, args })`.
*/
args: string;
/**
* Where the run came from, in `prompt.submit`'s words (PromptOrigin):
* the person's Enter (`composer`), the bridge, the SDK, or a plugin.
*
* A plugin's `$.command.run` reads `{ kind: 'plugin', name }`. `next(e)`
* passes it on as received.
*/
origin: PromptOrigin;
/**
* Where the command's answer will show (CommandPresentation): the
* fullscreen layout or the main screen, and the terminal's width.
*
* Pinned: the engine stamps it, `next(e)` passes it on, a rewrite that
* leaves it out keeps it and one that changes it is refused.
*/
presentation: CommandPresentation;
};
/**
* What a `command.run` hook returns and what `next(e)` and `$.command.run`
* resolve to: the command's output text and the notes it leaves the model.
*
* From core, `text` is what the command printed (a `local` command's
* returned text; a panel command may print nothing), `context` what it
* recorded for the model beside that, and `ref` names the run.
*
* A hook's own answer without `next` runs no command: its `text` is shown
* as the command's output, under the names of the plugins hooking the
* command unless each is bundled with Claude Code, whose answer reads as
* the built-in command's own, and its `context` is recorded after it.
*
* Both are the plugin's to size, as a core command's output is; what bounds
* them is what bounds any transcript row where a surface draws it.
*/
export type CommandRunResult = {
/**
* The command's output as a transcript line, or undefined when the
* command showed nothing as text (a panel, a prompt for the model).
*/
text?: string;
/**
* What the model reads after the command's output and the person never
* sees, each entry one hidden user message recorded after the output row.
*
* From core, the notes the command left the model, absent when none. Kept
* whole from `next`: left out after `next`, the last answer's notes ride
* along; written, it keeps every entry that answer had (none empty).
*/
context?: readonly string[];
/**
* Set by core on what `next(e)` resolves to: names the engine's run of
* the command (its result stays on the host side).
*
* A hook that returns the object it got makes the engine use that run
* verbatim. Absent on a hook's own `{ text }` and on `$.command.run`'s.
*/
ref?: number;
};
/**
* Where a slash command comes from, as `$.command.list()` tells them apart.
*
* `builtin` ships with Claude Code; `plugin` is a plugin's markdown command,
* skill or `$.command.register`; `user` is the user's or project's own
* file; `mcp` an MCP server's prompt.
*/
export type CommandSource = 'builtin' | 'plugin' | 'user' | 'mcp';
/**
* What `$.command.register` takes: the slash command this plugin serves.
*/
export type CommandSpec = {
/**
* The command's name without the slash (letters, digits, `_`, `-`; up to
* 64); the person runs it as `/`.
*/
name: string;
/**
* The one line the typeahead and `/help` show for it.
*/
description: string;
/**
* The hint drawn dim after the name (`[name]`), when it takes arguments.
*/
argumentHint?: string;
/**
* Set so that `/` typed while a turn is in flight runs at once
* instead of waiting for the turn to end, as it does when left out.
*
* Its `command.run` hook then runs while a turn may still be streaming and
* must not assume the turn's state (what the transcript holds, whether a
* tool is mid-call); its `{ text }` prints as an idle run's does.
*/
immediate?: true;
};
type ConfigChangeHookInput = BaseHookInput & {
hook_event_name: 'ConfigChange';
source: 'user_settings' | 'project_settings' | 'local_settings' | 'policy_settings' | 'skills';
file_path?: string;
};
/**
* The input of `config.describe`: how one `/config` row presents, at the
* moment the menu lists it (and for `$.config.list`).
*/
export type ConfigDescribeInput = {
/**
* Names the row, as `config.set`'s `key` does; the key a matcher narrows
* on. Pinned: a rewrite is refused.
*/
key: string;
/**
* What the menu draws for the row, before its value.
*/
label: string;
/**
* The help text beneath the label, when the row has one (a plugin
* field's `description`); absent for the panel's own rows.
*/
description?: string;
/**
* Whether the menu leaves the row out; a hidden row still answers
* `$.config.set` and `/config key=value`.
*/
isHidden: boolean;
/**
* Who owns the row, as `config.set`'s `provider`. Pinned.
*/
provider: Origin;
};
/**
* What a `config.describe` hook returns: the label, help text and hidden
* flag the menu uses; the key and provider stay as they were.
*/
export type ConfigDescribeResult = Omit;
/**
* How a `/config` row takes its value: `boolean` toggles, `choice` picks one
* of its `options`, `text` takes a string, `number` a number.
*/
export type ConfigKind = 'boolean' | 'choice' | 'text' | 'number';
/**
* Where a `config.set` came from, in `prompt.submit`'s words: the person
* in the `/config` menu (`composer`), the bridge, or a plugin, named.
*
* Set by the engine; `next(e)` passes it on as received and none sets it.
*/
export type ConfigOrigin = {
/**
* The person's own change in the `/config` menu (a toggle, a pick, a
* typed value), or their `/config key=value`.
*/
kind: 'composer';
} | {
/**
* A `/config key=value` that arrived over the Remote Control bridge (a
* phone or web client, or a relay): not attestably the owner's hand.
*/
kind: 'bridge';
} | {
/**
* A plugin's `$.config.set`; that plugin's own hooks do not see it.
*/
kind: 'plugin';
/**
* The calling plugin's name.
*/
name: string;
};
/**
* One `/config` row as `$.config.list()` returns it: what the menu would
* draw now, after every `config.describe` hook, a hidden row left out.
*/
export type ConfigRow = {
/**
* Names the row: the panel's id for a built-in, `.` for a
* plugin's `userConfig` field; what `$.config.set` takes.
*/
key: string;
/**
* What the menu draws for the row, before its value.
*/
label: string;
/**
* The help text under the label, when the row has one.
*/
description?: string;
/**
* How the row takes its value (ConfigKind).
*/
kind: ConfigKind;
/**
* What the row holds now.
*/
value: ConfigValue;
/**
* The values a `choice` row takes, in order (a plugin string field's
* declared `options` for its row); absent otherwise.
*/
options?: readonly string[];
/**
* Who owns the row: the engine for the panel's own, else the plugin.
*/
provider: Origin;
/**
* Whether a trusted source (managed settings, the organization's policy)
* owns the value, so the menu shows it and refuses a change.
*/
isLocked: boolean;
};
/**
* `config.set`'s input as a plugin's `$.config.set(args)` takes it: the
* row's key and the value; the engine fills the rest.
*/
export type ConfigSetArgs = Pick;
/**
* The input of `config.set`: one `/config` row about to change, from the
* menu or a plugin's `$.config.set`, with what it holds now and who owns it.
*/
export type ConfigSetInput = {
/**
* Names the row: the panel's own id for a built-in (`theme`, `verbose`,
* `autoCompact`), `.` for a plugin's `userConfig` field.
*
* The key a matcher narrows on; pinned: a rewrite is refused.
*/
key: string;
/**
* What the row is being set to; `next({ ...e, value })` clamps or
* replaces it, held to the row's kind (a toggle takes a boolean).
*/
value: ConfigValue;
/**
* What the row holds before the change, as the menu shows it. Pinned.
*/
previous: ConfigValue;
/**
* Who owns the row: `{ plugin: "engine", tier: "core" }` for the panel's
* own, the plugin and its tier for a `userConfig` field. Pinned.
*/
provider: Origin;
/**
* Where the change came from (ConfigOrigin): the person in the menu
* (`composer`) or a plugin's `$.config.set`. Pinned.
*/
origin: ConfigOrigin;
};
/**
* What a `config.set` hook returns and what `next(e)` resolves to:
* `{ value }` once written, or `{ deny: reason }`, the row left as it was.
*
* The menu shows a deny's reason beside the row; a plugin's `$.config.set`
* resolves with it.
*/
export type ConfigSetResult = {
value: ConfigValue;
deny?: undefined;
} | {
deny: string;
value?: undefined;
};
/**
* A `/config` row's value as a hook and `$.config` see it: a toggle's
* boolean, a choice's or a text's string, a number, or a list of strings.
*/
export type ConfigValue = boolean | string | number | readonly string[];
/**
* One custom agent whose description the Agent tool's prompt carries;
* built-in agents are left out.
*/
export type ContextAgent = {
/**
* The agent's type, as the Agent tool names it.
*/
agentType: string;
/**
* Where it was defined, by the engine's word (`projectSettings`,
* `userSettings`, `plugin`); the display label is the renderer's.
*/
source: string;
/**
* The description's estimated tokens.
*/
tokens: number;
};
/**
* The token counts the last API response of the live window reported, as
* the API spells them; the breakdown's `Messages` row is reconciled to it.
*/
export type ContextApiUsage = {
/**
* Uncached input tokens the response was answered over.
*/
input_tokens: number;
/**
* Tokens the response generated.
*/
output_tokens: number;
/**
* Input tokens written to the prompt cache by the request.
*/
cache_creation_input_tokens: number;
/**
* Input tokens read from the prompt cache by the request.
*/
cache_read_input_tokens: number;
};
/**
* How a context breakdown is counted: `full` with the token-count API per
* category, `summary` from the last response's usage and local estimates.
*
* The SDK's `get_context_usage` takes the same two words as its `detail`.
*/
export type ContextBreakdownDetail = 'summary' | 'full';
/**
* One row of the breakdown, as /context lists it beside the grid (`System
* prompt`, `Messages`, `Free space`, `Autocompact buffer`).
*/
export type ContextCategory = {
/**
* The row's label as /context prints it.
*/
name: string;
/**
* The row's estimated tokens; a `deferred` row's do not count toward the
* total.
*/
tokens: number;
/**
* The theme colour /context draws the row and its squares in, by its key
* in the theme (`promptBorder`, `inactive`, `permission`).
*/
color: string;
/**
* Whether the row is tool schemas loaded on demand, which the grid leaves
* out; the same fact as `kind` `deferred`.
*/
isDeferred: boolean;
/**
* What the row is (ContextCategoryKind), stamped by the engine.
*/
kind: ContextCategoryKind;
};
/**
* What a breakdown row is; branch on this, never on the row's `name`.
*
* `used` content occupies the window, `free` is the window left, `buffer`
* the compaction reserve, `deferred` tool schemas loaded on demand and
* outside the window.
*/
export type ContextCategoryKind = 'used' | 'free' | 'buffer' | 'deferred';
/**
* One square of the grid /context draws: which row it belongs to and how
* full it is.
*/
export type ContextGridSquare = {
/**
* The theme colour of the square's row, by its key in the theme.
*/
color: string;
/**
* Whether the square holds any of its row's tokens.
*/
isFilled: boolean;
/**
* The `name` of the row the square belongs to (`Free space` for the
* window left).
*/
categoryName: string;
/**
* The row's tokens, repeated on each of its squares.
*/
tokens: number;
/**
* The row's share of the window as a whole percentage, repeated likewise.
*/
percentage: number;
/**
* How full this one square is, 0 to 1: a row's last square is the partial
* one (/context draws it hollow under 0.7).
*/
squareFullness: number;
};
/**
* One MCP tool's schema as the context carries it.
*/
export type ContextMcpTool = {
/**
* The tool's wire name (`mcp__linear__create_issue`).
*/
name: string;
/**
* The server it belongs to, as /mcp lists it.
*/
serverName: string;
/**
* The schema's estimated tokens.
*/
tokens: number;
/**
* Whether the schema is inside the window now: always, unless tool
* schemas load on demand and this one has not been searched for yet.
*/
isLoaded: boolean;
};
/**
* One memory file the context carries (a CLAUDE.md, a rules file, an
* auto-memory entry).
*/
export type ContextMemoryFile = {
/**
* The file's path, absolute.
*/
path: string;
/**
* The display label of where it was loaded from (`Project`, `User`,
* `Local`, `Managed`, `AutoMem`).
*/
type: string;
/**
* The file's estimated tokens.
*/
tokens: number;
};
/**
* One skill whose listing the context carries.
*/
export type ContextSkill = {
/**
* The skill's name as `/skills` lists it.
*/
name: string;
/**
* Where it came from, by the engine's word (`userSettings`, `plugin`,
* `built-in`, `mcp`, `syncedSkills`); the display label is the renderer's.
*/
source: string;
/**
* The providing plugin's name, when the skill comes from one.
*/
pluginName?: string;
/**
* The listing's estimated tokens.
*/
tokens: number;
};
/**
* The skills the context lists for the model: how many there are, how many
* fit the listing's budget, and each one's share.
*/
export type ContextSkills = {
/**
* How many skills the session has.
*/
totalSkills: number;
/**
* How many the listing included within its token budget.
*/
includedSkills: number;
/**
* The listing's tokens in all.
*/
tokens: number;
/**
* One entry per listed skill.
*/
skillFrontmatter: ContextSkill[];
};
/**
* The slash commands the Skill tool's prompt lists, counted.
*/
export type ContextSlashCommands = {
/**
* How many commands the session has.
*/
totalCommands: number;
/**
* How many the listing included.
*/
includedCommands: number;
/**
* The listing's tokens in all.
*/
tokens: number;
};
/**
* How the window the breakdown measures against was settled; /context
* prints its `Auto-compact window` line off this.
*
* `auto` is the model's own limit; the rest name a compaction window by who
* set it: the `CLAUDE_CODE_AUTO_COMPACT_WINDOW` variable, the settings, the
* account, an experiment, the model's default, or an unrecognised model's.
*/
export type ContextWindowSource = 'env' | 'settings' | 'clientdata' | 'experiment' | 'model-default' | 'unknown-model' | 'auto';
/**
* The plugin's identity (`plugin`) and the nouns core contributes to `$` as
* the innermost step of the `engine.create` fold.
*
* A plugin's finished `$` (EngineInterface) has every core noun an outer
* step did not withhold, and every noun the plugins' steps added.
*/
export interface CoreEngineInterface {
/**
* This plugin, as loaded: its manifest name and its directory.
*/
plugin: {
/**
* From plugin.json; debug-log and `$.ui.log` lines carry it.
*/
name: string;
/**
* The plugin's directory (the one holding plugin.json), absolute.
*/
root: string;
};
/**
* Display: a line under an open dialog, a redraw or a repaint, a
* transcript or debug line, panes the surface places, a window or ring.
*/
ui: {
/**
* Shows `text` as one line under the dialog open for `tool_use_id`, or
* removes the line when `text` is undefined.
*
* Core removes the line when the call resolves. A call that is not open
* is refused, as is another plugin's `$.tool.call` run.
*
* @param tool_use_id the call whose open dialog gets the line
* @param text the line to show; undefined removes it
* @example
* $.ui.notice(e.tool_use_id, "checked by my-plugin"); return next(e)
*/
notice: (tool_use_id: string, text: string | undefined) => void;
/**
* Re-runs an event whose results the engine caches: `ui.render` draws the
* instances this plugin may draw again; the others drop the cached answers.
*
* A render hook whose state changed (a countdown) calls it for a redraw, at
* most ten a second, thirty for the shown pane and the band (calls sooner
* fold); a prompt section, context or attachment hook: dropped next turn.
*
* @param event `ui.render`, or a cached-answer event: `prompt.section`,
* `prompt.context`, `prompt.attachment`, `tool/command/config.describe`
*/
invalidate: (event: InvalidatableEventName) => void;
/**
* Repaints a mounted `Raster` this plugin's own render hook drew with new
* cells, or swaps a keyed `Image` it drew to a new source; no redraw.
*
* The surface paints the cells or source at its next frame, so blits
* between frames fold into one: up to 120 a second taken, some sixty
* shown. An Image swap sends one small command; a resize is a redraw.
*
* @param args `requestId` (the site), `key`, and `cells` (RasterProps) or
* `source` (ImageSource); `columns`, `rows` (the mounted size)
* @returns `{}` once the cells or source are its next frame, or `{ deny }`
* (not mounted, not this plugin's, another size, cells that do
* not decode, a bad source, or an Image drawing its alt there:
* no placeholder images, or a file this terminal cannot read)
* @example
* $.clock.every(33, () => $.ui.blit({ requestId, key, cells: frame() }))
* @example
* await $.ui.blit({ requestId: 'browser', key: 'view',
* source: { shm: name, format: 'rgb', width, height } })
*/
blit: (args: UiBlitArgs) => Promise;
/**
* The elements of the surface `e` is drawn on (Elements[e.surface]): a
* frozen table of constructors, the JSX tags a render hook draws with.
*
* A read, not a dispatch: the engine ran `ui.resolve` (the other hooks,
* then core) at load, per surface and component. Narrowed `e.surface`:
* that table exactly; unnarrowed: the union, so shared names type-check.
*
* @param e this hook's own `ui.render` argument (its surface and
* component pick the table)
* @returns the surface's frozen element table (`Elements[e.surface]`)
* @example
* const { Box, Text } = $.ui.resolve(e)
* return {await next(e)}done
*/
resolve: (e: E) => Elements[E['surface']];
/**
* Appends one line to the transcript, drawn like a system notice (dim;
* not sent to the model), or with `{ to: "debug" }` to the debug log alone.
*
* A row of its own at the next frame, in logging order; a `-p` or SDK
* host receives it as `ui_log`; the debug log has every line under this
* plugin's name. Raised as `ui.log`: a hook above may rewrite `e.to`.
*
* @param text the line's text
* @param options `to`: `transcript` (the default) or `debug`
* @example
* $.ui.log(`prompt from ${e.origin.kind}: ${e.text.length} chars`)
* @example
* $.ui.log(`cache miss for ${e.tool_use_id}`, { to: "debug" })
*/
log: (text: string, options?: UiLogOptions) => void;
/**
* Asks the user `question` in the engine's own AskUserQuestion dialog and
* resolves to the label they chose, or the text typed under "Other".
*
* A `tool.call` of `AskUserQuestion` through every hook but the calling
* one, drawn by `ui.render` on `AskUserQuestion`; a multi-select answer is
* comma-joined. Rejects when dismissed, and in a `-p` run (no one to ask).
*
* @param question the question, ending in a question mark
* @param options 2-4 option labels, or `{ options, header, multiSelect }`
* @returns the label chosen, the chosen labels comma-joined, or free text
* typed under "Other" (so compare it with the labels exactly)
* @example
* const mood = await $.ui.ask("How careful?", ["Bold", "Careful"])
*/
ask: (question: string, options?: readonly string[] | AskOptions) => Promise;
/**
* Shows `text` on the notification bar under the prompt for a few
* seconds, the way the engine's own "context left" notice appears.
*
* It leaves the transcript and the model untouched.
*
* @param text the line to show; an unpaired surrogate half in it is drawn
* as U+FFFD
* @param options `timeoutMs`: how long it stays (default 4000)
* @example
* $.ui.toast(`turn took ${Math.round(e.durationMs / 1000)} s`)
*/
toast: (text: string, options?: ToastOptions) => void;
/**
* Pins `text` as this plugin's status line under the prompt, beside the
* engine's own pinned notices, until the next call replaces it.
*
* One per plugin; `undefined` removes it.
*
* @param text the line to keep on screen (an unpaired surrogate half is
* drawn as U+FFFD); undefined clears it
* @example
* $.ui.status("thinking..."); return next(e)
*/
status: (text: string | undefined) => void;
/**
* Opens a pane: a framed region the surface places, whose body this
* plugin draws by hooking `ui.render` for `{ component: "Pane" }`.
*
* One instance per id (`requestId`): opening an open id retitles it; a hook
* on `ui.open` may refuse it. The keyboard is the person's; unasked, it
* waits undrawn below 144 columns (110 once asked), judged at each open.
*
* @param pane `id` (1-64 of letters, digits, `_`, `-`), `title`, `focus`,
* `closeOnEscape` and `holdToasts` (a dialog), `rows` / `columns` wanted
* @returns settles once the pane is open (or retitled)
* @example
* await $.ui.open({ id: "clock", title: "Clock" })
* @example
* await $.ui.open({ id: "ask", focus: true, closeOnEscape: true, rows: 9 })
*/
open: (pane: PaneOpenArgs) => Promise;
/**
* Closes one of the open panes; an id that is not open is left alone.
*
* Every close raises `ui.close`, `e.origin` naming whose it is: this call
* (`plugin`), the person's mark or key (`person`), an unload (`unload`).
* A hook answering without `next` keeps the pane open, save on an unload.
*
* @param pane `id`: the id the pane was opened under
* @returns settles once the pane is gone or a hook answered for it;
* rejects on a hook's `{ deny }`
* @example
* onPress: () => $.ui.close({ id: "clock" })
*/
close: (pane: PaneCloseArgs) => Promise;
/**
* Lists this plugin's own open panes (UiPane): each one's id and title,
* and whether it is shown, holds the keyboard, and is placed.
*
* The engine's record, not the module's: a module reloaded while its
* pane stayed up finds it here. Another plugin's panes are not listed.
*
* @returns the panes in open order, placed ones first; empty with none
* @example
* const isUp = (await $.ui.panes()).some(pane => pane.id === "clock")
* if (!isUp) await $.ui.open({ id: "clock", title: "Clock" })
*/
panes: () => Promise;
/**
* Scrolls something into view as the DOM's `scrollIntoView` would: a
* render instance by `requestId`, an element by `key`, a site's edge.
*
* A site of this plugin's (its pane, the band it draws into) moves under
* the event `ui.scroll`, origin `plugin`. A transcript row moves only
* while this call answers the person's own input, where one scrolls.
*
* @param args `to` (what), `in` (which site, required for `start` and
* `end`), `block` (where it lands; `nearest` by default)
* @returns `{}` once it moved, or `{ deny }` saying why not
* @example
* onPress: () => $.ui.scroll({ in: "log", to: "end" })
*/
scroll: (args: UiScrollArgs) => Promise;
/**
* Moves the focus ring of one of this plugin's sites onto an element it
* drew there, as the DOM's `element.focus()`, while it holds the keys.
*
* Raised as the event `ui.focus`, origin `plugin`; the engine's inverse
* marks the element. The keyboard is the person's to give: a site not
* holding it, or holding it on another plugin's element, is `{ deny }`.
*
* @param args `requestId` (which site: a pane's id, the band's) and `key`
* (the element's, as drawn)
* @returns `{}` once it moved, or `{ deny }` saying why not
* @example
* onPress: () => $.ui.focus({ requestId: "files", key: "row:0" })
*/
focus: (args: UiFocusArgs) => Promise;
};
/**
* Completions through the session's own client and credentials.
*/
model: {
/**
* Runs one text completion through the session's own API client and
* resolves to the reply's text.
*
* No tools, no history, no system prompt beyond the CLI's identity block
* and `request.system`.
*
* @param request the model (an alias such as `haiku`, or a full id,
* resolved like a `--model` value), the prompt, and a token cap
* @returns the reply's text
* @example
* const reply = await $.model.complete({ model: "haiku", prompt: "Hi." })
*/
complete: (request: ModelCompleteRequest) => Promise;
/**
* Runs one tool-less completion over the session's OWN transcript, sharing
* the main thread's prompt cache; the reply's text and the fork's usage.
*
* Not `complete`: sharing the cache prefix means the model and system
* prompt are the session's, read from its last turn's cache-safe snapshot,
* and tools are denied. Null on a cold snapshot or an API error.
*
* @param request the one user message the fork answers
* @returns the reply's text with the fork's usage, or null on a cold
* snapshot or an API error
* @example
* const reply = await $.model.fork({ prompt: "One line to learn?" })
* if (reply !== null) $.ui.log(`${reply.usage.output_tokens} tokens`)
*/
fork: (request: ModelForkRequest) => Promise;
/**
* Picks one of `labels` for `text` with one completion over
* `$.model.complete` and a fixed classifier prompt.
*
* `text` is data; the model answers with a label alone. It resolves
* `undefined` when the answer named none of `labels`. A failed request,
* an abort or a reply with no text rejects, naming the cause.
*
* @param text what to classify
* @param labels the labels to choose from (2 or more)
* @param options `model`: an alias or id; default the engine's small
* fast model
* @returns the label the model named, or undefined when it named none;
* rejects when the request fails or the reply has no text
* @example
* const kind = await $.model.classify(e.text, ["bug", "feature"])
* if (kind === undefined) return next(e)
*/
classify: (text: string, labels: readonly string[], options?: ClassifyOptions) => Promise;
};
/**
* Sound: clip playback and platform speech.
*/
audio: {
/**
* Plays one audio clip, starting now; clips are not queued, so two calls
* play together (a bed under speech).
*
* `{ asset }` is the plugin's own file, loaded by the engine and played
* through the platform's player (`afplay` on macOS). Resolves when
* playback ends; rejects, naming the cause, when the clip cannot play.
*
* @param clip the plugin's own file (`{ asset }`), a URL the engine
* fetches, or the bytes as base64 with their MIME type
* @param options `shouldLoop`, `gain`, and an AbortSignal that stops the
* clip
*/
play: (clip: AudioClip, options?: PlayOptions) => Promise;
/**
* Speaks `text` with the platform's own synthesizer (`say` on macOS).
* Plain text.
*
* Utterances are queued among themselves; clips are not. Resolves when
* the utterance has ended; rejects, naming the cause, when there is no
* synthesizer, the voice is not installed, or the utterance failed.
*
* @param text what to say, as plain text
* @param options `voice`: the system voice's exact name (`Samantha`);
* absent, the synthesizer's default
* @returns which synthesizer spoke, once the utterance has ended
*/
speak: (text: string, options?: SpeakOptions) => Promise;
};
/**
* The engine's connected MCP servers.
*/
mcp: {
/**
* Calls `tool` on one of the engine's connected MCP servers with the
* engine's own connection and credentials.
*
* A `cached` server is dialed on first use. No permission prompt: the
* plugin's call, seen by the hooks above it, is the grant. Positional, not
* the `{ tool: "mcp__server__tool", ... }` shape a `tool.call` hook sees.
*
* @param server the server's name as /mcp lists it (`claude.ai Gmail`;
* the tool-name spelling `claude_ai_Gmail` is accepted too)
* @param tool the tool's name on that server (`create_draft`)
* @param args the tool's arguments; none when absent
* @returns the tool's result as MCP returns it: `content` blocks and
* `isError`
* @example
* const { content } = await $.mcp.call("claude.ai Gmail", "create_draft", {
* to: "team@example.com",
* subject: "Release notes",
* })
*/
call: (server: string, tool: string, args?: Record) => Promise;
};
/**
* The running session, read as plain data, and compacting it.
*/
session: {
/**
* Returns the transcript so far, one entry per user or assistant
* message; progress rows, `$.ui.log` lines and notices are not messages.
*
* Each entry is a SessionMessage, `{ role, text, toolUses }` (a user
* message may add `toolResults`; a `toolUses` entry adds its `result` and
* `text` once answered). A long transcript answers its newest 4096.
*
* @example
* const last = (await $.session.messages()).at(-1)
*/
messages: () => Promise;
/**
* Returns the directory the session runs in, absolute.
*/
cwd: () => Promise;
/**
* Returns the session's project root, absolute: where it started, or
* where `/cd`, a host's directory change or a worktree move took it.
*
* A shell `cd` during the session does not move it; nested instruction
* files are read only beneath it.
*/
root: () => Promise;
/**
* Returns the main loop's model, as `/model` shows it.
*/
model: () => Promise;
/**
* Returns how many prompts the user has sent this session (user turns in
* the transcript).
*/
turns: () => Promise;
/**
* Returns the session's id (the transcript file's name).
*/
id: () => Promise;
/**
* Returns the git repository the session runs in, read from the working
* copy on each call; null when the directory is not inside one.
*
* @example
* const repo = await $.session.repo(); const publicRepo = !repo?.internal
*/
repo: () => Promise;
/**
* Returns every surface the session draws on, each once: `terminal` under
* the REPL first, then the remote ones in the order they attached.
*
* A session may draw on several at once (a terminal and two phones):
* clients attach (`session.attach`) and detach, and a render hook still
* reads `e.surface` per ask. Empty in a plain -p run; never rejects.
*
* @example
* const inApp = (await $.session.surfaces()).some(s => s !== "terminal")
*/
surfaces: () => Promise;
/**
* Returns the first of `$.session.surfaces()`, or null where nothing
* draws.
*
* @deprecated use `surfaces()`; a session may draw on several surfaces at
* once
*/
surface: () => Promise;
/**
* Returns the context window's fill, the rate-limit windows and the
* cost, as the status line has them; with `breakdown`, by category too.
*
* The plain call costs nothing; `"full"` counts each category with the
* token-count API as /context does, `"summary"` estimates locally, and
* `context.breakdown` comes back in the SDK's `get_context_usage` shape.
*
* @param args `{ breakdown, columns }`: how the breakdown is counted and
* the width its grid is drawn in; nothing for the status line's figures
* @returns `{ context, rateLimits, cost }` as the status line has them
* @example
* const { context } = await $.session.usage()
* if ((context.percent ?? 0) >= 85) await $.session.compact()
* @example
* const usage = await $.session.usage({ breakdown: "full", columns })
* for (const row of usage.context.breakdown?.gridRows ?? []) draw(row)
*/
usage: (args?: SessionUsageArgs) => Promise;
/**
* Returns the version of the engine the session runs on, the release it
* is built from, and when it was built.
*
* The same three values the engine's own analytics rows carry, answered
* in every mode and build; `base` is absent when the version is not
* spelled as a release, `builtAt` in a run from source that stamps none.
*
* @returns `{ version, base, builtAt }`: the full version, its release
* (`2.1.280`, or `2.1.280-dev` for a development build) and an
* ISO 8601 build time
* @example
* const { version, base, builtAt } = await $.session.version()
* row.env = { version, version_base: base, build_time: builtAt }
* @example
* const isDevelopmentBuild = (await $.session.version()).base?.endsWith("-dev")
*/
version: () => Promise;
/**
* Compacts the conversation: the event `session.compact` with `trigger`
* `plugin`, the same call `/compact` makes, between turns.
*
* It runs through every hook but the calling one, then core: a summary
* and the kept messages in the transcript's place. Resolves `{ skip }`
* when a hook vetoed it; rejects while a turn runs.
*
* @example
* const { skip } = await $.session.compact({ instructions: "the plan" })
*/
compact: EventCalls['session']['compact'];
/**
* Appends a row to a conversation of the session: the event
* `session.append`, the engine's own call for every row it keeps.
*
* A user-role row the person does not see as typed or a notice, of text
* blocks alone in this release (else refused with the reason): listed at
* once. `{ deny: reason }` when a plugin above refused it, nothing stored.
*
* @example
* await $.session.append({ message: { type: "user", content: [note] } })
*/
append: (args: SessionAppendArgs) => Promise;
/**
* Holds the session's Anthropic credential on the host and answers an
* opaque handle and its kind; the secret never reaches the plugin.
*
* The handle is spent through `$.http.fetch(url, { auth: handle })`,
* which sets the credential header, only for a first-party host. Null
* where the build or the provider has no first-party credential to hold.
*
* @example
* await $.http.fetch(url, { auth: (await $.session.authorize())?.handle })
*/
authorize: () => Promise;
};
/**
* The running model turn: ending it.
*/
turn: {
/**
* Cancels the running model turn: the one whose id `turn.start` handed
* this plugin, its running tools stopped, no interruption marker.
*
* The event `turn.abort`, seen by the hooks above; the prompt this
* plugin submits next is the context. Rejects, naming both ids, when
* `turnId` is not the running turn's; a hook may end its own turn.
*
* @param input `turnId`: the id `turn.start` carried
* @example
* on("turn.start", ($, e, next) => { held = e.turnId; return next(e) })
*/
abort: (input: OpEventOf['turn.abort']) => Promise;
};
/**
* Submitting a prompt the model reads as a user turn, and the person's
* prompt box: read as it stands, written, or proposed into.
*/
prompt: {
/**
* Submits a prompt: the event `prompt.submit`, the same call the engine
* makes for a typed prompt; `input.text` runs when the session is idle.
*
* It goes through every hook but the calling one (the plugin's others
* see it) with `e.origin` `{ kind: 'plugin', name }`, the name the
* model reads it under unless a hook leaves it out of its answer.
*
* @example
* void $.prompt.submit({ text: "List the TODOs you just mentioned." })
*/
submit: EventCalls['prompt']['submit'];
/**
* Returns the prompt box as it stands, the draft typed so far and the
* cursor's offset into it, so a `fill` can keep what the person typed.
*
* Never rejects: `{ text: '', cursor: 0 }` where the session draws no box
* (a -p run, an SDK host) or none is mounted yet.
*
* @example
* const { text, cursor } = await $.prompt.read()
*/
read: () => Promise;
/**
* Puts `input.text` in the prompt box as the draft, by `mode`: `replace`
* (the default) over it, `append` after it, `insert` at the cursor.
*
* The event `prompt.fill` through the other plugins' hooks; `isFilled:
* false` under a dialog or headless. To hand the model text WITH the next
* prompt instead, a `prompt.submit` hook adds `context` (second example).
*
* @example
* await $.prompt.fill({ text: `> ${quote}\n`, mode: "insert" })
* @example
* ($, e, next) => next({ ...e, context: [...(e.context ?? []), hunk] })
*/
fill: (input: PromptFillArgs) => Promise;
/**
* Proposes `input.text` as the prompt box's dim suggestion, Tab to take:
* the event `prompt.suggest`, as the engine's own guess after a turn.
*
* It goes through every other plugin's hook with `e.origin` `{ kind:
* 'plugin', name }`, the engine's own suggestions on or off; `{ isShown:
* false }` while the box holds text, a turn runs, or headless (no box).
*
* @example
* void $.prompt.suggest({ text: "run the tests you just wrote" })
*/
suggest: EventCalls['prompt']['suggest'];
/**
* Returns the system prompt's sections for `facts`: the event
* `prompt.compose`, the call the engine makes for every prompt it sends.
*
* A fact left out is the session's own (its model, its tools). Through
* every other plugin's hook, over the engine's own composition; composed
* for nobody to send, so nothing the session holds is written.
*
* @example
* const ids = (await $.prompt.compose()).sections.map(s => s.id)
*/
compose: EventCalls['prompt']['compose'];
};
/**
* The tools the model has in this session, and running one.
*/
tool: {
/**
* Returns the tools the model can call now, built-in and MCP alike, in
* the order the model sees them.
*
* @example
* const names = (await $.tool.list()).map(t => t.name)
*/
list: () => Promise;
/**
* Calls a tool: the event `tool.call`, the same call the engine makes for
* the model's tool calls, under a `tool_use_id` of its own.
*
* It runs through every hook but the calling one (the plugin's others
* see it), the permission check and its dialog, then the tool. Rejects
* when no tool has that name or the call is aborted.
*
* @example
* const { text } = await $.tool.call({ tool: "Read", file_path: "a.md" })
*/
call: EventCalls['tool']['call'];
/**
* Asks the engine's permission decision for a tool call now: the event
* `tool.check`, resolved to its verdict (`{ decision, reason?, ... }`).
*
* The hooks run (the calling hook's own frame skipped, `next.origin` this
* plugin, no `tool_use_id`); nothing runs, no dialog opens, no PreToolUse
* hook or classifier is asked.
*
* @example
* const { decision } = await $.tool.check({ tool: "Read", input })
*/
check: EventCalls['tool']['check'];
/**
* Declares a tool the model can call from the next prompt on: the name,
* description and input schema of `mcp____`.
*
* Serve it with a `tool.call` hook on `{ tool: "mcp____" }`
* that returns the result (a call no hook answers fails); a name registered
* again is replaced. Rejects until the session binds, at `session.start`.
*
* @param tool `name`, `description` (what the model reads), `inputSchema`
* (a JSON schema object; default `{ type: "object" }`)
* @returns `{ tool }`, the registered tool's full name
* `mcp____`
* @example
* await $.tool.register({ name: "weather", description: "Weather." })
*/
register: (tool: ToolSpec) => Promise;
};
/**
* The slash commands the person can run in this session, and running one.
*/
command: {
/**
* Returns the slash commands the person can run now, built-in, plugin
* and MCP alike, in the order the typeahead lists them.
*
* @example
* const names = (await $.command.list()).map(c => c.name)
*/
list: () => Promise;
/**
* Runs a slash command as if the person typed `/command args`: the
* event `command.run`, queued and run once the session is idle.
*
* It runs through every hook but the calling one with `e.origin`
* `{ kind: 'plugin', name }`, its lines in the transcript. Rejects an
* unknown name, and inside a hook the turn is waiting on.
*
* @example
* const { text } = await $.command.run({ command: "status" })
*/
run: EventCalls['command']['run'];
/**
* Declares the slash command `/` for this session, listed in the
* typeahead from the next keystroke on.
*
* Serve it with a `command.run` hook on `{ command: "" }` that
* returns `{ text }`; a run no hook answers says so as its output.
* Registering a name again replaces it; a built-in's name is refused.
*
* @param command `name`, `description` (what the menu shows),
* `argumentHint` (dim after the name), `immediate` (runs mid-turn)
* @returns `{ command }`, the registered name
* @example
* await $.command.register({ name: "hello", description: "Says hi." })
*/
register: (command: CommandSpec) => Promise;
};
/**
* Every row of the settings menu (`/config`), the panel's own and each
* enabled plugin's `userConfig` fields alike: listing and changing them.
*/
config: {
/**
* Returns the rows the `/config` menu would draw now, in its order,
* each with its current value, its kind, its owner and its lock.
*
* After every `config.describe` hook: a hidden row is left out, a
* relabelled one carries the new label.
*
* @example
* const theme = (await $.config.list()).find(row => row.key === "theme")
*/
list: () => Promise;
/**
* Changes one row as if the person did in the menu: the event
* `config.set` with `origin` `{ kind: 'plugin', name }`, then the writer.
*
* Through the other plugins' hooks, this plugin's own skipped; `{ deny }`
* when a hook refused, the value does not fit, a trusted source owns the
* row or only its dialog changes it. Rejects a key no row has.
*
* @param args `key` (as `list` names it) and `value` (the row's kind)
* @returns `{ value }` once written, or `{ deny }`
* @example
* const { deny } = await $.config.set({ key: "verbose", value: true })
*/
set: EventCalls['config']['set'];
};
/**
* Subagents.
*/
agent: {
/**
* Spawns a subagent: the event `agent.spawn`, the same call the engine
* makes when the Agent tool starts one; the engine fills the rest.
*
* It runs every hook but the calling one, then the Agent tool in the
* background under this call's origin: `{ model, agentId }` once the
* subagent started (its answer is its `turn.complete`), or `{ deny }`.
*
* @example
* const { agentId } = await $.agent.spawn({ prompt: "Read README.md." })
*/
spawn: EventCalls['agent']['spawn'];
/**
* Returns the session's subagents so far, the ones the model spawned and
* the ones plugins did alike.
*/
list: () => Promise;
/**
* Defines an agent type the Agent tool dispatches from the next turn on,
* named `:`: the event `agent.register`.
*
* Every field takes effect as in an agent file; re-registered, a name is
* replaced; unloaded, a plugin's types go. `agent.offer` hides it from
* the model alone; any plugin's `$.agent.spawn` answers to `agent.spawn`.
*
* @param spec `name`, `description` (when to delegate), `prompt` (its
* system prompt), and any other field of an agent definition
* @returns `{ agent }`, the full name; rejects until the session binds,
* on a spec the schema refuses (its reason), on a hook's deny
* @example
* await $.agent.register({ name: "runner", description: "Runs a spec",
* prompt: RUNNER_PROMPT, tools: ["Read", "Bash"], omitClaudeMd: true })
* @example
* // runner-only: hidden from the model, spawned by this plugin's tool,
* // answered by the subagent's turn.complete (matched by agentId)
* on("agent.offer", { agent: "lab:runner" }, () => ({ isOffered: false }))
* on("tool.call", { tool: "mcp__lab__run" }, async ($, e) => {
* const { agentId, deny } = await $.agent.spawn({
* subagentType: "lab:runner", prompt: e.spec, description: "run" })
* return { result: deny ?? (await answerOf(agentId)) }
* })
*/
register: (spec: AgentSpec) => Promise;
};
/**
* The file system as the engine's own process reaches it; a relative path
* is under the session's working directory, and text is UTF-8.
*
* An absolute path is used as given; where one may go is an `fs.*` hook's
* to say. A read or write over 4 MiB rejects, a foreign network location
* as spelled rejects untouched, and an OS refusal rejects with its errno.
*/
fs: {
/**
* Reads a file and returns its text, or with `{ as: "bytes" }` its bytes
* as `{ base64 }`.
*
* Rejects when missing, or over 4 MiB, which bounds what one read copies
* into the plugin's environment. A file the plugin ships is under
* `$.plugin.root`.
*
* @param path relative to the working directory, or absolute
* @param options `as`: `"text"` (the default) or `"bytes"`
* @returns the file's text, or `{ base64 }`
* @example
* const readme = await $.fs.read("README.md")
* @example
* const { base64 } = await $.fs.read(
* `${$.plugin.root}/hooks/weights.bin`, { as: "bytes" })
* const weights = Uint8Array.fromBase64(base64)
*/
read: FsReadCall;
/**
* Writes `text` to a file, creating it and its directories as needed.
*
* @param path relative to the working directory, or absolute
* @param text the whole new content
*/
write: (path: string, text: string) => Promise;
/**
* Lists a directory: `{ name, kind, size, isLink }` per entry, by name,
* each entry as it stands (a symbolic link is `other` with `isLink`).
*
* @param path the directory's path; absent, the working directory
* @returns the entries, `{ name, kind, size, isLink }` each
*/
list: (path?: string) => Promise;
/**
* Returns whether the path exists; rejects only a network location as
* spelled, which no `fs` call reaches.
*/
exists: (path: string) => Promise;
/**
* Returns `{ kind, size, mtimeMs, isLink }` of the path: what it leads to,
* and whether it is itself a symbolic link. Rejects when missing.
*
* With `{ resolve: true }` also `realPath`, where the path lands, absent
* when it leads nowhere; a guard matches it and denies without it, since a
* spelling it cannot resolve (`~`, a file not there yet) the tool may open.
*
* @param path relative to the working directory, or absolute
* @param options `resolve`: also answer `realPath` (one more file system
* call)
* @returns the stat, `realPath` with it when asked and resolvable; rejects
* `ENOENT` for a missing path
* @example
* // ROOT was resolved the same way and SEP is its separator; an
* // allow-list under it is the robust guard, a deny-list on spellings
* // only best effort (a hard link or a case alias keeps its own)
* on("tool.call", { tool: "Read" }, async ($, e, next) => {
* const stat = await $.fs.stat(e.file_path, { resolve: true })
* .catch(() => undefined)
* const real = stat?.realPath
* const isInside = real !== undefined && real.startsWith(ROOT + SEP)
* return isInside ? next(e) : { deny: "outside the project" }
* })
* @example
* // a Write may name a file not there yet: `placed` answers where the
* // path lands or undefined, and the guard denies on undefined or
* // outside ROOT. Unplaceable by spelling first, with no file system
* // call (drive-relative `D:x`, a `\\` or `//` network or device path,
* // a name that is empty, ".", ".." or itself `C:...`); then the file
* // if it stats; else its folder, cut after the last separator and
* // keeping it so a drive or share root stays that root, plus the name
* const placed = async (path) => {
* const cut = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"))
* const name = path.slice(cut + 1)
* const isPlaceable = !/^[A-Za-z]:(?![\\/])/.test(path) &&
* !/^[\\/][\\/]/.test(path) && !/^[A-Za-z]:/.test(name) &&
* name !== "" && name !== "." && name !== ".."
* if (!isPlaceable) return undefined
* const own = await $.fs.stat(path, { resolve: true })
* .catch(() => undefined)
* if (own) return own.realPath
* const folder = cut < 0 ? "." : path.slice(0, cut + 1)
* const dir = await $.fs.stat(folder, { resolve: true })
* .catch(() => undefined)
* return dir?.realPath === undefined ? undefined
* : `${dir.realPath.replace(/[\\/]$/, "")}${SEP}${name}`
* }
* const real = await placed(e.file_path)
* const isInside = real !== undefined && real.startsWith(ROOT + SEP)
* return isInside ? next(e) : { deny: "cannot place it, or outside" }
*/
stat: (path: string, options?: FsStatOptions) => Promise;
/**
* Reads the named instruction files in every directory above the
* session's original working directory, the way the engine reads CLAUDE.md.
*
* Root first, each `{ dir, name, content }` that exists, the content
* with its `@include`s after it; `of` and `below` together are the
* engine's own walk for a nested CLAUDE.md between root and read file.
*
* @param request `names`, relative `.md` file names (no `..`); `of`, the
* file walked down to; `below`, the directory the walk stays inside
* @returns the files found, root first
* @example
* const found = await $.fs.ancestors({ names: ["AGENTS.md"] })
* const stack = await $.fs.ancestors({ names: ["AGENTS.md"], of: path })
* const nested = await $.fs.ancestors({
* names: ["AGENTS.md"],
* of: e.file_path,
* below: await $.session.root(),
* })
*/
ancestors: (request: FsAncestorsRequest) => Promise;
};
/**
* This plugin's own key-value store, kept between sessions and hot
* reloads; values are JSON data.
*
* A JSON file of the plugin's own under the user's Claude Code
* configuration directory.
*/
store: {
/**
* Returns the value under `key`, or `undefined` when unset.
*
* @example
* const count = Number((await $.store.get("count")) ?? 0) + 1
*/
get: (key: string) => Promise;
/**
* Sets `key` to `value`, which must be JSON data.
*
* `get` reads back `JSON.parse(JSON.stringify(value))`: a Date is its ISO
* string, an `undefined` field is dropped, a Map or Set is `{}`. Rejects
* a function, a cycle, or a store over 4 MiB of JSON text in all.
*/
set: (key: string, value: unknown) => Promise;
/**
* Removes `key` from the store.
*/
delete: (key: string) => Promise;
/**
* Returns every key set, in insertion order.
*/
keys: () => Promise;
};
/**
* The time and timers, each an event through the host: `clock.now` reads
* the time; `clock.sleep`, `after` and `every` wait until it has passed.
*
* A timer's callback is the plugin's own function, kept in its environment
* and run there when the wait resolves; a hot reload of the plugin cancels
* its pending waits with the old environment.
*/
clock: {
/**
* Resolves milliseconds since the epoch, now.
*
* @example
* const startedAt = await $.clock.now()
*/
now: () => Promise;
/**
* Resolves after `ms` milliseconds; rejects at once when `signal` aborts.
*
* @param ms how long, in milliseconds
* @param options `signal`: ends the wait early with a rejection (pass
* `next.signal` so a hook's wait ends with its dispatch)
* @example
* await $.clock.sleep(500, { signal: next.signal })
*/
sleep: (ms: number, options?: SleepOptions) => Promise;
/**
* Calls `fn` once after `ms` milliseconds; `cancel()` before then stops it.
*
* One `clock.after` dispatch: `fn` runs when it resolves, and never when
* a hook refuses it.
*/
after: TimerCall;
/**
* Calls `fn` every `ms` milliseconds (at least 1) until `cancel()`.
*
* One `clock.every` dispatch per period: `fn` runs when it resolves and
* the next period is asked; a refused period ends the interval.
*
* @example
* const tick = $.clock.every(1000, () => $.ui.status("polling"))
*/
every: TimerCall;
};
/**
* The network, through the host.
*/
http: {
/**
* Fetches `url` through the host (never the plugin's own network) and
* resolves `{ status, ok, headers, text }` once the body is read.
*
* http or https, to whatever the host process can reach, unless the
* administrator's policy switches refuse it; an `auth` handle from
* `$.session.authorize()` rides https only, to a first-party host.
*
* @param url the URL (http or https)
* @param init `{ method, headers, body, auth, socketPath }` (body a
* string; socketPath a Unix socket to go over instead of TCP)
* @returns `{ status, ok, headers, text }` once the body is read
* @example
* const { ok, text } = await $.http.fetch("https://example.com/status")
* @example
* await $.http.fetch("http://bridge/reload", {
* method: "POST",
* socketPath: `${runDirectory}/bridge.sock`,
* })
*/
fetch: (url: string, init?: HttpInit) => Promise;
};
/**
* Commands on the host, run as the user the session runs as. CLI only.
*
* Local execution, not a network path: what a command of its own reaches
* is its own, as for the Bash tool and a settings `command` hook.
*/
process: {
/**
* Runs a command on the host by its argument vector (no shell) and
* resolves `{ exitCode, stdout, stderr }` once it exits, any exit code.
*
* One shot: the whole output is read, so a background process left
* writing holds the call until the timeout. Rejects when the command
* cannot start or is still running then. Git runs with repo hooks off.
*
* @param argv the command and its arguments, `argv[0]` the executable
* @param init `{ cwd, env, stdin, timeoutMs }` (cwd the session's by
* default; timeout 30 s by default, ten minutes at most)
* @returns `{ exitCode, stdout, stderr }` once the child exits
* @example
* const { exitCode, stdout } = await $.process.run(["git", "status"])
*/
run: (argv: readonly string[], init?: ProcessRunInit) => Promise;
};
/**
* What the settings files, `--settings` and managed policy hold, as the
* engine runs under it; read only.
*
* Every key crosses as the source holds it, `env` and the helper commands
* included: nothing is filtered. The OAuth session and the global config
* (~/.claude.json) are not settings and are never read.
*/
settings: {
/**
* Resolves with the settings merged over every source, as the engine
* reads them, or with one source's settings as loaded (`{ source }`).
*
* A snapshot in plain data each call; a source with no file answers
* `{}`. The sources (SettingsSource) rise in precedence from `user` to
* `policy`: the merge takes a key from the last source that has it.
*
* @param args `{ source }` to read one source; nothing for the merge
* @returns the settings object, keyed as a settings.json is
* @example
* const { permissions } = await $.settings.read()
* const policy = await $.settings.read({ source: "policy" })
*/
read: (args?: SettingsReadArgs) => Promise;
};
/**
* The environment of this process, the one every Bash child, MCP server
* and `$.process.run` command started after inherits.
*
* `get` and `set` take the variable's name as a string literal, so what a
* module reads and writes is read off its source: `claude plugin validate`
* lists the names, and a name the module does not spell is refused.
*/
env: {
/**
* Resolves with the variable's value, or `undefined` when it is unset.
*
* `name` must be a string literal; `claude plugin validate` lists the
* names your module reads and writes.
*
* @example
* const home = await $.env.get("HOME")
*/
get: (name: string) => Promise;
/**
* Sets the variable for this process and everything it starts after, or
* unsets it when `value` is `undefined`.
*
* `name` must be a string literal; `claude plugin validate` lists the
* names your module reads and writes.
*
* @example
* await $.env.set("GIT_PAGER", "cat")
*/
set: (name: string, value: string | undefined) => Promise;
};
}
/**
* The name of an event the engine defines itself (a key of CoreEventOf);
* EventName adds the declared plugin nouns' events.
*/
type CoreEventName = keyof CoreEventOf;
/**
* The argument of each event the engine defines itself: its call sites'
* (EngineEventOf), the classic hooks' (ClassicEventOf), the calls on `$`.
*/
type CoreEventOf = EngineEventOf & ClassicEventOf & OpEventOf;
type CwdChangedHookInput = BaseHookInput & {
hook_event_name: 'CwdChanged';
old_cwd: string;
new_cwd: string;
};
type DirectoryAddedHookInput = BaseHookInput & {
hook_event_name: 'DirectoryAdded';
/**
* Absolute path of the directory that was added.
*/
directory: string;
/**
* How the directory was added: "slash_command" for /add-dir, "register_repo_root" for the SDK control_request.
*/
source: 'slash_command' | 'register_repo_root';
};
/**
* The `children` field every element constructor's props carry, appended
* beside its own props type: one child, or a list that may nest.
*
* JSX types a lone child as the child itself (`done`
* passes the string, `{count}` the number), and a mapped list
* beside a sibling as a nested list (`{rows.map(row)}ok`).
*/
export type ElementChildren = {
children?: RenderChildren;
};
/**
* An element as `$.ui.resolve(e)` hands it out: a constructor from props to
* the frozen plain-data element, `children` among the props as JSX passes.
*
* `const { Box } = $.ui.resolve(e)` then `...` compiles to
* `h(Box, { gap: 1 }, ...children)`, and `h` calls a function tag with its
* props, so the table's constructors are the JSX tags.
*/
export type ElementConstructor = (props: P & ElementChildren) => RenderElement;
/**
* Every element name of every surface: what a table handed out is completed to
* (an omitted one draws a fragment; see `ui.resolve`).
*/
export type ElementName = {
[P in RenderSurface]: keyof Elements[P];
}[RenderSurface];
/**
* The element constructors each surface draws, by `e.surface`: what
* `$.ui.resolve(e)` returns and a `ui.resolve` hook passes on; no globals.
*
* All carry `Box`, `Text`, `Button`, `Link`, `Code`, `Markdown`; every remote
* surface `Svg`; all but mobile `Input` and `Select`; terminal and desktop
* `Client`; terminal `Raster` and `Image`. Narrowed on `e.surface`, that table.
*/
export type Elements = {
terminal: {
Box: ElementConstructor;
Text: ElementConstructor;
Button: ElementConstructor;
Input: ElementConstructor;
Select: ElementConstructor;
Link: ElementConstructor;
Code: ElementConstructor;
Markdown: ElementConstructor;
/**
* A region one of the plugin's surface modules draws (ClientModule), named
* by `module`: a string literal, the module's path relative to this file.
*
* The engine reads the surface module off the hooks module's source
* before anything runs, so a variable, a template with a substitution or
* a computed path as `module` is refused at load, naming the line.
*/
Client: ElementConstructor;
Raster: ElementConstructor;
Image: ElementConstructor;
};
desktop: {
Box: ElementConstructor;
Text: ElementConstructor;
Button: ElementConstructor;
Input: ElementConstructor;
Select: ElementConstructor;
Svg: ElementConstructor;
Link: ElementConstructor;
Code: ElementConstructor;
Markdown: ElementConstructor;
Client: ElementConstructor;
};
/**
* No `Input` or `Select`: the control protocol carries presses (ui_press)
* but no ui_input or ui_select yet; not a limit of the device.
*
* The table grows when those messages exist.
*/
mobile: {
Box: ElementConstructor;
Text: ElementConstructor;
Button: ElementConstructor;
Svg: ElementConstructor;
Link: ElementConstructor;
Code: ElementConstructor;
Markdown: ElementConstructor;
};
/**
* The desktop's table without `Client`: a remote `Client`'s module, presses
* and posts (ui_client_module, ui_client_press, ui_message) name no surface.
*
* They are the desktop's alone today, not a limit of the editor's webview:
* the table gains `Client` when those asks name a surface.
*/
vscode: {
Box: ElementConstructor;
Text: ElementConstructor;
Button: ElementConstructor;
Input: ElementConstructor;
Select: ElementConstructor;
Svg: ElementConstructor;
Link: ElementConstructor;
Code: ElementConstructor;
Markdown: ElementConstructor;
};
};
/**
* The table `ui.resolve` answers for an argument of surface `P`.
*/
export type ElementTable = Elements[P];
/**
* Hook input for the Elicitation event. Fired when an MCP server requests user input. Hooks can auto-respond (accept/decline) instead of showing the dialog.
*/
type ElicitationHookInput = BaseHookInput & {
hook_event_name: 'Elicitation';
mcp_server_name: string;
message: string;
mode?: 'form' | 'url';
url?: string;
elicitation_id?: string;
requested_schema?: Record;
};
/**
* Hook input for the ElicitationResult event. Fired after the user responds to an MCP elicitation. Hooks can observe or override the response before it is sent to the server.
*/
type ElicitationResultHookInput = BaseHookInput & {
hook_event_name: 'ElicitationResult';
mcp_server_name: string;
elicitation_id?: string;
mode?: 'form' | 'url';
action: 'accept' | 'decline' | 'cancel';
content?: Record;
};
/**
* The input of `engine.create`: the fold that builds `$`, once per load,
* core innermost.
*
* A hook is written in post-order: `const built = await next(e)` is `$` as
* built so far; `return { ...built, voice: { say } }` adds this plugin's noun.
* See EngineEventOf's `engine.create` for what a step may and may not do.
*/
export type EngineCreateInput = {
/**
* The modules this fold builds, in list order, first outermost (managed
* plugins first, so an org plugin's withholding wins).
*
* The whole set at a load, this plugin alone at its reload, the ones added
* or changed at a refresh that keeps the rest: a module added later that
* does not hook this event is built against the table as it stands.
*/
plugins: readonly string[];
};
/**
* What an `engine.create` hook returns: `$` as built so far with this
* plugin's nouns added, less any it withheld.
*
* Every declared noun is optional here. Between hooks it crosses the chain
* as interface descriptors; a hook sees objects (EngineInterfaceBuilt).
*/
export type EngineCreateResult = Partial & {
readonly [noun: string]: unknown;
};
/**
* The events the engine raises at its call sites, and `engine.create`; the
* classic settings hooks' events are ClassicEventOf.
*
* At every one, a hook that fails (throws, overruns its budget: HookBudget,
* answers a wrong shape) is skipped: the hooks beneath and core run in its
* place, or its last `next` result stands; the failure is reported by name.
*/
export type EngineEventOf = {
/**
* Fires when the engine is about to run a tool. `next(e)` runs the hooks
* beneath, then core (the permission prompt, the tool itself).
*
* Return `{ deny: reason }` to refuse or `{ result }` to answer yourself; a
* hook that returns while its `next` is pending aborts what runs beneath.
* The managed-settings hooks run first: their deny is the call's result.
*/
'tool.call': ToolCallInput;
/**
* Fires when the engine decides whether a tool call may run, after the
* `tool.call` and PreToolUse hooks and before the mode settles an ask.
*
* `next(e)` resolves to the engine's verdict (rules, mode, the tool's own
* check, PreToolUse's decision); return any `{ decision }`. `$.tool.check`
* runs the same chain and executes nothing.
*
* @example
* on("tool.check", { tool: "Read" }, () => ({ decision: "allow" }))
*/
'tool.check': ToolCheckInput;
/**
* Fires when the engine is about to draw a component: once per input value
* (props, viewport width), plugin load or `$.ui.invalidate("ui.render")`.
*
* A repaint reuses the answer; a clock invalidates. `next(e)` resolves to the
* drawing: return it, wrap it, draw your own, or rewrite `props`. A tree that
* does not validate draws the engine's own; `--plugin-dir` is told why.
*/
'ui.render': RenderInput;
/**
* Fires when the plugins load (not per draw), once per surface, component
* and plugin: `e` names the surface and component, never the props.
*
* `next(e)` resolves to the surface's table, which `$.ui.resolve(e)` then
* reads. Return it, one with an element restyled for every other plugin,
* or one with a key left out (a fragment there); own table: hook skipped.
*/
'ui.resolve': ResolveInput;
/**
* Fires when a `Button` a render hook drew is pressed on a surface; `e` is
* `{ plugin, element, component, surface }`, `element` the button's `key`.
*
* `next(e)` runs the hooks beneath, then core: the element's own `onPress`
* closure, in its plugin's environment, resolving to `{ element }`. Return
* `next(e)` to let the press through, or `{ element }` to take it.
*/
'ui.press': UiPressArgument;
/**
* Fires when an `Input` a render hook drew changes or is submitted; `e` is
* `{ plugin, element, component, surface, kind, value }`.
*
* `next(e)` runs the hooks beneath, then the element's own `onInput` or
* `onSubmit` with `e.value` as the chain left it, resolving to `{ element,
* value }`; `next({ ...e, value })` rewrites the typing, an answer takes it.
*/
'ui.input': UiInputArgument;
/**
* Fires when a `Select` a render hook drew is picked from; `e` is
* `{ plugin, element, component, surface, value }`.
*
* `next(e)` runs the hooks beneath, then the element's own `onSelect` with
* `e.value` as the chain left it, resolving to `{ element, value }`;
* `next({ ...e, value })` rewrites the pick, an answer takes it.
*/
'ui.select': UiSelectArgument;
/**
* Fires when a `Client` THIS plugin drew posts from its surface module
* (`surface.post(data)`); only this plugin's hooks see it.
*
* `next.origin` names `client`: `data` came from code. Core answers `{}`;
* `next({ ...e, data })` rewrites the data; `{ props }` hands the posting
* instance its next props without a redraw. One per instance per frame.
*/
'ui.message': UiMessageArgument;
/**
* Fires before a site's window moves: the person's wheel or scroll keys on
* a `Pane` body or the `AbovePrompt` band, at its edges too; `$.ui.scroll`.
*
* `next(e)` moves it to `e.offset` and draws: `{}`; `next({ ...e, offset
* })` elsewhere; no `next` (`{}` or `{ deny }`) leaves it undrawn, so a hook
* drawing its own rows under a header moves them by `e.by` and invalidates.
*
* @example
* on("ui.scroll", { requestId: "log" }, ($, e) => (scrollOwnRows(e.by), {}))
*/
'ui.scroll': UiScrollInput;
/**
* Fires before a site's focus ring moves: the person's Tab, arrows or click
* in a `Pane` or the band; an `autoFocus` element taking it; `$.ui.focus`.
*
* `next(e)` lands it on `e.element` (absent: one of the engine's stops) and
* draws: `{}`; `next({ ...e, element })` on another of `e.plugin`'s; no
* `next` (`{}` or `{ deny }`) keeps it where it was, drawn as it was.
*
* @example
* on("ui.focus", { requestId: "list" }, ($, e, next) => (mark(e), next(e)))
*/
'ui.focus': UiFocusInput;
/**
* Fires when the engine offers an agent type to the model, in the agent
* listing and again at dispatch; `next(e)` resolves to `{ isOffered: true }`.
*
* Return `{ isOffered: false }` to keep the type out of the listing and
* refuse its dispatch. A hook that fails passes it through.
*
* @example
* on("agent.offer", { agent: "Plan" }, () => ({ isOffered: false }))
*/
'agent.offer': AgentOfferInput;
/**
* Fires when the Agent tool is about to start a subagent, everything
* decided and its model not yet resolved.
*
* `next(e)` resolves to `{ model }`. Return it, `next({ ...e, model })`,
* `{ model }` of your own (an alias resolves like the tool's parameter), or
* `{ deny: reason }`.
*/
'agent.spawn': AgentSpawnInput;
/**
* Fires when a prompt is submitted, before the turn starts. `next(e)` runs
* the hooks beneath and the UserPromptSubmit settings hooks.
*
* Rewrite with `next({ ...e, text })` (the user message on screen follows)
* or stop it with `{ drop: reason }`; a broken plugin never blocks a prompt.
* A prompt typed while a turn ran fires at Enter, with that turn's id.
*/
'prompt.submit': PromptSubmitInput;
/**
* Fires when a text is about to be put in the prompt box as the person's
* draft (a plugin's `$.prompt.fill`); `next(e)` writes it by `e.mode`.
*
* `replace` over the draft, `append` after it, `insert` at the cursor;
* rewrite `text` or `mode` going down, or answer `{ isFilled: false }`
* without `next` to keep it out, as core does under a dialog or headless.
*
* @example
* on("prompt.fill", ($, e, next) => next({ ...e, text: e.text.trim() }))
*/
'prompt.fill': PromptFillInput;
/**
* Fires when a text is proposed as the prompt box's dim suggestion, Tab to
* take: the engine's guess after a turn, or a plugin's `$.prompt.suggest`.
*
* `next(e)` shows it: `{ isShown }`. Rewrite with `next({ ...e, text })`,
* or answer `{ isShown: false }` without `next` to drop it; core answers
* that too while the box holds text or a turn runs.
*
* @example
* on("prompt.suggest", { origin: { kind: "suggestion" } }, hide)
*/
'prompt.suggest': PromptSuggestInput;
/**
* Fires when the person edits the main prompt box: a key the editor took as
* an edit, or a paste; `next(e)` resolves the box the editor shows.
*
* `e` is the draft before and the splice (`start`, `end`, `inputText`); a
* burst of keys is one edit. Rewrite `inputText` going down or the box
* coming up; `{ text: e.text, cursor: e.cursor }` without `next` consumes.
*
* @example
* on("prompt.edit", ($, e, next) => next({ ...e, inputText: up(e) }))
*/
'prompt.edit': PromptEditInput;
/**
* Fires once per named section of the system prompt, when the engine
* assembles it; `next(e)` resolves to `{ text }` as core computed it.
*
* Sections are cached by name for the session until
* `$.ui.invalidate("prompt.section")`: an unstable answer spends the
* model's prompt cache on every call. A hook that fails passes it through.
*
* @example
* on("prompt.section", { name: "memory" }, () => ({ text: null }))
*/
'prompt.section': PromptSectionInput;
/**
* Fires once per conversation, when the engine computes the context blocks
* its first user message carries; `next(e)` resolves to `{ blocks }`.
*
* Append, drop, reorder or rewrite with `next({ ...e, blocks })`; the
* engine renders what comes back, in order, until
* `$.ui.invalidate("prompt.context")` or a re-read (compaction, `/clear`).
*
* @example
* on("prompt.context", () => ({ blocks: [] }))
*/
'prompt.context': PromptContextInput;
/**
* Fires when the engine renders a system prompt; `next(e)` resolves to
* `{ sections }`, each `{ id, text, scope }`, in the order they are sent.
*
* The bottom is the engine's own composition (`intro`, `tools`, `memory`,
* ...). Append, replace by id, reorder or drop what `next(e)` answered;
* answer without `next` to replace it all. The engine places each cache mark.
*
* @example
* on("prompt.compose", async ($, e, next) => dropped(await next(e), "tone"))
*/
'prompt.compose': PromptComposeInput;
/**
* Fires once per message the engine injects for the model on its own (a
* reminder, a mode transition, a mentioned file), as a request carries it.
*
* `next(e)` resolves to `{ text }`; `{ text: null }` leaves it out. The
* answer holds per attachment for the process (asked again on resume or
* `$.ui.invalidate`); the transcript keeps the engine's record.
*
* @example
* on("prompt.attachment", { type: "todo_reminder" }, () => ({ text: null }))
*/
'prompt.attachment': PromptAttachmentInput;
/**
* Fires once per tool, when the engine first renders the tool's schema in
* a session; `next(e)` resolves to `{ description, isDeferred? }`.
*
* Cached for the session until `$.ui.invalidate("tool.describe")`: an
* unstable answer spends the model's prompt cache. An explicit `isDeferred`
* moves the tool behind ToolSearch (true) or into the prompt's list (false).
*
* @example
* on("tool.describe", { tool: "Bash" }, ($, e) => ({ ...e, description }))
* @example
* on("tool.describe", { tool: "Monitor" }, pin) // {...e, isDeferred: false}
*/
'tool.describe': ToolDescribeInput;
/**
* Fires when a slash command is about to run (`/name args` typed, or a
* plugin's `$.command.run`); `next(e)` resolves to `{ text }`, its output.
*
* Core is the engine's command (a registered one has none). Rewrite `args`
* with `next`, or return `{ text }` without it to answer in its place; one
* after `next` replaces a printed output, not a panel or prompt it opened.
*
* @example
* on("command.run", { command: "hello" }, () => ({ text: "hello" }))
*/
'command.run': CommandRunInput;
/**
* Fires once per command, when the engine lists it for the typeahead and
* `/help`; `next(e)` resolves to `{ description, argumentHint, isHidden }`.
*
* Listed answers are cached for the session until
* `$.ui.invalidate("command.describe")`. A hook that fails passes it
* through.
*
* @example
* on("command.describe", ($, e, next) => next({ ...e, isHidden: true }))
*/
'command.describe': CommandDescribeInput;
/**
* Fires when a `/config` row is about to change, from the menu or a
* plugin's `$.config.set`; `next(e)` resolves to `{ value }` once written.
*
* Return `{ deny: reason }` to leave the row as it is (the menu says why),
* or `next({ ...e, value })` to clamp it; a value of the wrong kind for
* the row is refused. A row a trusted source owns is core's to refuse.
*
* @example
* on("config.set", { key: "theme" }, () => ({ deny: "the theme stays" }))
*/
'config.set': ConfigSetInput;
/**
* Fires once per `/config` row, when the menu lists it and for
* `$.config.list`; `next(e)` resolves to `{ label, description, isHidden }`.
*
* Relabel, re-describe or hide with `next({ ...e, isHidden: true })`; the
* answers are cached until `$.ui.invalidate("config.describe")` or the
* loaded plugins change. A hook that fails passes the row through.
*
* @example
* on("config.describe", { key: "tips" }, hide) // answers isHidden: true
*/
'config.describe': ConfigDescribeInput;
/**
* Fires when the engine expands a skill's prompt for the model (`/name`,
* the Skill tool, a preload); `next(e)` resolves to `{ text }` as computed.
*
* Return `{ text }` with the text the model reads instead. A hook that
* fails passes it through.
*
* @example
* on("skill.prompt", { skill: "commit" }, () => ({ text: "A haiku." }))
*/
'skill.prompt': SkillPromptInput;
/**
* Fires when the engine composes a git text the model is to write (`kind`:
* `commit`, `pr`, `exemption`, `remedy`); `next(e)` resolves to `{ text }`.
*
* Return `{ text }` with the text the model reads instead. A hook that
* fails passes it through.
*
* @example
* on("attribution.text", { kind: "commit" }, () => ({ text: "" }))
*/
'attribution.text': AttributionTextInput;
/**
* Fires once per process for each loaded plugin, before the first prompt,
* then once per fresh load of one (never `/clear`); `next(e)` is `{ cwd }`.
*
* Observe. The first is awaited: a `$.tool.register` is listed by turn one.
* A later one runs its hooks alone: an enable, a worker respawn, or a reload
* (changed modules only; all if one hooks `engine.create`/`plugin.register`).
*
* @example
* on("session.start", ($, e, next) => $.tool.register(t).then(() => next(e)))
*/
'session.start': SessionStartInput;
/**
* Fires when a delivery reaches the session (a relay's event, a peer's
* message, a Remote Control prompt), before it is queued; `{ text }`.
*
* Rewrite with `next({ ...e, text })`, or return `{ consumed: reason }` to
* take it: nothing is queued, shown or read by the model. `origin` and
* `event` pass on as received; `session.send`, its dual, is reserved.
*
* @example
* on("session.receive", { origin: "peer" }, () => ({ consumed: "muted" }))
*/
'session.receive': SessionReceiveInput;
/**
* Fires once per row a conversation of this session keeps (a prompt, a
* response block, a tool result, a notice), before it is stored.
*
* `next({ ...e, message })` rewrites `content`: stored and sent after. The
* screen, an SDK stream or Remote Control may show the row just before its
* rewrite; the model and the transcript file never read that form.
*
* @example
* on("session.append", { door: "tool-result" }, ($, e, n) => n(scrub(e)))
*/
'session.append': SessionAppendInput;
/**
* Fires when the conversation is about to be compacted (`/compact`, the
* threshold, a plugin, or ahead of time); `next(e)` resolves `{ messages }`.
*
* Rewrite `instructions` or `messages` on the way down, the messages on
* the way up, or answer `{ messages }` of your own; `{ skip: reason }`
* leaves the conversation as it is. `trigger` passes on as received.
*
* @example
* on("session.compact", { trigger: "precompute" }, () => ({ skip: "off" }))
*/
'session.compact': SessionCompactInput;
/**
* Fires when a remote client joins the session's roster of attached
* surfaces: it said so (ui_attach), or it first asked to draw.
*
* Observe (a phone joined: draw the lobby); `next(e)` resolves to
* `{ clientId }`, a different return changes nothing. `$.session.surfaces()`
* reads the roster; a render hook still reads `e.surface` per ask.
*
* @example
* on("session.attach", { surface: "mobile" }, ($, e, next) => next(e))
*/
'session.attach': SessionAttachInput;
/**
* Fires when a client leaves the roster: it detached, or the session ended
* with it attached (`e.reason`). Observe; `next(e)` echoes `{ clientId }`.
*/
'session.detach': SessionDetachInput;
/**
* Fires when the engine measures the session and a unit moved: after each
* main-thread turn, and when a rate-limit window moves a whole point.
*
* Observe; `next(e)` echoes `{ changed }`. `$.session.usage()`'s figures,
* pushed, not polled: compare them with your own threshold here, call the
* op for the breakdown. One at a time, a burst folding into one more.
*
* @example
* on("session.measure", ($, e, next) => (toastPast90(e.rateLimits), next(e)))
*/
'session.measure': SessionMeasureInput;
/**
* Fires once when the session ends (exit, /clear, resume, logout, signal, a
* `-p` run done), after its SessionEnd settings hooks; `e.reason` says which.
*
* `e.resume.id` is `--resume`'s id; `next(e)` runs the engine's end step for
* the plugins (at an exit the attached clients leave): `{ sessionId }`. The
* SessionEnd bound afresh, 1.5 s by default; a `kill -9` raises nothing.
*
* @example
* on("session.end", async ($, e, next) => (await keep(e.resume), next(e)))
*/
'session.end': SessionEndInput;
/**
* Fires once per hooks module about to join the chain, at load (the set
* folded and built, nothing swapped in) and at reload; core allows.
*
* Return `{ refuse: reason }` and it never joins: no hook, no noun, no tool
* of it; the transcript names who refused. Its judges, `$` whole, are the
* plugins admitted before it and the binary's; judge by `tier` and `uses`.
*
* @example
* on("plugin.register", { tier: "user" }, () => ({ refuse: "managed only" }))
*/
'plugin.register': PluginRegisterInput;
/**
* Fires when a model turn begins, before its first model call; `next(e)`
* resolves to `{ turnId }`. Observe: a different return changes nothing.
*/
'turn.start': TurnStartInput;
/**
* Fires when the engine is about to send a model request of a turn, main's
* or a subagent's (`e.agentId`); `next(e)` resolves to the whole response.
*
* `next({ ...e, model })` or `effort` sends another; the turn, the index and
* the message count are pinned. An answer without `next` sends no request.
* It streams (StreamNext): the hook's budget counts its own code alone.
*/
'turn.step': TurnStepInput;
/**
* Fires when a model turn has ended, at the point its duration is reported;
* `next(e)` resolves to `{ text }`, the answer. `e.reason` says why.
*
* Return `{ text }` with a different text to show it beneath the answer (a
* synopsis, a TL;DR line); the transcript's record is never rewritten. A
* hook that fails leaves the answer as it was.
*/
'turn.complete': TurnCompleteInput;
/**
* Runs while `$` is being built, once per load or reload of this plugin
* and before any other hook of it; `next(e)` resolves to `$` built so far.
*
* A step may ADD nouns and WITHHOLD nouns (leave one out, or return without
* `next`); it may NOT REPLACE one another step added: the step fails, named
* with both plugins. A step that fails unloads its plugin; `$` is rebuilt.
*/
'engine.create': EngineCreateInput;
};
/**
* `$`, the first parameter of every hook. Frozen; core's interface plus
* every noun the plugins' `engine.create` steps added.
*
* Flat, `.`; it does not carry `on`, since registration happens
* before `$` exists. An interface so a plugin types the noun it provides by
* declaration merging, the way a jQuery plugin types `$.fn`.
*
* @example
* declare module "claude-code" { interface EngineInterface { voice: Voice } }
*/
export interface EngineInterface extends CoreEngineInterface {
}
/**
* What `next(e)` resolves to at `engine.create`: `$` as the steps beneath
* built it, typed as `$` is, open to nouns no declaration names yet.
*
* A withheld noun is on it as a stub (a step inside withheld it, or the
* last fold did and this is a reload), and the host refuses an op on one a
* step outside withholds later; a typed module bootstraps at load on it.
*/
export type EngineInterfaceBuilt = EngineInterface & {
readonly [noun: string]: unknown;
};
/**
* The engine's events' results.
*/
export type EngineResultOf = {
/**
* `{ result, context? }`, `{ deny }`, or core's `{ ref, result }`.
*/
'tool.call': ToolCallResult;
/**
* `{ decision, reason?, rule?, hook?, setting?, decider?, ceiling? }`.
*/
'tool.check': ToolCheckResult;
/**
* The tree to draw; `{ type: "engine", ref }` is core's own drawing.
*/
'ui.render': RenderElement;
/**
* The surface's element table (Elements[e.surface]): constructors from props
* to a RenderElement.
*/
'ui.resolve': ElementTable;
/**
* `{ element }`: the element whose handler the press reached.
*/
'ui.press': UiPressResult;
/**
* `{ element, value }`: the field whose handler the input reached, and
* the text it received.
*/
'ui.input': UiInputResult;
/**
* `{ element, value }`: the picker whose handler the pick reached, and
* the value it received.
*/
'ui.select': UiSelectResult;
/**
* `{ props? }`: the posting instance's next props, when a hook hands some.
*/
'ui.message': UiMessageResult;
/**
* `{}` once the window moved, or `{ deny }`.
*/
'ui.scroll': UiScrollResult;
/**
* `{}` once the ring moved, or `{ deny }`.
*/
'ui.focus': UiFocusResult;
/**
* `{ isOffered }`.
*/
'agent.offer': AgentOfferResult;
/**
* `{ model }` or `{ deny }`.
*/
'agent.spawn': AgentSpawnResult;
/**
* `{ text, context? }` or `{ drop }`.
*/
'prompt.submit': PromptSubmitResult;
/**
* `{ isFilled }`.
*/
'prompt.fill': PromptFillResult;
/**
* `{ isShown }`.
*/
'prompt.suggest': PromptSuggestResult;
/**
* `{ text, cursor }`, the box the editor shows next.
*/
'prompt.edit': PromptEditResult;
/**
* `{ text }` (null leaves the section out).
*/
'prompt.section': PromptSectionResult;
/**
* `{ blocks }` (a block left out is not sent).
*/
'prompt.context': PromptContextResult;
/**
* `{ sections }`, every `shared` one ahead of every `session` one (a
* section left out is not sent).
*/
'prompt.compose': PromptComposeResult;
/**
* `{ text }` (null leaves the attachment out).
*/
'prompt.attachment': PromptAttachmentResult;
/**
* `{ description, isDeferred? }`.
*/
'tool.describe': ToolDescribeResult;
/**
* `{ text }` (the command's output, when it printed one).
*/
'command.run': CommandRunResult;
/**
* `{ description, argumentHint, isHidden }`.
*/
'command.describe': CommandDescribeResult;
/**
* `{ value }` once written, or `{ deny }`.
*/
'config.set': ConfigSetResult;
/**
* `{ label, description, isHidden }`.
*/
'config.describe': ConfigDescribeResult;
/**
* `{ text }`.
*/
'skill.prompt': SkillPromptResult;
/**
* `{ text }`.
*/
'attribution.text': AttributionTextResult;
/**
* `{ cwd }`.
*/
'session.start': SessionStartResult;
/**
* `{ text }`, or `{ consumed }`.
*/
'session.receive': SessionReceiveResult;
/**
* `{ message, uuid }`, the row as stored.
*/
'session.append': SessionAppendResult;
/**
* `{ messages, tokensBefore?, tokensAfter? }`, or `{ skip }`.
*/
'session.compact': SessionCompactResult;
/**
* `{ clientId }`.
*/
'session.attach': SessionAttachResult;
/**
* `{ clientId }`.
*/
'session.detach': SessionDetachResult;
/**
* `{ changed }`.
*/
'session.measure': SessionMeasureResult;
/**
* `{ sessionId }`.
*/
'session.end': SessionEndResult;
/**
* `{ allow: true }`, or `{ refuse }`.
*/
'plugin.register': PluginRegisterResult;
/**
* `{ turnId }`.
*/
'turn.start': TurnStartResult;
/**
* The response: `{ turnId, index, answer, toolUses, stopReason, usage }`.
*/
'turn.step': TurnStepResult;
/**
* `{ text }`.
*/
'turn.complete': TurnCompleteResult;
/**
* `$` as built so far, with this plugin's interface added and any it
* withheld left out; `next(e)` resolves to EngineInterfaceBuilt.
*/
'engine.create': EngineCreateResult;
};
/**
* The engine's own events as calls on `$`, one signature each:
* `$..(input)` resolves to its result, or to its stream.
*
* The engine raises its events through these same calls; a plugin's call
* runs the same chain with the calling hook alone skipped. `input` may leave
* out what the engine fills (`tool_use_id`, the parent agent).
*/
export type EventCalls = {
tool: {
call: ToolCallOverloads;
check: (input: ToolCheckArgs) => Promise;
describe: (input: ToolDescribeInput) => Promise;
};
command: {
run: (input: CommandRunArgs) => Promise;
describe: (input: CommandDescribeInput) => Promise;
};
config: {
set: (input: ConfigSetArgs) => Promise;
describe: (input: ConfigDescribeInput) => Promise;
};
prompt: {
submit: (input: PromptSubmitArgs) => Promise;
fill: (input: PromptFillArgs) => Promise;
suggest: (input: PromptSuggestArgs) => Promise;
section: (input: PromptSectionInput) => Promise;
context: (input: PromptContextInput) => Promise;
attachment: (input: PromptAttachmentInput) => Promise;
compose: (input?: PromptComposeArgs) => Promise;
};
skill: {
prompt: (input: SkillPromptInput) => Promise;
};
attribution: {
text: (input: AttributionTextInput) => Promise;
};
agent: {
offer: (input: AgentOfferInput) => Promise;
spawn: (input: AgentSpawnArgs) => Promise;
};
session: {
start: (input: SessionStartInput) => Promise;
receive: (input: SessionReceiveInput) => Promise;
append: (input: SessionAppendInput) => Promise;
compact: (input?: SessionCompactArgs) => Promise;
attach: (input: SessionAttachInput) => Promise;
detach: (input: SessionDetachInput) => Promise;
measure: (input: SessionMeasureInput) => Promise;
end: (input: SessionEndInput) => Promise;
};
turn: {
start: (input: TurnStartInput) => Promise;
step: (input: TurnStepInput) => HookStream;
complete: (input: TurnCompleteInput) => Promise;
};
ui: {
render: (input: RenderInput) => Promise;
resolve: (e: E) => Elements[E['surface']];
scroll: (input: UiScrollArgs) => Promise;
focus: (input: UiFocusArgs) => Promise;
};
};
/**
* The name of an event: a key of EventOf, the engine's own (CoreEventName)
* and the declared plugin nouns' (NounEventName).
*/
export type EventName = keyof EventOf;
/**
* The argument of each event, by event name: what a hook receives as `e`
* and what the call on `$` takes. Plain data, frozen to every depth.
*
* The engine's own events (CoreEventOf: a plugin's `$.fs.write(...)` is
* a dispatch the hooks above it see) and the methods of the plugin nouns
* declared on EngineInterface (NounEventOf).
*/
export type EventOf = CoreEventOf & NounEventOf;
/**
* The result of event `N`: what its hooks return and what their `next(e)`
* resolves to.
*/
export type EventResult = ResultOf[N];
/**
* The hook signature of each event, `($, e, next)`, as one mapped type over
* EventOf; a streaming event's is the generator form (StreamHook).
*
* With one handler type per event, `Events[E]` for a generic E would be a
* union; as one mapped type it stays a single function type the engine
* calls without a cast (TS 4.6 correlated unions).
*
* @param $ the engine interface, frozen, the same object at every invocation;
* at `engine.create` the empty table, since `$` exists after the fold
* @param e the event's argument, frozen to every depth (`e.command = "ls"`
* is a type error and throws); a rewrite is a copy passed to `next`
* @param next the rest of the chain; a chain a hook raises through `$` skips
* this hook and runs every other, its plugin's others too
*/
export type Events = {
[E in keyof EventOf]: E extends StreamingEventName ? StreamHook : ($: E extends 'engine.create' ? NoEngineInterface : EngineInterface, e: Frozen>, next: Next) => EventResult | Promise>;
};
type ExitReason = 'clear' | 'resume' | 'logout' | 'prompt_input_exit' | 'other';
type FileChangedHookInput = BaseHookInput & {
hook_event_name: 'FileChanged';
file_path: string;
event: 'change' | 'add' | 'unlink';
};
/**
* `T` with every property read-only to every depth, arrays and tuples kept
* as declared: how a hook's `e` is typed.
*
* `e.command = 'x'` is a type error; `next({ ...e, command: 'x' })` compiles.
*/
export type Frozen = T extends (...args: never[]) => unknown ? T : T extends readonly unknown[] ? {
[K in keyof T]: Frozen;
} : T extends object ? {
readonly [K in keyof T]: Frozen;
} : T;
/**
* One file `$.fs.ancestors` found: the directory it stands in, the name it
* was asked for by, and its text as the engine's memory loader reads it.
*/
export type FsAncestor = {
/**
* The directory the file stands in, absolute.
*/
dir: string;
/**
* The spelling the caller asked for it by.
*/
name: string;
/**
* The file's text, with what its `@include`s bring after it.
*/
content: string;
/**
* The file and then each file its `@` imports brought, in load order,
* path and text apiece; `content` is these texts joined.
*/
parts: readonly FsAncestorPart[];
};
/**
* One file of an ancestor entry: the file itself or one it imported.
*/
export type FsAncestorPart = {
/**
* The file's path, absolute.
*/
path: string;
/**
* Its text as the engine's memory loader reads it.
*/
content: string;
};
/**
* The argument of `$.fs.ancestors`: the file names to look for in each
* directory, the file to walk down to, and the directory to walk beneath.
*/
export type FsAncestorsRequest = {
/**
* Relative `.md` file names, each looked for in every directory.
*/
names: readonly string[];
/**
* The file the walk goes on down to the directory of, relative to the
* working directory or absolute; absent, it ends at the working directory.
*/
of?: string;
/**
* The directory the walk starts beneath, relative to the working directory
* or absolute; absent, the walk starts at the filesystem root.
*
* Only directories strictly inside it are read, so with `of` a file under
* the project root the walk reads the directories between the two, as the
* engine reads a nested CLAUDE.md; a file not inside it finds nothing.
*/
below?: string;
};
/**
* What `$.fs.read(path, { as: "bytes" })` resolves with: the file's bytes,
* base64, since only plain data crosses into a plugin's environment.
*/
export type FsBytes = {
/**
* The file's content, standard padded base64;
* `Uint8Array.fromBase64(base64)` gives the bytes back.
*/
base64: string;
};
/**
* One entry of `$.fs.list`: the entry itself, a link not followed.
*/
export type FsEntry = {
/**
* The entry's name (no directory part).
*/
name: string;
/**
* `file`, `dir`, or `other`, of the entry itself: a symbolic link is
* `other` (`$.fs.stat` says what it leads to, and with `resolve` where).
*/
kind: 'file' | 'dir' | 'other';
/**
* Bytes, for a file.
*/
size: number;
/**
* True when the entry is a symbolic link.
*/
isLink: boolean;
};
/**
* How `$.fs.read` answers: `text` (UTF-8, the default) or `bytes` (base64).
*/
export type FsReadAs = 'text' | 'bytes';
/**
* The options of `$.fs.read` that ask for the bytes: the call answers
* `{ base64 }`.
*/
export type FsReadBytesOptions = {
/**
* `bytes`.
*/
as: 'bytes';
};
/**
* `$.fs.read`: the file's text, or with `{ as: "bytes" }` its bytes as
* `{ base64 }`.
*/
export type FsReadCall = {
(path: string): Promise;
(path: string, options: FsReadBytesOptions): Promise;
(path: string, options: FsReadOptions): Promise;
};
/**
* The options of `$.fs.read`.
*/
export type FsReadOptions = {
/**
* `bytes` answers `{ base64 }` for a binary file; `text` (the default) the
* file's text.
*/
as: FsReadAs;
};
/**
* What `$.fs.stat` resolves with: what the path leads to, whether the path
* itself is a symbolic link, and where it lands when asked.
*/
export type FsStat = {
/**
* `file`, `dir`, or `other`, of what the path leads to: a link is
* followed, and one that leads nowhere is `other`.
*/
kind: 'file' | 'dir' | 'other';
/**
* Bytes, for a file.
*/
size: number;
/**
* Last modification, milliseconds since the epoch.
*/
mtimeMs: number;
/**
* True when the path itself is a symbolic link; `kind`, `size` and
* `mtimeMs` then describe what it points at, or the link when that is gone.
*/
isLink: boolean;
/**
* Where the path landed when asked with `{ resolve: true }`: absolute,
* every symbolic link followed, `.` and `..` folded; else absent.
*
* Absent too when the path leads nowhere or a hook above withheld it, so
* a guard denies without it; and a hard link, a volume or file-id spelling
* (macOS `/.vol/`) or a case alias keeps its own spelling, `isLink` false.
*
* @remarks a deny-list on spellings is thus best effort; an allow-list on
* `realPath` under a root resolved the same way is the robust guard.
*/
realPath?: string;
};
/**
* The options of `$.fs.stat`.
*/
export type FsStatOptions = {
/**
* True to resolve where the path lands as well.
*
* The answer then carries `realPath` when the path leads somewhere, at
* the cost of one more file system call: where it landed then, not a hold
* on what a tool opens afterwards.
*/
resolve: boolean;
};
/**
* Every event (`*`), or every event under a namespace (`classic.*`: each
* one whose name starts with `classic.`).
*/
export type Glob = '*' | `${Namespace}.*`;
/**
* The hook `on(pattern, hook)` takes for a glob or a negation: one function
* placed on every selected event, `e` and the result typed as their union.
*
* `next.event` says which event a run is; `next.is(pattern, e)` narrows `e`
* to one of them. At `engine.create` (a negation may select it) `$` is the
* empty table and the hook observes the fold, as a `*` hook does.
*/
export type GlobHook> = ($: EngineInterface, e: Frozen>, next: GlobNext) => EventResult | Promise>;
/**
* `next` in a hook on a glob or a negation: an overload per selected event,
* then one over their union for an `e` not yet narrowed.
*
* `is` narrows `e` to the selected events its pattern names; `event` is one
* of the selected names.
*/
export type GlobNext> = OrderedOverloads & {
(e: Args): Promise>;
/**
* Continues this dispatch at a tier, as Next's `to` (a managed hook's),
* over the selected events' union for an `e` not yet narrowed.
*/
readonly to: (e: Args, tier: TargetTier) => Promise>;
readonly signal: AbortSignal;
readonly is: >(pattern: M, e: unknown) => e is Frozen>>>;
readonly event: N;
readonly origin: Origin;
readonly trace: readonly TraceEntry, GlobNextResult>[];
readonly budget: NextBudget;
};
/**
* What `next(e)` resolves to in a glob hook before `e` is narrowed: the
* NextResult of each selected event, as a union.
*/
type GlobNextResult = {
[K in N]: NextResult;
}[N];
/**
* One hook, `($, e, next)`, on event `E`.
*/
export type Hook = Events[E];
/**
* The time bounds every hook runs under, in milliseconds: the engine's own
* constants are typed by these members, and `next.budget` reads the live one.
*
* Each bounds the hook's OWN time: the clock stops while a `next(e)` call or
* any `$` call of the hook's is in flight (a `$.clock` wait excepted), so a
* slow chain beneath or a minute-long `$.model.complete` costs it nothing.
*
* @example
* await $.model.complete(ask) // a minute; next.budget.remainingMs unmoved
*/
export type HookBudget = {
/**
* A hook's budget per dispatch, from its call to its return; past it the
* hook is absent (its `.catch` asked, else `next(e)` run on its behalf).
*
* A streaming hook's (`turn.step`, an async generator) spans its whole
* run and counts only while its own code runs: never at a `yield`, never
* while it reads the stream beneath. Not per chunk: the sum of its work.
*/
readonly ms: 10_000;
/**
* A `.catch` handler's grace: a fresh budget from the moment it is called,
* on the same clock (its `next` replay and its `$` calls are free).
*
* Past it the hook is absent as if it had no handler; `next.error.budget`
* and `next.budget.ms` both read it there. `engine.create` has no budget.
*/
readonly catchMs: 1_000;
/**
* How long a hook may keep running after `next.signal` aborted (the person
* interrupted, a hook above settled first, its own budget ran out).
*
* Past it the hook is reported as lingering; the dispatch had already gone
* on without it when the signal aborted.
*/
readonly lingerMs: 5_000;
};
/**
* Why a hook failed, as its `.catch` handler reads it on `next.error`: plain
* frozen data.
*
* `throw`: the hook threw, or returned what the site refuses, `message`
* saying what; `timeout`: it outran its budget, `message` then what its last
* `next()` rejected with, if it did. `budget` is the handler's own grace.
*/
export type HookFailure = {
readonly kind: 'throw' | 'timeout';
/**
* The thrown error's message, or for a timeout what the hook's last
* `next()` rejected with; absent for a timeout with nothing rejected.
*/
readonly message?: string;
/**
* The grace the handler runs under, in milliseconds; past it, the hook is
* absent as if it had no handler.
*/
readonly budget: number;
};
/**
* The hook type per pattern: an event's own (Events), `*`'s (AnyEventHook),
* or a glob's over the events it selects (GlobHook), as one conditional type.
*
* One type, so the two-argument `on` stays ONE generic signature: as an
* overload set the language service offers no tool-name completions inside
* `e.tool === "`; as an index into a table TS intersects every argument.
*/
export type HookFor = P extends '*' ? AnyEventHook : P extends EventName ? Events[P] : GlobHook
;
type HookInput = PreToolUseHookInput | PostToolUseHookInput | PostToolUseFailureHookInput | PostToolBatchHookInput | PermissionDeniedHookInput | NotificationHookInput | UserPromptSubmitHookInput | UserPromptExpansionHookInput | SessionStartHookInput | SessionEndHookInput | StopHookInput | StopFailureHookInput | SubagentStartHookInput | SubagentStopHookInput | PreCompactHookInput | PostCompactHookInput | PreModelSwitchHookInput | PostModelSwitchHookInput | PermissionRequestHookInput | SetupHookInput | TeammateIdleHookInput | TaskCreatedHookInput | TaskCompletedHookInput | ElicitationHookInput | ElicitationResultHookInput | ConfigChangeHookInput | InstructionsLoadedHookInput | WorktreeCreateHookInput | WorktreeRemoveHookInput | CwdChangedHookInput | FileChangedHookInput | DirectoryAddedHookInput | MessageDisplayHookInput;
/**
* The hook event `E` takes: an async generator over its chunks for a
* streaming event (`turn.step`), `($, e, next) => result` for every other.
*/
export type HookOf = Events[E];
/**
* What a hooks module exports: `register`, and nothing the loader reads
* besides.
*/
export type HooksModule = {
register: Register;
};
/**
* What `next(e)` returns on a streaming event: the stream of everything
* beneath, chunk by chunk, whose return value is the result from beneath.
*
* `yield* next(e)` forwards the chunks and evaluates to that result; a
* transforming hook reads `for await (const chunk of stream)` and then
* `await stream.result`. Each call runs what is beneath afresh.
*/
export type HookStream = AsyncGenerator & {
/**
* Settles with what beneath returned once the stream has been read to
* its end; rejects if the stream is closed before that.
*/
readonly result: Promise;
};
/**
* Options of `$.http.fetch`.
*/
export type HttpInit = {
/**
* `GET` (default), `POST`, ...
*/
method?: string;
/**
* Request headers.
*/
headers?: Record;
/**
* The request body, as text.
*/
body?: string;
/**
* The handle `$.session.authorize()` answered: the engine sets the
* session's credential header itself, only for a first-party host.
*/
auth?: string;
/**
* The absolute path of a Unix domain socket the request goes over instead
* of TCP (near 100 B at most): one per session, in a private directory.
*
* The URL still gives path, query and Host; redirects stay on it; no proxy;
* `https:` is TLS verified against the URL's host (always, once `auth`
* rides), so a credential reaches only its host. No relative path, no NUL.
*
* @example
* await $.http.fetch(url, { socketPath: `${runDirectory}/bridge.sock` })
*/
socketPath?: string;
};
/**
* What `$.http.fetch` resolves with.
*/
export type HttpResponse = {
/**
* The HTTP status code.
*/
status: number;
/**
* True for a 2xx status.
*/
ok: boolean;
/**
* Response headers, lower-cased names.
*/
headers: Record;
/**
* The body, as text.
*/
text: string;
};
/**
* A `$.ui.blit` argument swapping one of the caller's mounted keyed Images
* to its next picture; every call sends it, so a stream needs no generation.
*
* The surface writes them with its frames, some sixty a second: byte and
* `file` sources fold to the last per frame; every `shm` source reaches the
* terminal (it unlinks each), and is denied while frames are not written.
*
* @example await $.ui.blit({ requestId: 'browser', key: 'view',
* source: { shm: '/tb-4', format: 'rgb', width: 1280, height: 720 } })
*/
export type ImageBlitArgs = {
/**
* The site the Image is drawn in, by the `requestId` this plugin draws it
* under.
*/
requestId: string;
/**
* The Image's `key` in that drawing.
*/
key: string;
/**
* The next picture (ImageSource): bytes, or a name the terminal reads.
*/
source: ImageSource;
/**
* Refused unless it is the mounted Image's width in cells. Absent, that.
*/
columns?: number;
/**
* Refused unless it is the mounted Image's height in cells. Absent, that.
*/
rows?: number;
};
/**
* The props of `Image`, the terminal surface's picture leaf: pixels over a
* box of cells where the terminal can (kitty, Ghostty), the `alt` elsewhere.
*
* A leaf: no children, `hover` or `onPress`; the cells are text, so the
* picture scrolls and clips as a word does. Drawn again with another `source`
* it is replaced in place; keyed, `$.ui.blit` swaps it at the frame rate.
*
* @example const { bytes } = await $.fs.read('chart.png', { as: 'bytes' })
*
* @example
*/
export type ImageProps = {
/**
* The element's address within the drawing: what `$.ui.blit` names to swap.
*
* Unique among the Images of one tree; absent, only a redraw changes it.
*/
key?: string;
/**
* The picture (ImageSource): `{ png }` or `{ rgba, width, height }` bytes,
* or `{ file, format }` / `{ shm, format, width, height }` read here.
*/
source: ImageSource;
/** How many terminal columns wide, 1 to 255; the picture is scaled to fill
* the box and the site clips what its body cannot show. */
columns: number;
/**
* How many terminal rows tall, 1 to 255.
*/
rows: number;
/**
* What the picture says, drawn dim in its place where the picture cannot
* be (and read by a screen reader); required, may be a single space.
*/
alt: string;
};
/**
* The picture an `Image` shows: base64 bytes the plugin holds (at most 2 MiB
* decoded), or the name of a file or POSIX shared-memory object it does not.
*
* A name is one another process on this machine wrote; the terminal, running
* as the person, opens, reads and decodes it itself, so no pixel crosses `$`
* and the engine never touches it. One it will not read (not a regular file,
* under `/proc`, `/sys` or `/dev`, gone, across ssh) draws a blank box, its
* words in the debug log; it unlinks a shared-memory object once read, so
* each frame is a fresh one. A source equal to the last drawn sends nothing;
* `generation` makes new content under an unchanged name a new source.
*
* @example { rgba: pixels.toBase64(), width: 64, height: 32 }
* @example { file: '/dev/shm/frame-3.rgba', format: 'rgba', width: 640,
* height: 400 }
* @example { shm: '/tb-view-2', format: 'rgb', width: 1280, height: 720 }
*/
export type ImageSource = {
/**
* A whole PNG file, base64.
*/
png: string;
} | {
/**
* `width * height` RGBA pixels, 4 bytes each, base64.
*/
rgba: string;
/**
* Pixels per row, 1 to 2048.
*/
width: number;
/**
* Rows of pixels, 1 to 2048.
*/
height: number;
} | {
/**
* The absolute path of a regular file holding a whole PNG, at most
* 3072 bytes of path; read by the terminal, left in place.
*/
file: string;
/**
* The file is a whole PNG; the terminal decodes it and sizes it itself.
*/
format: 'png';
/**
* A whole number that changes when the file's content does under the
* same path, so a redraw reads it again; absent, the path alone tells.
*/
generation?: number;
} | {
/**
* The absolute path of a regular file of `width * height` raw pixels, at
* most 3072 bytes of path; read by the terminal, left in place.
*
* On Linux a file under `/dev/shm` is memory.
*/
file: string;
/**
* `rgba`, 4 bytes a pixel, or `rgb`, 3.
*/
format: 'rgba' | 'rgb';
/**
* Pixels per row, 1 to 4096.
*/
width: number;
/**
* Rows of pixels, 1 to 4096.
*/
height: number;
/**
* A whole number that changes when the file's content does under the
* same path, so a redraw reads it again; absent, the path alone tells.
*/
generation?: number;
} | {
/**
* The name of a POSIX shared-memory object of `width * height` raw
* pixels: `/` then up to 254 of `A-Z a-z 0-9 . _ -` (macOS takes 30).
*
* The terminal unlinks it after reading, so a name feeds one Image
* drawn once and is never sent again on a redraw; POSIX terminals only.
*/
shm: string;
/**
* `rgba`, 4 bytes a pixel, or `rgb`, 3.
*/
format: 'rgba' | 'rgb';
/**
* Pixels per row, 1 to 4096.
*/
width: number;
/**
* Rows of pixels, 1 to 4096.
*/
height: number;
/**
* A whole number that changes when a fresh object reuses a name, so a
* redraw reads it again; absent, the name alone tells.
*/
generation?: number;
};
/**
* The keys of object pattern `P` that object member `E` cannot satisfy, `D`
* levels down; `never` when there is none, which is what keeps the member.
*
* A key `E` does not have (an open record has every string key), or one
* whose value `P` narrows to nothing.
*/
type ImpossibleKeys = {
[K in keyof P]-?: K extends keyof E ? [NarrowedValue, P[K], D>] extends [never] ? K : never : K;
}[keyof P];
/**
* The props of `Input`, every surface's one-line text field: an address,
* optional texts, and the closures a change and a submit run. A leaf.
*
* Focused through the same ring as `Button` (`abovePrompt:focus`); while it
* has focus every printable key reaches it alone and Esc returns them; a
* change and Enter raise `ui.input`, whose bottom is `onInput` / `onSubmit`.
*/
export type InputProps = {
/**
* The element's address: `e.element` at `ui.input`, what a matcher names.
*/
key: string;
/**
* Text drawn before the field.
*/
label?: string;
/**
* Text drawn dim in an empty field.
*/
placeholder?: string;
/**
* The text the field holds when drawn; the person's typing replaces it
* until the hook draws another.
*/
value?: string;
/**
* What Enter does, in a word or two, drawn beside the field while it has
* focus (`send`). Defaults to `submit`.
*/
submitLabel?: string;
/**
* The site's focus ring starts here when the site takes the keyboard,
* instead of on nothing, as the DOM's `autofocus`: Enter acts on it at once.
*
* A pane opened with `focus`, or the person's focus chord or click, is the
* take. Of several in one site the first drawn wins; it raises `ui.focus`,
* origin this plugin. A ring the person has moved stays where it was put.
*/
autoFocus?: true;
/**
* Runs on every change of the text, in the plugin's own environment: the
* bottom of a `ui.input` chain of kind `change`.
*/
onInput?: (value: string, e: UiInputArgument) => void;
/**
* Runs on Enter with the text, in the plugin's own environment: the bottom
* of a `ui.input` chain of kind `submit`. No model turn unless it asks one.
*/
onSubmit: (value: string, e: UiInputArgument) => void;
};
/**
* Every tier an instruction file can belong to, for checking a hook's
* answer; the kind type is derived from this list.
*/
const INSTRUCTION_FILE_KINDS: readonly ["managed", "user", "project", "local", "memory"];
/**
* One instruction file behind the `claudeMd` block: where it was read, its
* tier, its text as loaded, and the file that `@`-imported it if one did.
*/
export type InstructionFile = {
/**
* The file's path, absolute.
*/
path: string;
/**
* Its tier.
*/
kind: InstructionFileKind;
/**
* The text as loaded (comments and frontmatter already stripped).
*/
content: string;
/**
* The path of the file whose `@` import brought this one, when one did.
*/
parent?: string;
};
/**
* What tier an instruction file belongs to: the organization's managed
* policy, the person's own, the project's checked-in or private ones, memory.
*/
export type InstructionFileKind = (typeof INSTRUCTION_FILE_KINDS)[number];
type InstructionsLoadedHookInput = BaseHookInput & {
hook_event_name: 'InstructionsLoaded';
file_path: string;
memory_type: 'User' | 'Project' | 'Local' | 'Managed';
load_reason: 'session_start' | 'nested_traversal' | 'path_glob_match' | 'include' | 'compact';
globs?: string[];
trigger_file_path?: string;
parent_file_path?: string;
};
/**
* What `$.ui.invalidate` takes: a render event, or one of the six events
* whose answers the engine caches for the session.
*/
export type InvalidatableEventName = RenderEventName | 'prompt.section' | 'prompt.context' | 'prompt.attachment' | 'tool.describe' | 'command.describe' | 'config.describe';
/**
* Whether tag key `K` selects members of `I`: it does when each member gives
* it ONE literal (`component: "ToolUse"` on the ToolUse variant).
*
* A key that is the same union on every member is a filter at runtime; it
* is left out of the selection so that it cannot defeat the narrowing the
* other keys give.
*/
type IsDiscriminant = I extends unknown ? K extends KnownKeys ? IsSingleLiteral : true : never;
/**
* Whether `V` is made of literals only: `"a" | "b"` is, `string` is not.
*/
type IsLiteralValued = string extends V ? false : number extends V ? false : boolean extends V ? false : [V] extends [string | number | boolean] ? true : false;
/**
* Whether `V` is exactly one string, number or boolean literal.
*/
type IsSingleLiteral = [V] extends [string | number | boolean] ? IsUnion extends true ? false : true : false;
/**
* Whether `T` is a union of two or more members.
*/
type IsUnion = T extends unknown ? [U] extends [T] ? false : true : never;
/**
* Plain data: what JSON holds, and what crosses between a plugin's hooks
* module and its surface module whole (a `Client`'s props, a post's data).
*
* A function, a class instance, `undefined` or a cycle is not plain data;
* the engine refuses one where it checks, and drops it where it clones.
*/
export type JsonValue = string | number | boolean | null | readonly JsonValue[] | {
readonly [key: string]: JsonValue;
};
/**
* What `next` takes in a matched hook: the variants of `e` the matcher can
* match (KeptMembers), as declared, so a rewrite of a pinned field passes.
*/
type KeptEvent = MatchedNames
extends infer N extends EventName ? N extends unknown ? KeptMembers, M> : never : never;
/**
* The members of the argument union `E` matcher `P` can match, as declared;
* what `next` takes, so a rewrite may change a field the matcher pinned.
*
* A member is dropped when `P` names a key it lacks, or gives a key, at any
* depth, a value none of that key's values can equal (ImpossibleKeys).
*/
type KeptMembers = E extends unknown ? [ImpossibleKeys] extends [never] ? E : never : never;
/**
* The declared keys of `T`, the string and number index signatures left out.
*/
type KnownKeys = keyof {
[K in keyof T as string extends K ? never : number extends K ? never : K]: 0;
};
/**
* The events whose overload must come after the rest, lest it shadow them.
*
* `classic.PreToolUse` shares `tool.call`'s envelope, `turn.abort` every
* turn event's `turnId`, and an object of any shape is assignable to NoArgs.
*/
type LateOverload = 'classic.PreToolUse' | 'turn.abort' | NoArgsEvent;
/**
* The props of `Link`, a hyperlink every surface draws: an OSC 8 span on the
* terminal (else its text then the URL in dim), an anchor on desktop.
*
* An inline element: its children are the text, strings and inline
* elements; absent children the `label`, absent both the URL. The engine
* bounds `href` before the tree crosses.
*/
export type LinkProps = {
/**
* Where the link goes: an `https:` URL (or `http://localhost`), at most
* 2048 characters of printable ASCII, spelled as `new URL(href).href`.
*
* No `user@host` part, no raw `@`, space or non-ASCII letter (encode them);
* anything else refuses the tree the Link is in.
*/
href: string;
/**
* The text drawn when the element has no children; absent both, the URL
* itself is the text.
*/
label?: string;
};
/**
* What a matcher value selects by: itself, or `unknown` for a RegExp, which
* selects nothing.
*/
type Literal = X extends RegExp ? unknown : X;
/**
* What a `Markdown` carries across the boundary: its address, text, dimness
* and which links it answers; `onLinkPress` stays behind, a `press` instead.
*/
export type MarkdownLeafProps = {
/**
* The element's address: what `e.element` carries and what a matcher
* names; present whenever the element carries a `press`.
*/
key?: string;
/**
* The markdown drawn, bounded as a Text's string is.
*/
text: string;
/**
* The whole block dim, as `Text`'s `dimColor`; absent draws as false.
*/
dimColor?: boolean;
/**
* Which links a press belongs to, by `href` as written; absent, with a
* `press`, every link drawn. Compared with the pressed target only.
*/
pressableLinks?: readonly string[];
};
/**
* The props of `Markdown`, a block of markdown every surface draws as it
* draws an assistant reply's text: its own renderer, links, tables, fences.
*
* A leaf: no children. `text` is the element's data as a `Text`'s string is,
* bounded the same way. With `onLinkPress` the links it draws are the
* plugin's to answer: a press on one raises `ui.press` addressed to `key`.
*/
export type MarkdownProps = {
/**
* The element's address: `e.element` at `ui.press`, what a matcher names.
* Required with `onLinkPress`, since a press needs one; else optional.
*/
key?: string;
/**
* The markdown drawn, as an assistant reply would write it.
*
* At most 10000 characters, tab and newline its only control characters;
* a link whose scheme is not `https:`, `http:` or `file:` draws as text,
* never clickable. Not drawn around the approval dialog.
*/
text: string;
/**
* The whole block dim, as `Text`'s `dimColor`: a thought, an aside.
*/
dimColor?: boolean;
/**
* What a press on a link in `text` runs, in the plugin's own environment:
* the bottom of a `ui.press` chain whose `e.link` names the link.
*
* A press is a plain single click where the surface reports clicks (the
* fullscreen terminal): it opens nothing and lands once no double-click
* followed; a ctrl, alt or terminal-kept cmd click opens it as before.
*/
onLinkPress?: (link: PressedLink, e: UiPressArgument) => void;
/**
* Which links in `text` a press belongs to, by `href` as the markdown
* writes it; absent, every link drawn. Only with `onLinkPress`.
*
* A link left out keeps the surface's own behaviour. At most 256 entries
* of at most 2048 characters; compared with the pressed link's target,
* never opened.
*/
pressableLinks?: readonly string[];
};
/**
* The argument a matched hook receives: `e` narrowed by `M` (Narrowed), per
* event the registration covers.
*/
export type MatchedEvent = MatchedNames
extends infer N extends EventName ? N extends unknown ? Narrowed, M> : never : never;
/**
* The hook `on(pattern, matcher, hook)` takes: `($, e, next)` with `e`
* narrowed by the matcher (MatchedEvent), and a tagged result the same way.
*
* On a streaming event it is the generator form (MatchedStreamHook). `next`
* takes the variants the matcher keeps, as declared (KeptEvent); `next.is`
* names the events the registration covers and narrows as the matcher does.
*/
export type MatchedHook = P extends StreamingEventName ? MatchedStreamHook
: ($: EngineInterface, e: Frozen>, next: Next, KeptEvent, MatchedResult
, {
[K in MatchedNames
]: Narrowed, M>;
}>) => MatchedResult | Promise>;
/**
* The events a matched registration on `P` covers: the event named, or for a
* glob every selected event whose input has each key the matcher names.
*/
type MatchedNames = P extends EventName ? P : {
[N in Selected
]: [M] extends [never] ? N : keyof M extends AnyKeyOf> ? N : never;
}[Selected];
/**
* What a matched hook returns: the event's result, narrowed by `M` where the
* result is a union tagged by the matcher's tag keys.
*/
export type MatchedResult
= MatchedNames
extends infer N extends EventName ? N extends unknown ? Select, Selection, M>> : never : never;
/**
* The hook `on(event, matcher, hook)` takes on a streaming event: the
* generator form, `e` narrowed by the matcher, `next(e)` the stream beneath.
*/
export type MatchedStreamHook = ($: EngineInterface, e: Frozen>, next: MatchedStreamNext) => StreamHookBody, MatchedResult>;
/**
* What a matched streaming hook's `next.is(pattern, e)` narrows `e` to:
* the streaming event's argument narrowed by the matcher.
*/
type MatchedStreamNarrowings
= {
[K in P]: Narrowed, M>;
};
/**
* A matched streaming hook's `next`: the variants the matcher keeps, the
* result tagged the same way, `next.is` narrowing as the matcher does.
*/
type MatchedStreamNext = StreamNext
, MatchedResult
, MatchedStreamNarrowings
>;
/**
* What `on(event, matcher, hook)` takes for an argument of type `I`: the
* shape of the `e` the hook wants, a partial of it at any depth.
*
* A leaf is `===` or a RegExp (`{ command: /^p4 / }`); an array is any-of;
* an object is a partial of an OBJECT, so `{ command: { startsWith } }` is
* a type error where `e` is typed and free where it is `unknown`.
*/
export type Matcher = I extends unknown ? {
readonly [K in KnownKeys