mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 11:02:17 +00:00
feat(coding-agent): add hook API for CLI flags, shortcuts, and tool control
Hook API additions: - pi.getTools() / pi.setTools(toolNames) - dynamically enable/disable tools - pi.registerFlag(name, options) / pi.getFlag(name) - register custom CLI flags - pi.registerShortcut(shortcut, options) - register keyboard shortcuts Plan mode hook (examples/hooks/plan-mode.ts): - /plan command or Shift+P shortcut to toggle - --plan CLI flag to start in plan mode - Read-only tools: read, bash, grep, find, ls - Bash restricted to non-destructive commands (blocks rm, mv, git commit, etc.) - Interactive prompt after each response: execute, stay, or refine - Shows plan indicator in footer when active - State persists across sessions
This commit is contained in:
parent
059292ead1
commit
c956a726ed
13 changed files with 636 additions and 46 deletions
|
|
@ -38,7 +38,9 @@
|
|||
|
||||
- `$ARGUMENTS` syntax for custom slash commands as alternative to `$@` for all arguments joined. Aligns with patterns used by Claude, Codex, and OpenCode. Both syntaxes remain fully supported. ([#418](https://github.com/badlogic/pi-mono/pull/418) by [@skuridin](https://github.com/skuridin))
|
||||
- Hook API: `pi.getTools()` and `pi.setTools(toolNames)` for dynamically enabling/disabling tools from hooks
|
||||
- New example hook: `plan-mode.ts` - Claude Code-style read-only exploration mode with `/plan` command
|
||||
- Hook API: `pi.registerFlag(name, options)` and `pi.getFlag(name)` for hooks to register custom CLI flags
|
||||
- Hook API: `pi.registerShortcut(shortcut, options)` for hooks to register custom keyboard shortcuts
|
||||
- New example hook: `plan-mode.ts` - Claude Code-style read-only exploration mode with `/plan` command, `--plan` flag, and Shift+P shortcut
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
|||
|
|
@ -775,6 +775,44 @@ pi.setTools(["read", "bash", "edit", "write"]);
|
|||
|
||||
Only built-in tools can be enabled/disabled. Custom tools are always active. Unknown tool names are ignored.
|
||||
|
||||
### pi.registerFlag(name, options)
|
||||
|
||||
Register a CLI flag for this hook. Flag values are accessible via `pi.getFlag()`.
|
||||
|
||||
```typescript
|
||||
pi.registerFlag("plan", {
|
||||
description: "Start in plan mode (read-only)",
|
||||
type: "boolean", // or "string"
|
||||
default: false,
|
||||
});
|
||||
```
|
||||
|
||||
### pi.getFlag(name)
|
||||
|
||||
Get the value of a CLI flag registered by this hook.
|
||||
|
||||
```typescript
|
||||
if (pi.getFlag("plan") === true) {
|
||||
// plan mode enabled via --plan flag
|
||||
}
|
||||
```
|
||||
|
||||
### pi.registerShortcut(shortcut, options)
|
||||
|
||||
Register a keyboard shortcut for this hook. The handler is called when the shortcut is pressed.
|
||||
|
||||
```typescript
|
||||
pi.registerShortcut("shift+p", {
|
||||
description: "Toggle plan mode",
|
||||
handler: async (ctx) => {
|
||||
// toggle mode
|
||||
ctx.ui.notify("Plan mode toggled");
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Shortcut format: `modifier+key` where modifier can be `shift`, `ctrl`, `alt`, or combinations like `ctrl+shift`.
|
||||
|
||||
## Examples
|
||||
|
||||
### Permission Gate
|
||||
|
|
|
|||
|
|
@ -9,13 +9,15 @@
|
|||
* - In plan mode: only read, bash (read-only), grep, find, ls are available
|
||||
* - Injects system context telling the agent about the restrictions
|
||||
* - After each agent response, prompts to execute the plan or continue planning
|
||||
* - Shows "plan" indicator in footer when active
|
||||
*
|
||||
* Usage:
|
||||
* 1. Copy this file to ~/.pi/agent/hooks/ or your project's .pi/hooks/
|
||||
* 2. Use /plan to toggle plan mode on/off
|
||||
* 3. Or start in plan mode: PI_PLAN_MODE=1 pi
|
||||
*/
|
||||
|
||||
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
|
||||
import type { HookAPI, HookContext } from "@mariozechner/pi-coding-agent/hooks";
|
||||
|
||||
// Read-only tools for plan mode
|
||||
const PLAN_MODE_TOOLS = ["read", "bash", "grep", "find", "ls"];
|
||||
|
|
@ -23,28 +25,190 @@ const PLAN_MODE_TOOLS = ["read", "bash", "grep", "find", "ls"];
|
|||
// Full set of tools for normal mode
|
||||
const NORMAL_MODE_TOOLS = ["read", "bash", "edit", "write"];
|
||||
|
||||
// Patterns for destructive bash commands that should be blocked in plan mode
|
||||
const DESTRUCTIVE_PATTERNS = [
|
||||
// File/directory modification
|
||||
/\brm\b/i,
|
||||
/\brmdir\b/i,
|
||||
/\bmv\b/i,
|
||||
/\bcp\b/i, // cp can overwrite files
|
||||
/\bmkdir\b/i,
|
||||
/\btouch\b/i,
|
||||
/\bchmod\b/i,
|
||||
/\bchown\b/i,
|
||||
/\bchgrp\b/i,
|
||||
/\bln\b/i, // symlinks
|
||||
// File content modification
|
||||
/\btee\b/i,
|
||||
/\btruncate\b/i,
|
||||
/\bdd\b/i,
|
||||
/\bshred\b/i,
|
||||
// Redirects that write to files
|
||||
/[^<]>(?!>)/, // > but not >> or <>
|
||||
/>>/, // append
|
||||
// Package managers / installers
|
||||
/\bnpm\s+(install|uninstall|update|ci|link|publish)/i,
|
||||
/\byarn\s+(add|remove|install|publish)/i,
|
||||
/\bpnpm\s+(add|remove|install|publish)/i,
|
||||
/\bpip\s+(install|uninstall)/i,
|
||||
/\bapt(-get)?\s+(install|remove|purge|update|upgrade)/i,
|
||||
/\bbrew\s+(install|uninstall|upgrade)/i,
|
||||
// Git write operations
|
||||
/\bgit\s+(add|commit|push|pull|merge|rebase|reset|checkout\s+-b|branch\s+-[dD]|stash|cherry-pick|revert|tag|init|clone)/i,
|
||||
// Other dangerous commands
|
||||
/\bsudo\b/i,
|
||||
/\bsu\b/i,
|
||||
/\bkill\b/i,
|
||||
/\bpkill\b/i,
|
||||
/\bkillall\b/i,
|
||||
/\breboot\b/i,
|
||||
/\bshutdown\b/i,
|
||||
/\bsystemctl\s+(start|stop|restart|enable|disable)/i,
|
||||
/\bservice\s+\S+\s+(start|stop|restart)/i,
|
||||
// Editors (interactive, could modify files)
|
||||
/\b(vim?|nano|emacs|code|subl)\b/i,
|
||||
];
|
||||
|
||||
// Read-only commands that are always safe
|
||||
const SAFE_COMMANDS = [
|
||||
/^\s*cat\b/,
|
||||
/^\s*head\b/,
|
||||
/^\s*tail\b/,
|
||||
/^\s*less\b/,
|
||||
/^\s*more\b/,
|
||||
/^\s*grep\b/,
|
||||
/^\s*find\b/,
|
||||
/^\s*ls\b/,
|
||||
/^\s*pwd\b/,
|
||||
/^\s*echo\b/,
|
||||
/^\s*printf\b/,
|
||||
/^\s*wc\b/,
|
||||
/^\s*sort\b/,
|
||||
/^\s*uniq\b/,
|
||||
/^\s*diff\b/,
|
||||
/^\s*file\b/,
|
||||
/^\s*stat\b/,
|
||||
/^\s*du\b/,
|
||||
/^\s*df\b/,
|
||||
/^\s*tree\b/,
|
||||
/^\s*which\b/,
|
||||
/^\s*whereis\b/,
|
||||
/^\s*type\b/,
|
||||
/^\s*env\b/,
|
||||
/^\s*printenv\b/,
|
||||
/^\s*uname\b/,
|
||||
/^\s*whoami\b/,
|
||||
/^\s*id\b/,
|
||||
/^\s*date\b/,
|
||||
/^\s*cal\b/,
|
||||
/^\s*uptime\b/,
|
||||
/^\s*ps\b/,
|
||||
/^\s*top\b/,
|
||||
/^\s*htop\b/,
|
||||
/^\s*free\b/,
|
||||
/^\s*git\s+(status|log|diff|show|branch|remote|config\s+--get)/i,
|
||||
/^\s*git\s+ls-/i,
|
||||
/^\s*npm\s+(list|ls|view|info|search|outdated|audit)/i,
|
||||
/^\s*yarn\s+(list|info|why|audit)/i,
|
||||
/^\s*node\s+--version/i,
|
||||
/^\s*python\s+--version/i,
|
||||
/^\s*curl\s/i, // curl without -o is usually safe (reading)
|
||||
/^\s*wget\s+-O\s*-/i, // wget to stdout only
|
||||
/^\s*jq\b/,
|
||||
/^\s*sed\s+-n/i, // sed with -n (no auto-print) for reading only
|
||||
/^\s*awk\b/,
|
||||
/^\s*rg\b/, // ripgrep
|
||||
/^\s*fd\b/, // fd-find
|
||||
/^\s*bat\b/, // bat (cat clone)
|
||||
/^\s*exa\b/, // exa (ls clone)
|
||||
];
|
||||
|
||||
/**
|
||||
* Check if a bash command is safe (read-only) for plan mode.
|
||||
*/
|
||||
function isSafeCommand(command: string): boolean {
|
||||
// Check if it's an explicitly safe command
|
||||
if (SAFE_COMMANDS.some((pattern) => pattern.test(command))) {
|
||||
// But still check for destructive patterns (e.g., cat > file)
|
||||
if (!DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(command))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for destructive patterns
|
||||
if (DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(command))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allow commands that don't match any destructive pattern
|
||||
// This is permissive - unknown commands are allowed
|
||||
return true;
|
||||
}
|
||||
|
||||
export default function planModeHook(pi: HookAPI) {
|
||||
// Track plan mode state
|
||||
let planModeEnabled = false;
|
||||
|
||||
// Register --plan CLI flag
|
||||
pi.registerFlag("plan", {
|
||||
description: "Start in plan mode (read-only exploration)",
|
||||
type: "boolean",
|
||||
default: false,
|
||||
});
|
||||
|
||||
// Helper to update footer status
|
||||
function updateStatus(ctx: HookContext) {
|
||||
if (planModeEnabled) {
|
||||
ctx.ui.setStatus("plan-mode", ctx.ui.theme.fg("warning", "⏸ plan"));
|
||||
} else {
|
||||
ctx.ui.setStatus("plan-mode", undefined);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to toggle plan mode
|
||||
function togglePlanMode(ctx: HookContext) {
|
||||
planModeEnabled = !planModeEnabled;
|
||||
|
||||
if (planModeEnabled) {
|
||||
pi.setTools(PLAN_MODE_TOOLS);
|
||||
ctx.ui.notify(`Plan mode enabled. Tools: ${PLAN_MODE_TOOLS.join(", ")}`);
|
||||
} else {
|
||||
pi.setTools(NORMAL_MODE_TOOLS);
|
||||
ctx.ui.notify("Plan mode disabled. Full access restored.");
|
||||
}
|
||||
updateStatus(ctx);
|
||||
}
|
||||
|
||||
// Register /plan command
|
||||
pi.registerCommand("plan", {
|
||||
description: "Toggle plan mode (read-only exploration)",
|
||||
handler: async (_args, ctx) => {
|
||||
planModeEnabled = !planModeEnabled;
|
||||
|
||||
if (planModeEnabled) {
|
||||
// Switch to read-only tools
|
||||
pi.setTools(PLAN_MODE_TOOLS);
|
||||
ctx.ui.notify(`Plan mode enabled. Tools: ${PLAN_MODE_TOOLS.join(", ")}`);
|
||||
} else {
|
||||
// Switch back to normal tools
|
||||
pi.setTools(NORMAL_MODE_TOOLS);
|
||||
ctx.ui.notify("Plan mode disabled. Full access restored.");
|
||||
}
|
||||
togglePlanMode(ctx);
|
||||
},
|
||||
});
|
||||
|
||||
// Register Shift+P shortcut
|
||||
pi.registerShortcut("shift+p", {
|
||||
description: "Toggle plan mode",
|
||||
handler: async (ctx) => {
|
||||
togglePlanMode(ctx);
|
||||
},
|
||||
});
|
||||
|
||||
// Block destructive bash commands in plan mode
|
||||
pi.on("tool_call", async (event) => {
|
||||
if (!planModeEnabled) return;
|
||||
if (event.toolName !== "bash") return;
|
||||
|
||||
const command = event.input.command as string;
|
||||
if (!isSafeCommand(command)) {
|
||||
return {
|
||||
block: true,
|
||||
reason: `Plan mode: destructive command blocked. Use /plan to disable plan mode first.\nCommand: ${command}`,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Inject plan mode context at the start of each turn via before_agent_start
|
||||
pi.on("before_agent_start", async () => {
|
||||
if (!planModeEnabled) return;
|
||||
|
|
@ -57,8 +221,10 @@ export default function planModeHook(pi: HookAPI) {
|
|||
You are in plan mode - a read-only exploration mode for safe code analysis.
|
||||
|
||||
Restrictions:
|
||||
- You can only use: read, bash (read-only commands), grep, find, ls
|
||||
- You can only use: read, bash, grep, find, ls
|
||||
- You CANNOT use: edit, write (file modifications are disabled)
|
||||
- Bash is restricted to READ-ONLY commands (cat, ls, grep, git status, etc.)
|
||||
- Destructive bash commands are BLOCKED (rm, mv, cp, git commit, npm install, etc.)
|
||||
- Focus on analysis, planning, and understanding the codebase
|
||||
|
||||
Your task is to explore, analyze, and create a detailed plan.
|
||||
|
|
@ -84,10 +250,17 @@ When you have a complete plan, I will switch to normal mode to execute it.`,
|
|||
// Switch to normal mode
|
||||
planModeEnabled = false;
|
||||
pi.setTools(NORMAL_MODE_TOOLS);
|
||||
ctx.ui.notify("Switched to normal mode. Full access restored.");
|
||||
updateStatus(ctx);
|
||||
|
||||
// Set editor text to prompt execution
|
||||
ctx.ui.setEditorText("Execute the plan you just created. Proceed step by step.");
|
||||
// Send message to trigger execution immediately
|
||||
pi.sendMessage(
|
||||
{
|
||||
customType: "plan-mode-execute",
|
||||
content: "Execute the plan you just created. Proceed step by step.",
|
||||
display: true,
|
||||
},
|
||||
{ triggerTurn: true },
|
||||
);
|
||||
} else if (choice === "Refine the plan") {
|
||||
const refinement = await ctx.ui.input("What should be refined?");
|
||||
if (refinement) {
|
||||
|
|
@ -97,17 +270,28 @@ When you have a complete plan, I will switch to normal mode to execute it.`,
|
|||
// "Stay in plan mode" - do nothing, just continue
|
||||
});
|
||||
|
||||
// Persist plan mode state across sessions
|
||||
// Initialize plan mode state on session start
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
// Check if there's persisted plan mode state
|
||||
// Check --plan flag first
|
||||
if (pi.getFlag("plan") === true) {
|
||||
planModeEnabled = true;
|
||||
}
|
||||
|
||||
// Check if there's persisted plan mode state (from previous session)
|
||||
const entries = ctx.sessionManager.getEntries();
|
||||
const planModeEntry = entries
|
||||
.filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === "plan-mode")
|
||||
.pop() as { data?: { enabled: boolean } } | undefined;
|
||||
|
||||
if (planModeEntry?.data?.enabled) {
|
||||
planModeEnabled = true;
|
||||
// Restore from session (overrides flag if session has state)
|
||||
if (planModeEntry?.data?.enabled !== undefined) {
|
||||
planModeEnabled = planModeEntry.data.enabled;
|
||||
}
|
||||
|
||||
// Apply initial state if plan mode is enabled
|
||||
if (planModeEnabled) {
|
||||
pi.setTools(PLAN_MODE_TOOLS);
|
||||
updateStatus(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ export interface Args {
|
|||
listModels?: string | true;
|
||||
messages: string[];
|
||||
fileArgs: string[];
|
||||
/** Unknown flags (potentially hook flags) - map of flag name to value */
|
||||
unknownFlags: Map<string, boolean | string>;
|
||||
}
|
||||
|
||||
const VALID_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
||||
|
|
@ -43,10 +45,11 @@ export function isValidThinkingLevel(level: string): level is ThinkingLevel {
|
|||
return VALID_THINKING_LEVELS.includes(level as ThinkingLevel);
|
||||
}
|
||||
|
||||
export function parseArgs(args: string[]): Args {
|
||||
export function parseArgs(args: string[], hookFlags?: Map<string, { type: "boolean" | "string" }>): Args {
|
||||
const result: Args = {
|
||||
messages: [],
|
||||
fileArgs: [],
|
||||
unknownFlags: new Map(),
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
|
|
@ -131,6 +134,18 @@ export function parseArgs(args: string[]): Args {
|
|||
}
|
||||
} else if (arg.startsWith("@")) {
|
||||
result.fileArgs.push(arg.slice(1)); // Remove @ prefix
|
||||
} else if (arg.startsWith("--") && hookFlags) {
|
||||
// Check if it's a hook-registered flag
|
||||
const flagName = arg.slice(2);
|
||||
const hookFlag = hookFlags.get(flagName);
|
||||
if (hookFlag) {
|
||||
if (hookFlag.type === "boolean") {
|
||||
result.unknownFlags.set(flagName, true);
|
||||
} else if (hookFlag.type === "string" && i + 1 < args.length) {
|
||||
result.unknownFlags.set(flagName, args[++i]);
|
||||
}
|
||||
}
|
||||
// Unknown flags without hookFlags are silently ignored (first pass)
|
||||
} else if (!arg.startsWith("-")) {
|
||||
result.messages.push(arg);
|
||||
}
|
||||
|
|
@ -172,6 +187,8 @@ ${chalk.bold("Options:")}
|
|||
--help, -h Show this help
|
||||
--version, -v Show version number
|
||||
|
||||
Hooks can register additional flags (e.g., --plan from plan-mode hook).
|
||||
|
||||
${chalk.bold("Examples:")}
|
||||
# Interactive mode
|
||||
${APP_NAME}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ export {
|
|||
type AppendEntryHandler,
|
||||
type BranchHandler,
|
||||
type GetToolsHandler,
|
||||
type HookFlag,
|
||||
type HookShortcut,
|
||||
type LoadedHook,
|
||||
type LoadHooksResult,
|
||||
type NavigateTreeHandler,
|
||||
|
|
|
|||
|
|
@ -71,6 +71,36 @@ export type GetToolsHandler = () => string[];
|
|||
*/
|
||||
export type SetToolsHandler = (toolNames: string[]) => void;
|
||||
|
||||
/**
|
||||
* CLI flag definition registered by a hook.
|
||||
*/
|
||||
export interface HookFlag {
|
||||
/** Flag name (without --) */
|
||||
name: string;
|
||||
/** Description for --help */
|
||||
description?: string;
|
||||
/** Type: boolean or string */
|
||||
type: "boolean" | "string";
|
||||
/** Default value */
|
||||
default?: boolean | string;
|
||||
/** Hook path that registered this flag */
|
||||
hookPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyboard shortcut registered by a hook.
|
||||
*/
|
||||
export interface HookShortcut {
|
||||
/** Shortcut string (e.g., "shift+p", "ctrl+shift+x") */
|
||||
shortcut: string;
|
||||
/** Description for help */
|
||||
description?: string;
|
||||
/** Handler function */
|
||||
handler: (ctx: import("./types.js").HookContext) => Promise<void> | void;
|
||||
/** Hook path that registered this shortcut */
|
||||
hookPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* New session handler type for ctx.newSession() in HookCommandContext.
|
||||
*/
|
||||
|
|
@ -106,6 +136,12 @@ export interface LoadedHook {
|
|||
messageRenderers: Map<string, HookMessageRenderer>;
|
||||
/** Map of command name to registered command */
|
||||
commands: Map<string, RegisteredCommand>;
|
||||
/** CLI flags registered by this hook */
|
||||
flags: Map<string, HookFlag>;
|
||||
/** Flag values (set after CLI parsing) */
|
||||
flagValues: Map<string, boolean | string>;
|
||||
/** Keyboard shortcuts registered by this hook */
|
||||
shortcuts: Map<string, HookShortcut>;
|
||||
/** Set the send message handler for this hook's pi.sendMessage() */
|
||||
setSendMessageHandler: (handler: SendMessageHandler) => void;
|
||||
/** Set the append entry handler for this hook's pi.appendEntry() */
|
||||
|
|
@ -114,6 +150,8 @@ export interface LoadedHook {
|
|||
setGetToolsHandler: (handler: GetToolsHandler) => void;
|
||||
/** Set the set tools handler for this hook's pi.setTools() */
|
||||
setSetToolsHandler: (handler: SetToolsHandler) => void;
|
||||
/** Set a flag value (called after CLI parsing) */
|
||||
setFlagValue: (name: string, value: boolean | string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -167,14 +205,19 @@ function resolveHookPath(hookPath: string, cwd: string): string {
|
|||
function createHookAPI(
|
||||
handlers: Map<string, HandlerFn[]>,
|
||||
cwd: string,
|
||||
hookPath: string,
|
||||
): {
|
||||
api: HookAPI;
|
||||
messageRenderers: Map<string, HookMessageRenderer>;
|
||||
commands: Map<string, RegisteredCommand>;
|
||||
flags: Map<string, HookFlag>;
|
||||
flagValues: Map<string, boolean | string>;
|
||||
shortcuts: Map<string, HookShortcut>;
|
||||
setSendMessageHandler: (handler: SendMessageHandler) => void;
|
||||
setAppendEntryHandler: (handler: AppendEntryHandler) => void;
|
||||
setGetToolsHandler: (handler: GetToolsHandler) => void;
|
||||
setSetToolsHandler: (handler: SetToolsHandler) => void;
|
||||
setFlagValue: (name: string, value: boolean | string) => void;
|
||||
} {
|
||||
let sendMessageHandler: SendMessageHandler = () => {
|
||||
// Default no-op until mode sets the handler
|
||||
|
|
@ -188,6 +231,9 @@ function createHookAPI(
|
|||
};
|
||||
const messageRenderers = new Map<string, HookMessageRenderer>();
|
||||
const commands = new Map<string, RegisteredCommand>();
|
||||
const flags = new Map<string, HookFlag>();
|
||||
const flagValues = new Map<string, boolean | string>();
|
||||
const shortcuts = new Map<string, HookShortcut>();
|
||||
|
||||
// Cast to HookAPI - the implementation is more general (string event names)
|
||||
// but the interface has specific overloads for type safety in hooks
|
||||
|
|
@ -221,12 +267,37 @@ function createHookAPI(
|
|||
setTools(toolNames: string[]): void {
|
||||
setToolsHandler(toolNames);
|
||||
},
|
||||
registerFlag(
|
||||
name: string,
|
||||
options: { description?: string; type: "boolean" | "string"; default?: boolean | string },
|
||||
): void {
|
||||
flags.set(name, { name, hookPath, ...options });
|
||||
// Set default value if provided
|
||||
if (options.default !== undefined) {
|
||||
flagValues.set(name, options.default);
|
||||
}
|
||||
},
|
||||
getFlag(name: string): boolean | string | undefined {
|
||||
return flagValues.get(name);
|
||||
},
|
||||
registerShortcut(
|
||||
shortcut: string,
|
||||
options: {
|
||||
description?: string;
|
||||
handler: (ctx: import("./types.js").HookContext) => Promise<void> | void;
|
||||
},
|
||||
): void {
|
||||
shortcuts.set(shortcut, { shortcut, hookPath, ...options });
|
||||
},
|
||||
} as HookAPI;
|
||||
|
||||
return {
|
||||
api,
|
||||
messageRenderers,
|
||||
commands,
|
||||
flags,
|
||||
flagValues,
|
||||
shortcuts,
|
||||
setSendMessageHandler: (handler: SendMessageHandler) => {
|
||||
sendMessageHandler = handler;
|
||||
},
|
||||
|
|
@ -239,6 +310,9 @@ function createHookAPI(
|
|||
setSetToolsHandler: (handler: SetToolsHandler) => {
|
||||
setToolsHandler = handler;
|
||||
},
|
||||
setFlagValue: (name: string, value: boolean | string) => {
|
||||
flagValues.set(name, value);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -270,11 +344,15 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo
|
|||
api,
|
||||
messageRenderers,
|
||||
commands,
|
||||
flags,
|
||||
flagValues,
|
||||
shortcuts,
|
||||
setSendMessageHandler,
|
||||
setAppendEntryHandler,
|
||||
setGetToolsHandler,
|
||||
setSetToolsHandler,
|
||||
} = createHookAPI(handlers, cwd);
|
||||
setFlagValue,
|
||||
} = createHookAPI(handlers, cwd, hookPath);
|
||||
|
||||
// Call factory to register handlers
|
||||
factory(api);
|
||||
|
|
@ -286,10 +364,14 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo
|
|||
handlers,
|
||||
messageRenderers,
|
||||
commands,
|
||||
flags,
|
||||
flagValues,
|
||||
shortcuts,
|
||||
setSendMessageHandler,
|
||||
setAppendEntryHandler,
|
||||
setGetToolsHandler,
|
||||
setSetToolsHandler,
|
||||
setFlagValue,
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -168,6 +168,43 @@ export class HookRunner {
|
|||
return this.hooks.map((h) => h.path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all CLI flags registered by hooks.
|
||||
*/
|
||||
getFlags(): Map<string, import("./loader.js").HookFlag> {
|
||||
const allFlags = new Map<string, import("./loader.js").HookFlag>();
|
||||
for (const hook of this.hooks) {
|
||||
for (const [name, flag] of hook.flags) {
|
||||
allFlags.set(name, flag);
|
||||
}
|
||||
}
|
||||
return allFlags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a flag value (after CLI parsing).
|
||||
*/
|
||||
setFlagValue(name: string, value: boolean | string): void {
|
||||
for (const hook of this.hooks) {
|
||||
if (hook.flags.has(name)) {
|
||||
hook.setFlagValue(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all keyboard shortcuts registered by hooks.
|
||||
*/
|
||||
getShortcuts(): Map<string, import("./loader.js").HookShortcut> {
|
||||
const allShortcuts = new Map<string, import("./loader.js").HookShortcut>();
|
||||
for (const hook of this.hooks) {
|
||||
for (const [key, shortcut] of hook.shortcuts) {
|
||||
allShortcuts.set(key, shortcut);
|
||||
}
|
||||
}
|
||||
return allShortcuts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to hook errors.
|
||||
* @returns Unsubscribe function
|
||||
|
|
|
|||
|
|
@ -771,6 +771,70 @@ export interface HookAPI {
|
|||
* pi.setTools(["read", "bash", "edit", "write"]);
|
||||
*/
|
||||
setTools(toolNames: string[]): void;
|
||||
|
||||
/**
|
||||
* Register a CLI flag for this hook.
|
||||
* Flags are parsed from command line and values accessible via getFlag().
|
||||
*
|
||||
* @param name - Flag name (will be --name on CLI)
|
||||
* @param options - Flag configuration
|
||||
*
|
||||
* @example
|
||||
* pi.registerFlag("plan", {
|
||||
* description: "Start in plan mode (read-only)",
|
||||
* type: "boolean",
|
||||
* });
|
||||
*/
|
||||
registerFlag(
|
||||
name: string,
|
||||
options: {
|
||||
/** Description shown in --help */
|
||||
description?: string;
|
||||
/** Flag type: boolean (--flag) or string (--flag value) */
|
||||
type: "boolean" | "string";
|
||||
/** Default value */
|
||||
default?: boolean | string;
|
||||
},
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Get the value of a CLI flag registered by this hook.
|
||||
* Returns undefined if flag was not provided and has no default.
|
||||
*
|
||||
* @param name - Flag name (without --)
|
||||
* @returns Flag value, or undefined
|
||||
*
|
||||
* @example
|
||||
* if (pi.getFlag("plan")) {
|
||||
* // plan mode enabled
|
||||
* }
|
||||
*/
|
||||
getFlag(name: string): boolean | string | undefined;
|
||||
|
||||
/**
|
||||
* Register a keyboard shortcut for this hook.
|
||||
* The handler is called when the shortcut is pressed in interactive mode.
|
||||
*
|
||||
* @param shortcut - Shortcut definition (e.g., "shift+p", "ctrl+shift+x")
|
||||
* @param options - Shortcut configuration
|
||||
*
|
||||
* @example
|
||||
* pi.registerShortcut("shift+p", {
|
||||
* description: "Toggle plan mode",
|
||||
* handler: async (ctx) => {
|
||||
* // toggle plan mode
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
registerShortcut(
|
||||
shortcut: string,
|
||||
options: {
|
||||
/** Description shown in help */
|
||||
description?: string;
|
||||
/** Handler called when shortcut is pressed */
|
||||
handler: (ctx: HookContext) => Promise<void> | void;
|
||||
},
|
||||
): void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -112,6 +112,8 @@ export interface CreateAgentSessionOptions {
|
|||
hooks?: Array<{ path?: string; factory: HookFactory }>;
|
||||
/** Additional hook paths to load (merged with discovery). */
|
||||
additionalHookPaths?: string[];
|
||||
/** Pre-loaded hooks (skips loading, used when hooks were loaded early for CLI flags). */
|
||||
preloadedHooks?: LoadedHook[];
|
||||
|
||||
/** Skills. Default: discovered from multiple locations */
|
||||
skills?: Skill[];
|
||||
|
|
@ -341,9 +343,13 @@ function createFactoryFromLoadedHook(loaded: LoadedHook): HookFactory {
|
|||
*/
|
||||
function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; factory: HookFactory }>): LoadedHook[] {
|
||||
return definitions.map((def) => {
|
||||
const hookPath = def.path ?? "<inline>";
|
||||
const handlers = new Map<string, Array<(...args: unknown[]) => Promise<unknown>>>();
|
||||
const messageRenderers = new Map<string, any>();
|
||||
const commands = new Map<string, any>();
|
||||
const flags = new Map<string, any>();
|
||||
const flagValues = new Map<string, boolean | string>();
|
||||
const shortcuts = new Map<string, any>();
|
||||
let sendMessageHandler: (
|
||||
message: any,
|
||||
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" },
|
||||
|
|
@ -375,6 +381,16 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa
|
|||
registerCommand: (name: string, options: any) => {
|
||||
commands.set(name, { name, ...options });
|
||||
},
|
||||
registerFlag: (name: string, options: any) => {
|
||||
flags.set(name, { name, hookPath, ...options });
|
||||
if (options.default !== undefined) {
|
||||
flagValues.set(name, options.default);
|
||||
}
|
||||
},
|
||||
getFlag: (name: string) => flagValues.get(name),
|
||||
registerShortcut: (shortcut: string, options: any) => {
|
||||
shortcuts.set(shortcut, { shortcut, hookPath, ...options });
|
||||
},
|
||||
newSession: (options?: any) => newSessionHandler(options),
|
||||
branch: (entryId: string) => branchHandler(entryId),
|
||||
navigateTree: (targetId: string, options?: any) => navigateTreeHandler(targetId, options),
|
||||
|
|
@ -385,11 +401,14 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa
|
|||
def.factory(api as any);
|
||||
|
||||
return {
|
||||
path: def.path ?? "<inline>",
|
||||
resolvedPath: def.path ?? "<inline>",
|
||||
path: hookPath,
|
||||
resolvedPath: hookPath,
|
||||
handlers,
|
||||
messageRenderers,
|
||||
commands,
|
||||
flags,
|
||||
flagValues,
|
||||
shortcuts,
|
||||
setSendMessageHandler: (
|
||||
handler: (message: any, options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" }) => void,
|
||||
) => {
|
||||
|
|
@ -413,6 +432,9 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa
|
|||
setSetToolsHandler: (handler: (toolNames: string[]) => void) => {
|
||||
setToolsHandler = handler;
|
||||
},
|
||||
setFlagValue: (name: string, value: boolean | string) => {
|
||||
flagValues.set(name, value);
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
@ -566,7 +588,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|||
}
|
||||
|
||||
let hookRunner: HookRunner | undefined;
|
||||
if (options.hooks !== undefined) {
|
||||
if (options.preloadedHooks !== undefined && options.preloadedHooks.length > 0) {
|
||||
// Use pre-loaded hooks (from early CLI flag discovery)
|
||||
hookRunner = new HookRunner(options.preloadedHooks, cwd, sessionManager, modelRegistry);
|
||||
} else if (options.hooks !== undefined) {
|
||||
if (options.hooks.length > 0) {
|
||||
const loadedHooks = createLoadedHooksFromDefinitions(options.hooks);
|
||||
hookRunner = new HookRunner(loadedHooks, cwd, sessionManager, modelRegistry);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import type { AgentSession } from "./core/agent-session.js";
|
|||
|
||||
import type { LoadedCustomTool } from "./core/custom-tools/index.js";
|
||||
import { exportFromFile } from "./core/export-html/index.js";
|
||||
import { discoverAndLoadHooks } from "./core/hooks/index.js";
|
||||
import type { HookUIContext } from "./core/index.js";
|
||||
import type { ModelRegistry } from "./core/model-registry.js";
|
||||
import { resolveModelScope, type ScopedModel } from "./core/model-resolver.js";
|
||||
|
|
@ -212,6 +213,7 @@ function buildSessionOptions(
|
|||
scopedModels: ScopedModel[],
|
||||
sessionManager: SessionManager | undefined,
|
||||
modelRegistry: ModelRegistry,
|
||||
preloadedHooks?: import("./core/hooks/index.js").LoadedHook[],
|
||||
): CreateAgentSessionOptions {
|
||||
const options: CreateAgentSessionOptions = {};
|
||||
|
||||
|
|
@ -270,9 +272,9 @@ function buildSessionOptions(
|
|||
options.skills = [];
|
||||
}
|
||||
|
||||
// Additional hook paths from CLI
|
||||
if (parsed.hooks && parsed.hooks.length > 0) {
|
||||
options.additionalHookPaths = parsed.hooks;
|
||||
// Pre-loaded hooks (from early CLI flag discovery)
|
||||
if (preloadedHooks && preloadedHooks.length > 0) {
|
||||
options.preloadedHooks = preloadedHooks;
|
||||
}
|
||||
|
||||
// Additional custom tool paths from CLI
|
||||
|
|
@ -294,9 +296,38 @@ export async function main(args: string[]) {
|
|||
const modelRegistry = discoverModels(authStorage);
|
||||
time("discoverModels");
|
||||
|
||||
const parsed = parseArgs(args);
|
||||
// First pass: parse args to get --hook paths
|
||||
const firstPass = parseArgs(args);
|
||||
time("parseArgs-firstPass");
|
||||
|
||||
// Early load hooks to discover their CLI flags
|
||||
const cwd = process.cwd();
|
||||
const agentDir = getAgentDir();
|
||||
const hookPaths = firstPass.hooks ?? [];
|
||||
const { hooks: loadedHooks } = await discoverAndLoadHooks(hookPaths, cwd, agentDir);
|
||||
time("discoverHookFlags");
|
||||
|
||||
// Collect all hook flags
|
||||
const hookFlags = new Map<string, { type: "boolean" | "string" }>();
|
||||
for (const hook of loadedHooks) {
|
||||
for (const [name, flag] of hook.flags) {
|
||||
hookFlags.set(name, { type: flag.type });
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: parse args with hook flags
|
||||
const parsed = parseArgs(args, hookFlags);
|
||||
time("parseArgs");
|
||||
|
||||
// Pass flag values to hooks
|
||||
for (const [name, value] of parsed.unknownFlags) {
|
||||
for (const hook of loadedHooks) {
|
||||
if (hook.flags.has(name)) {
|
||||
hook.setFlagValue(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed.version) {
|
||||
console.log(VERSION);
|
||||
return;
|
||||
|
|
@ -331,7 +362,6 @@ export async function main(args: string[]) {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
const cwd = process.cwd();
|
||||
const settingsManager = SettingsManager.create(cwd);
|
||||
time("SettingsManager.create");
|
||||
const { initialMessage, initialImages } = await prepareInitialMessage(parsed, settingsManager.getImageAutoResize());
|
||||
|
|
@ -369,7 +399,7 @@ export async function main(args: string[]) {
|
|||
sessionManager = SessionManager.open(selectedPath);
|
||||
}
|
||||
|
||||
const sessionOptions = buildSessionOptions(parsed, scopedModels, sessionManager, modelRegistry);
|
||||
const sessionOptions = buildSessionOptions(parsed, scopedModels, sessionManager, modelRegistry, loadedHooks);
|
||||
sessionOptions.authStorage = authStorage;
|
||||
sessionOptions.modelRegistry = modelRegistry;
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ export class CustomEditor extends Editor {
|
|||
public onEscape?: () => void;
|
||||
public onCtrlD?: () => void;
|
||||
public onPasteImage?: () => void;
|
||||
/** Handler for hook-registered shortcuts. Returns true if handled. */
|
||||
public onHookShortcut?: (data: string) => boolean;
|
||||
|
||||
constructor(theme: EditorTheme, keybindings: KeybindingsManager) {
|
||||
super(theme);
|
||||
|
|
@ -26,6 +28,11 @@ export class CustomEditor extends Editor {
|
|||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
// Check hook-registered shortcuts first
|
||||
if (this.onHookShortcut?.(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for Ctrl+V to handle clipboard image paste
|
||||
if (matchesKey(data, "ctrl+v")) {
|
||||
this.onPasteImage?.();
|
||||
|
|
|
|||
|
|
@ -406,20 +406,7 @@ export class InteractiveMode {
|
|||
}
|
||||
|
||||
// Create and set hook & tool UI context
|
||||
const uiContext: HookUIContext = {
|
||||
select: (title, options) => this.showHookSelector(title, options),
|
||||
confirm: (title, message) => this.showHookConfirm(title, message),
|
||||
input: (title, placeholder) => this.showHookInput(title, placeholder),
|
||||
notify: (message, type) => this.showHookNotify(message, type),
|
||||
setStatus: (key, text) => this.setHookStatus(key, text),
|
||||
custom: (factory) => this.showHookCustom(factory),
|
||||
setEditorText: (text) => this.editor.setText(text),
|
||||
getEditorText: () => this.editor.getText(),
|
||||
editor: (title, prefill) => this.showHookEditor(title, prefill),
|
||||
get theme() {
|
||||
return theme;
|
||||
},
|
||||
};
|
||||
const uiContext = this.createHookUIContext();
|
||||
this.setToolUIContext(uiContext, true);
|
||||
|
||||
// Notify custom tools of session start
|
||||
|
|
@ -532,6 +519,9 @@ export class InteractiveMode {
|
|||
this.showHookError(error.hookPath, error.error);
|
||||
});
|
||||
|
||||
// Set up hook-registered shortcuts
|
||||
this.setupHookShortcuts(hookRunner);
|
||||
|
||||
// Show loaded hooks
|
||||
const hookPaths = hookRunner.getHookPaths();
|
||||
if (hookPaths.length > 0) {
|
||||
|
|
@ -579,6 +569,82 @@ export class InteractiveMode {
|
|||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up keyboard shortcuts registered by hooks.
|
||||
*/
|
||||
private setupHookShortcuts(hookRunner: import("../../core/hooks/index.js").HookRunner): void {
|
||||
const shortcuts = hookRunner.getShortcuts();
|
||||
if (shortcuts.size === 0) return;
|
||||
|
||||
// Create a context for shortcut handlers
|
||||
const createContext = (): import("../../core/hooks/types.js").HookContext => ({
|
||||
ui: this.createHookUIContext(),
|
||||
hasUI: true,
|
||||
cwd: process.cwd(),
|
||||
sessionManager: this.sessionManager,
|
||||
modelRegistry: this.session.modelRegistry,
|
||||
model: this.session.model,
|
||||
isIdle: () => !this.session.isStreaming,
|
||||
abort: () => this.session.abort(),
|
||||
hasPendingMessages: () => this.session.pendingMessageCount > 0,
|
||||
});
|
||||
|
||||
// Set up the hook shortcut handler on the editor
|
||||
this.editor.onHookShortcut = (data: string) => {
|
||||
for (const [shortcutStr, shortcut] of shortcuts) {
|
||||
if (this.matchShortcut(data, shortcutStr)) {
|
||||
// Run handler async, don't block input
|
||||
Promise.resolve(shortcut.handler(createContext())).catch((err) => {
|
||||
this.showError(`Shortcut handler error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Match a key input against a shortcut string like "shift+p" or "ctrl+shift+x".
|
||||
*/
|
||||
private matchShortcut(data: string, shortcut: string): boolean {
|
||||
const parts = shortcut.toLowerCase().split("+");
|
||||
const key = parts.pop() ?? "";
|
||||
const modifiers = new Set(parts);
|
||||
|
||||
const hasShift = modifiers.has("shift");
|
||||
const hasCtrl = modifiers.has("ctrl");
|
||||
const hasAlt = modifiers.has("alt");
|
||||
|
||||
// Get the key codepoint
|
||||
const keyCode = key.length === 1 ? key.charCodeAt(0) : 0;
|
||||
if (keyCode === 0) return false;
|
||||
|
||||
// Calculate expected modifier bits for Kitty protocol
|
||||
// Kitty modifier bits: 1=shift, 2=alt, 4=ctrl
|
||||
let expectedMod = 0;
|
||||
if (hasShift) expectedMod |= 1;
|
||||
if (hasAlt) expectedMod |= 2;
|
||||
if (hasCtrl) expectedMod |= 4;
|
||||
|
||||
// Try to match Kitty protocol: \x1b[<code>;<mod>u
|
||||
// With modifier offset: mod in sequence = expectedMod + 1
|
||||
const kittyPattern = new RegExp(`^\x1b\\[${keyCode};(\\d+)u$`);
|
||||
const kittyMatch = data.match(kittyPattern);
|
||||
if (kittyMatch) {
|
||||
const actualMod = parseInt(kittyMatch[1], 10) - 1; // Subtract 1 for the offset
|
||||
// Mask out lock bits (8=capslock, 16=numlock)
|
||||
return (actualMod & 0x7) === expectedMod;
|
||||
}
|
||||
|
||||
// Try uppercase letter for shift+letter (legacy terminals)
|
||||
if (hasShift && !hasCtrl && !hasAlt && key.length === 1) {
|
||||
return data === key.toUpperCase();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set hook status text in the footer.
|
||||
*/
|
||||
|
|
@ -587,6 +653,26 @@ export class InteractiveMode {
|
|||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the HookUIContext for hooks and tools.
|
||||
*/
|
||||
private createHookUIContext(): HookUIContext {
|
||||
return {
|
||||
select: (title, options) => this.showHookSelector(title, options),
|
||||
confirm: (title, message) => this.showHookConfirm(title, message),
|
||||
input: (title, placeholder) => this.showHookInput(title, placeholder),
|
||||
notify: (message, type) => this.showHookNotify(message, type),
|
||||
setStatus: (key, text) => this.setHookStatus(key, text),
|
||||
custom: (factory) => this.showHookCustom(factory),
|
||||
setEditorText: (text) => this.editor.setText(text),
|
||||
getEditorText: () => this.editor.getText(),
|
||||
editor: (title, prefill) => this.showHookEditor(title, prefill),
|
||||
get theme() {
|
||||
return theme;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a selector for hooks.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -78,10 +78,14 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
|||
handlers,
|
||||
messageRenderers: new Map(),
|
||||
commands: new Map(),
|
||||
flags: new Map(),
|
||||
flagValues: new Map(),
|
||||
shortcuts: new Map(),
|
||||
setSendMessageHandler: () => {},
|
||||
setAppendEntryHandler: () => {},
|
||||
setGetToolsHandler: () => {},
|
||||
setSetToolsHandler: () => {},
|
||||
setFlagValue: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -269,10 +273,14 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
|||
]),
|
||||
messageRenderers: new Map(),
|
||||
commands: new Map(),
|
||||
flags: new Map(),
|
||||
flagValues: new Map(),
|
||||
shortcuts: new Map(),
|
||||
setSendMessageHandler: () => {},
|
||||
setAppendEntryHandler: () => {},
|
||||
setGetToolsHandler: () => {},
|
||||
setSetToolsHandler: () => {},
|
||||
setFlagValue: () => {},
|
||||
};
|
||||
|
||||
createSession([throwingHook]);
|
||||
|
|
@ -318,10 +326,14 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
|||
]),
|
||||
messageRenderers: new Map(),
|
||||
commands: new Map(),
|
||||
flags: new Map(),
|
||||
flagValues: new Map(),
|
||||
shortcuts: new Map(),
|
||||
setSendMessageHandler: () => {},
|
||||
setAppendEntryHandler: () => {},
|
||||
setGetToolsHandler: () => {},
|
||||
setSetToolsHandler: () => {},
|
||||
setFlagValue: () => {},
|
||||
};
|
||||
|
||||
const hook2: LoadedHook = {
|
||||
|
|
@ -349,10 +361,14 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
|||
]),
|
||||
messageRenderers: new Map(),
|
||||
commands: new Map(),
|
||||
flags: new Map(),
|
||||
flagValues: new Map(),
|
||||
shortcuts: new Map(),
|
||||
setSendMessageHandler: () => {},
|
||||
setAppendEntryHandler: () => {},
|
||||
setGetToolsHandler: () => {},
|
||||
setSetToolsHandler: () => {},
|
||||
setFlagValue: () => {},
|
||||
};
|
||||
|
||||
createSession([hook1, hook2]);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue