A mod is now a plugin built with defineMod() around its register(on,
options); the bridge is the ClaudeCodeMods service (ctx.claudeCodeMods)
that keeps the loaded mods and raises their events. The plugin.json /
hooks.json loader, pluginDirs, and dynamic imports are gone.
prompt.submit context becomes text blocks appended to the human's own
prompt message and $.prompt.submit queues a user-sourced message, so the
bridge adds no message source kind: the persistence record and the
regenerated schema are removed and the artifacts match master again.
Engine correctness from the review round: next() is memoized and every
started run beneath is awaited before a hook's answer settles; a rewritten
tool argument skips the hook with a report pointing at the pre-tool input
rewrite proposal; timers belong to the session whose event started them;
$.tool.call injects a tool's deferred contexts; $.clock.sleep honors the
event signal; a built-in tool answered with { result } is validated against
its output schema before it becomes an error-shaped result; hooks on events
this host never raises are reported at load.
Examples: the three mods from the Claude Code blog post under examples/,
Token Weather and its test unchanged, Blast Radius and Replay Theater
completed from the published fragments, run by their own tests through a
claude-code/testing alias over the new test kit ($.ui.mount/find/press).
42 lines
1.5 KiB
JavaScript
42 lines
1.5 KiB
JavaScript
// The tutorial mod from Claude Code's "Create a mod" page, unchanged apart
|
|
// from the configurable greeting: it counts tool calls, adds /tally, and
|
|
// restyles the spinner (a render site this bridge never raises).
|
|
|
|
// The count, shared by the hooks below
|
|
let calls = 0
|
|
|
|
// Claude Code calls this once when the mod loads
|
|
export function register(on, options) {
|
|
// Runs when the session starts, before your first prompt
|
|
on('session.start', async ($, e, next) => {
|
|
// Add the /tally command
|
|
await $.command.register({
|
|
name: 'tally',
|
|
description: 'Show how many tool calls Claude has made',
|
|
})
|
|
// Let the session start as usual
|
|
return next(e)
|
|
})
|
|
|
|
// Runs each time Claude is about to use a tool
|
|
on('tool.call', async ($, e, next) => {
|
|
calls += 1
|
|
// Ask Claude Code to draw the interface again, so the new count shows
|
|
$.ui.invalidate('ui.render')
|
|
// Let the tool run as usual
|
|
return next(e)
|
|
})
|
|
|
|
// Runs when you type /tally, and only then, because of the matcher
|
|
on('command.run', { command: 'tally' }, async () => {
|
|
// The text to print in the transcript
|
|
return { text: options.greeting + ' ' + calls + ' tool calls since this mod loaded' }
|
|
})
|
|
|
|
// Runs each time Claude Code draws the spinner
|
|
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
|
|
// Keep Claude Code's spinner, with the count added after its word
|
|
return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
|
|
})
|
|
}
|