WIP: Add hook API for dynamic tool control with plan-mode hook example

- Add pi.getTools() and pi.setTools(toolNames) to HookAPI
- Hooks can now enable/disable tools dynamically
- Changes take effect on next agent turn

New example hook: plan-mode.ts
- Claude Code-style read-only exploration mode
- /plan command toggles plan mode on/off
- Plan mode tools: read, bash, grep, find, ls
- Edit/write tools disabled in plan mode
- Injects context telling agent about restrictions
- After each response, prompts to execute/stay/refine
- State persists across sessions
This commit is contained in:
Helmut Januschka 2026-01-03 09:31:39 +01:00
parent 6ddfd1be13
commit 57bba4e32b
14 changed files with 304 additions and 8 deletions

View file

@ -61,6 +61,16 @@ export type SendMessageHandler = <T = unknown>(
*/
export type AppendEntryHandler = <T = unknown>(customType: string, data?: T) => void;
/**
* Get tools handler type for pi.getTools().
*/
export type GetToolsHandler = () => string[];
/**
* Set tools handler type for pi.setTools().
*/
export type SetToolsHandler = (toolNames: string[]) => void;
/**
* New session handler type for ctx.newSession() in HookCommandContext.
*/
@ -100,6 +110,10 @@ export interface LoadedHook {
setSendMessageHandler: (handler: SendMessageHandler) => void;
/** Set the append entry handler for this hook's pi.appendEntry() */
setAppendEntryHandler: (handler: AppendEntryHandler) => void;
/** Set the get tools handler for this hook's pi.getTools() */
setGetToolsHandler: (handler: GetToolsHandler) => void;
/** Set the set tools handler for this hook's pi.setTools() */
setSetToolsHandler: (handler: SetToolsHandler) => void;
}
/**
@ -159,6 +173,8 @@ function createHookAPI(
commands: Map<string, RegisteredCommand>;
setSendMessageHandler: (handler: SendMessageHandler) => void;
setAppendEntryHandler: (handler: AppendEntryHandler) => void;
setGetToolsHandler: (handler: GetToolsHandler) => void;
setSetToolsHandler: (handler: SetToolsHandler) => void;
} {
let sendMessageHandler: SendMessageHandler = () => {
// Default no-op until mode sets the handler
@ -166,6 +182,10 @@ function createHookAPI(
let appendEntryHandler: AppendEntryHandler = () => {
// Default no-op until mode sets the handler
};
let getToolsHandler: GetToolsHandler = () => [];
let setToolsHandler: SetToolsHandler = () => {
// Default no-op until mode sets the handler
};
const messageRenderers = new Map<string, HookMessageRenderer>();
const commands = new Map<string, RegisteredCommand>();
@ -195,6 +215,12 @@ function createHookAPI(
exec(command: string, args: string[], options?: ExecOptions) {
return execCommand(command, args, options?.cwd ?? cwd, options);
},
getTools(): string[] {
return getToolsHandler();
},
setTools(toolNames: string[]): void {
setToolsHandler(toolNames);
},
} as HookAPI;
return {
@ -207,6 +233,12 @@ function createHookAPI(
setAppendEntryHandler: (handler: AppendEntryHandler) => {
appendEntryHandler = handler;
},
setGetToolsHandler: (handler: GetToolsHandler) => {
getToolsHandler = handler;
},
setSetToolsHandler: (handler: SetToolsHandler) => {
setToolsHandler = handler;
},
};
}
@ -234,10 +266,15 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo
// Create handlers map and API
const handlers = new Map<string, HandlerFn[]>();
const { api, messageRenderers, commands, setSendMessageHandler, setAppendEntryHandler } = createHookAPI(
handlers,
cwd,
);
const {
api,
messageRenderers,
commands,
setSendMessageHandler,
setAppendEntryHandler,
setGetToolsHandler,
setSetToolsHandler,
} = createHookAPI(handlers, cwd);
// Call factory to register handlers
factory(api);
@ -251,6 +288,8 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo
commands,
setSendMessageHandler,
setAppendEntryHandler,
setGetToolsHandler,
setSetToolsHandler,
},
error: null,
};