@mariozechner/pi-coding-agent 0.32.3 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/README.md +76 -3
  3. package/dist/cli/args.d.ts +5 -1
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +18 -1
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/core/agent-session.d.ts +24 -1
  8. package/dist/core/agent-session.d.ts.map +1 -1
  9. package/dist/core/agent-session.js +65 -9
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/bash-executor.d.ts.map +1 -1
  12. package/dist/core/bash-executor.js +2 -1
  13. package/dist/core/bash-executor.js.map +1 -1
  14. package/dist/core/custom-tools/loader.d.ts.map +1 -1
  15. package/dist/core/custom-tools/loader.js +1 -0
  16. package/dist/core/custom-tools/loader.js.map +1 -1
  17. package/dist/core/export-html/template.css +34 -4
  18. package/dist/core/export-html/template.js +17 -4
  19. package/dist/core/hooks/index.d.ts +1 -1
  20. package/dist/core/hooks/index.d.ts.map +1 -1
  21. package/dist/core/hooks/index.js.map +1 -1
  22. package/dist/core/hooks/loader.d.ts +56 -1
  23. package/dist/core/hooks/loader.d.ts.map +1 -1
  24. package/dist/core/hooks/loader.js +54 -2
  25. package/dist/core/hooks/loader.js.map +1 -1
  26. package/dist/core/hooks/runner.d.ts +33 -5
  27. package/dist/core/hooks/runner.d.ts.map +1 -1
  28. package/dist/core/hooks/runner.js +100 -9
  29. package/dist/core/hooks/runner.js.map +1 -1
  30. package/dist/core/hooks/types.d.ts +135 -3
  31. package/dist/core/hooks/types.d.ts.map +1 -1
  32. package/dist/core/hooks/types.js.map +1 -1
  33. package/dist/core/keybindings.d.ts +59 -0
  34. package/dist/core/keybindings.d.ts.map +1 -0
  35. package/dist/core/keybindings.js +149 -0
  36. package/dist/core/keybindings.js.map +1 -0
  37. package/dist/core/sdk.d.ts +3 -0
  38. package/dist/core/sdk.d.ts.map +1 -1
  39. package/dist/core/sdk.js +102 -27
  40. package/dist/core/sdk.js.map +1 -1
  41. package/dist/index.d.ts +1 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +1 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/main.d.ts.map +1 -1
  46. package/dist/main.js +32 -7
  47. package/dist/main.js.map +1 -1
  48. package/dist/modes/interactive/components/custom-editor.d.ts +13 -12
  49. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  50. package/dist/modes/interactive/components/custom-editor.js +50 -68
  51. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  52. package/dist/modes/interactive/components/hook-editor.d.ts.map +1 -1
  53. package/dist/modes/interactive/components/hook-editor.js +5 -4
  54. package/dist/modes/interactive/components/hook-editor.js.map +1 -1
  55. package/dist/modes/interactive/components/hook-input.d.ts.map +1 -1
  56. package/dist/modes/interactive/components/hook-input.js +4 -3
  57. package/dist/modes/interactive/components/hook-input.js.map +1 -1
  58. package/dist/modes/interactive/components/hook-selector.d.ts.map +1 -1
  59. package/dist/modes/interactive/components/hook-selector.js +6 -5
  60. package/dist/modes/interactive/components/hook-selector.js.map +1 -1
  61. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  62. package/dist/modes/interactive/components/model-selector.js +6 -5
  63. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  64. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  65. package/dist/modes/interactive/components/oauth-selector.js +6 -5
  66. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  67. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  68. package/dist/modes/interactive/components/session-selector.js +6 -9
  69. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  70. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  71. package/dist/modes/interactive/components/tree-selector.js +14 -15
  72. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  73. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  74. package/dist/modes/interactive/components/user-message-selector.js +6 -11
  75. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  76. package/dist/modes/interactive/interactive-mode.d.ts +34 -1
  77. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  78. package/dist/modes/interactive/interactive-mode.js +300 -64
  79. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  80. package/dist/modes/interactive/theme/theme.d.ts +1 -0
  81. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  82. package/dist/modes/interactive/theme/theme.js +3 -0
  83. package/dist/modes/interactive/theme/theme.js.map +1 -1
  84. package/dist/modes/print-mode.d.ts.map +1 -1
  85. package/dist/modes/print-mode.js +3 -0
  86. package/dist/modes/print-mode.js.map +1 -1
  87. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  88. package/dist/modes/rpc/rpc-mode.js +16 -0
  89. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  90. package/dist/modes/rpc/rpc-types.d.ts +6 -0
  91. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  92. package/dist/modes/rpc/rpc-types.js.map +1 -1
  93. package/docs/hooks.md +114 -4
  94. package/docs/tui.md +18 -15
  95. package/examples/custom-tools/subagent/README.md +2 -2
  96. package/examples/hooks/README.md +3 -0
  97. package/examples/hooks/pirate.ts +44 -0
  98. package/examples/hooks/plan-mode.ts +548 -0
  99. package/examples/hooks/snake.ts +7 -7
  100. package/examples/hooks/todo/index.ts +2 -2
  101. package/examples/hooks/tools.ts +145 -0
  102. package/package.json +5 -4
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/hooks/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA8dH,mEAAmE;AACnE,MAAM,UAAU,gBAAgB,CAAC,CAAkB,EAA4B;IAC9E,OAAO,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC;AAAA,CAC7B;AACD,MAAM,UAAU,gBAAgB,CAAC,CAAkB,EAA4B;IAC9E,OAAO,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC;AAAA,CAC7B;AACD,MAAM,UAAU,gBAAgB,CAAC,CAAkB,EAA4B;IAC9E,OAAO,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC;AAAA,CAC7B;AACD,MAAM,UAAU,iBAAiB,CAAC,CAAkB,EAA6B;IAChF,OAAO,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC;AAAA,CAC9B;AACD,MAAM,UAAU,gBAAgB,CAAC,CAAkB,EAA4B;IAC9E,OAAO,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC;AAAA,CAC7B;AACD,MAAM,UAAU,gBAAgB,CAAC,CAAkB,EAA4B;IAC9E,OAAO,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC;AAAA,CAC7B;AACD,MAAM,UAAU,cAAc,CAAC,CAAkB,EAA0B;IAC1E,OAAO,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC;AAAA,CAC3B","sourcesContent":["/**\n * Hook system types.\n *\n * Hooks are TypeScript modules that can subscribe to agent lifecycle events\n * and interact with the user via UI primitives.\n */\n\nimport type { AgentMessage } from \"@mariozechner/pi-agent-core\";\nimport type { ImageContent, Message, Model, TextContent, ToolResultMessage } from \"@mariozechner/pi-ai\";\nimport type { Component, TUI } from \"@mariozechner/pi-tui\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { CompactionPreparation, CompactionResult } from \"../compaction/index.js\";\nimport type { ExecOptions, ExecResult } from \"../exec.js\";\nimport type { HookMessage } from \"../messages.js\";\nimport type { ModelRegistry } from \"../model-registry.js\";\nimport type {\n\tBranchSummaryEntry,\n\tCompactionEntry,\n\tReadonlySessionManager,\n\tSessionEntry,\n\tSessionManager,\n} from \"../session-manager.js\";\n\nimport type { EditToolDetails } from \"../tools/edit.js\";\nimport type {\n\tBashToolDetails,\n\tFindToolDetails,\n\tGrepToolDetails,\n\tLsToolDetails,\n\tReadToolDetails,\n} from \"../tools/index.js\";\n\n// Re-export for backward compatibility\nexport type { ExecOptions, ExecResult } from \"../exec.js\";\n\n/**\n * UI context for hooks to request interactive UI from the harness.\n * Each mode (interactive, RPC, print) provides its own implementation.\n */\nexport interface HookUIContext {\n\t/**\n\t * Show a selector and return the user's choice.\n\t * @param title - Title to display\n\t * @param options - Array of string options\n\t * @returns Selected option string, or null if cancelled\n\t */\n\tselect(title: string, options: string[]): Promise<string | undefined>;\n\n\t/**\n\t * Show a confirmation dialog.\n\t * @returns true if confirmed, false if cancelled\n\t */\n\tconfirm(title: string, message: string): Promise<boolean>;\n\n\t/**\n\t * Show a text input dialog.\n\t * @returns User input, or undefined if cancelled\n\t */\n\tinput(title: string, placeholder?: string): Promise<string | undefined>;\n\n\t/**\n\t * Show a notification to the user.\n\t */\n\tnotify(message: string, type?: \"info\" | \"warning\" | \"error\"): void;\n\n\t/**\n\t * Set status text in the footer/status bar.\n\t * Pass undefined as text to clear the status for this key.\n\t * Text can include ANSI escape codes for styling.\n\t * Note: Newlines, tabs, and carriage returns are replaced with spaces.\n\t * The combined status line is truncated to terminal width.\n\t * @param key - Unique key to identify this status (e.g., hook name)\n\t * @param text - Status text to display, or undefined to clear\n\t */\n\tsetStatus(key: string, text: string | undefined): void;\n\n\t/**\n\t * Show a custom component with keyboard focus.\n\t * The factory receives TUI, theme, and a done() callback to close the component.\n\t * Can be async for fire-and-forget work (don't await the work, just start it).\n\t *\n\t * @param factory - Function that creates the component. Call done() when finished.\n\t * @returns Promise that resolves with the value passed to done()\n\t *\n\t * @example\n\t * // Sync factory\n\t * const result = await ctx.ui.custom((tui, theme, done) => {\n\t * const component = new MyComponent(tui, theme);\n\t * component.onFinish = (value) => done(value);\n\t * return component;\n\t * });\n\t *\n\t * // Async factory with fire-and-forget work\n\t * const result = await ctx.ui.custom(async (tui, theme, done) => {\n\t * const loader = new CancellableLoader(tui, theme.fg(\"accent\"), theme.fg(\"muted\"), \"Working...\");\n\t * loader.onAbort = () => done(null);\n\t * doWork(loader.signal).then(done); // Don't await - fire and forget\n\t * return loader;\n\t * });\n\t */\n\tcustom<T>(\n\t\tfactory: (\n\t\t\ttui: TUI,\n\t\t\ttheme: Theme,\n\t\t\tdone: (result: T) => void,\n\t\t) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,\n\t): Promise<T>;\n\n\t/**\n\t * Set the text in the core input editor.\n\t * Use this to pre-fill the input box with generated content (e.g., prompt templates, extracted questions).\n\t * @param text - Text to set in the editor\n\t */\n\tsetEditorText(text: string): void;\n\n\t/**\n\t * Get the current text from the core input editor.\n\t * @returns Current editor text\n\t */\n\tgetEditorText(): string;\n\n\t/**\n\t * Show a multi-line editor for text editing.\n\t * Supports Ctrl+G to open external editor ($VISUAL or $EDITOR).\n\t * @param title - Title describing what is being edited\n\t * @param prefill - Optional initial text\n\t * @returns Edited text, or undefined if cancelled (Escape)\n\t */\n\teditor(title: string, prefill?: string): Promise<string | undefined>;\n\n\t/**\n\t * Get the current theme for styling text with ANSI codes.\n\t * Use theme.fg() and theme.bg() to style status text.\n\t *\n\t * @example\n\t * const theme = ctx.ui.theme;\n\t * ctx.ui.setStatus(\"my-hook\", theme.fg(\"success\", \"✓\") + \" Ready\");\n\t */\n\treadonly theme: Theme;\n}\n\n/**\n * Context passed to hook event handlers.\n * For command handlers, see HookCommandContext which extends this with session control methods.\n */\nexport interface HookContext {\n\t/** UI methods for user interaction */\n\tui: HookUIContext;\n\t/** Whether UI is available (false in print mode) */\n\thasUI: boolean;\n\t/** Current working directory */\n\tcwd: string;\n\t/** Session manager (read-only) - use pi.sendMessage()/pi.appendEntry() for writes */\n\tsessionManager: ReadonlySessionManager;\n\t/** Model registry - use for API key resolution and model retrieval */\n\tmodelRegistry: ModelRegistry;\n\t/** Current model (may be undefined if no model is selected yet) */\n\tmodel: Model<any> | undefined;\n\t/** Whether the agent is idle (not streaming) */\n\tisIdle(): boolean;\n\t/** Abort the current agent operation (fire-and-forget, does not wait) */\n\tabort(): void;\n\t/** Whether there are queued messages waiting to be processed */\n\thasPendingMessages(): boolean;\n}\n\n/**\n * Extended context for slash command handlers.\n * Includes session control methods that are only safe in user-initiated commands.\n *\n * These methods are not available in event handlers because they can cause\n * deadlocks when called from within the agent loop (e.g., tool_call, context events).\n */\nexport interface HookCommandContext extends HookContext {\n\t/** Wait for the agent to finish streaming */\n\twaitForIdle(): Promise<void>;\n\n\t/**\n\t * Start a new session, optionally with a setup callback to initialize it.\n\t * The setup callback receives a writable SessionManager for the new session.\n\t *\n\t * @param options.parentSession - Path to parent session for lineage tracking\n\t * @param options.setup - Async callback to initialize the new session (e.g., append messages)\n\t * @returns Object with `cancelled: true` if a hook cancelled the new session\n\t *\n\t * @example\n\t * // Handoff: summarize current session and start fresh with context\n\t * await ctx.newSession({\n\t * parentSession: ctx.sessionManager.getSessionFile(),\n\t * setup: async (sm) => {\n\t * sm.appendMessage({ role: \"user\", content: [{ type: \"text\", text: summary }] });\n\t * }\n\t * });\n\t */\n\tnewSession(options?: {\n\t\tparentSession?: string;\n\t\tsetup?: (sessionManager: SessionManager) => Promise<void>;\n\t}): Promise<{ cancelled: boolean }>;\n\n\t/**\n\t * Branch from a specific entry, creating a new session file.\n\t *\n\t * @param entryId - ID of the entry to branch from\n\t * @returns Object with `cancelled: true` if a hook cancelled the branch\n\t */\n\tbranch(entryId: string): Promise<{ cancelled: boolean }>;\n\n\t/**\n\t * Navigate to a different point in the session tree (in-place).\n\t *\n\t * @param targetId - ID of the entry to navigate to\n\t * @param options.summarize - Whether to summarize the abandoned branch\n\t * @returns Object with `cancelled: true` if a hook cancelled the navigation\n\t */\n\tnavigateTree(targetId: string, options?: { summarize?: boolean }): Promise<{ cancelled: boolean }>;\n}\n\n// ============================================================================\n// Session Events\n// ============================================================================\n\n/** Fired on initial session load */\nexport interface SessionStartEvent {\n\ttype: \"session_start\";\n}\n\n/** Fired before switching to another session (can be cancelled) */\nexport interface SessionBeforeSwitchEvent {\n\ttype: \"session_before_switch\";\n\t/** Reason for the switch */\n\treason: \"new\" | \"resume\";\n\t/** Session file we're switching to (only for \"resume\") */\n\ttargetSessionFile?: string;\n}\n\n/** Fired after switching to another session */\nexport interface SessionSwitchEvent {\n\ttype: \"session_switch\";\n\t/** Reason for the switch */\n\treason: \"new\" | \"resume\";\n\t/** Session file we came from */\n\tpreviousSessionFile: string | undefined;\n}\n\n/** Fired before branching a session (can be cancelled) */\nexport interface SessionBeforeBranchEvent {\n\ttype: \"session_before_branch\";\n\t/** ID of the entry to branch from */\n\tentryId: string;\n}\n\n/** Fired after branching a session */\nexport interface SessionBranchEvent {\n\ttype: \"session_branch\";\n\tpreviousSessionFile: string | undefined;\n}\n\n/** Fired before context compaction (can be cancelled or customized) */\nexport interface SessionBeforeCompactEvent {\n\ttype: \"session_before_compact\";\n\t/** Compaction preparation with messages to summarize, file ops, previous summary, etc. */\n\tpreparation: CompactionPreparation;\n\t/** Branch entries (root to current leaf). Use to inspect custom state or previous compactions. */\n\tbranchEntries: SessionEntry[];\n\t/** Optional user-provided instructions for the summary */\n\tcustomInstructions?: string;\n\t/** Abort signal - hooks should pass this to LLM calls and check it periodically */\n\tsignal: AbortSignal;\n}\n\n/** Fired after context compaction */\nexport interface SessionCompactEvent {\n\ttype: \"session_compact\";\n\tcompactionEntry: CompactionEntry;\n\t/** Whether the compaction entry was provided by a hook */\n\tfromHook: boolean;\n}\n\n/** Fired on process exit (SIGINT/SIGTERM) */\nexport interface SessionShutdownEvent {\n\ttype: \"session_shutdown\";\n}\n\n/** Preparation data for tree navigation (used by session_before_tree event) */\nexport interface TreePreparation {\n\t/** Node being switched to */\n\ttargetId: string;\n\t/** Current active leaf (being abandoned), null if no current position */\n\toldLeafId: string | null;\n\t/** Common ancestor of target and old leaf, null if no common ancestor */\n\tcommonAncestorId: string | null;\n\t/** Entries to summarize (old leaf back to common ancestor or compaction) */\n\tentriesToSummarize: SessionEntry[];\n\t/** Whether user chose to summarize */\n\tuserWantsSummary: boolean;\n}\n\n/** Fired before navigating to a different node in the session tree (can be cancelled) */\nexport interface SessionBeforeTreeEvent {\n\ttype: \"session_before_tree\";\n\t/** Preparation data for the navigation */\n\tpreparation: TreePreparation;\n\t/** Abort signal - honors Escape during summarization (model available via ctx.model) */\n\tsignal: AbortSignal;\n}\n\n/** Fired after navigating to a different node in the session tree */\nexport interface SessionTreeEvent {\n\ttype: \"session_tree\";\n\t/** The new active leaf, null if navigated to before first entry */\n\tnewLeafId: string | null;\n\t/** Previous active leaf, null if there was no position */\n\toldLeafId: string | null;\n\t/** Branch summary entry if one was created */\n\tsummaryEntry?: BranchSummaryEntry;\n\t/** Whether summary came from hook */\n\tfromHook?: boolean;\n}\n\n/** Union of all session event types */\nexport type SessionEvent =\n\t| SessionStartEvent\n\t| SessionBeforeSwitchEvent\n\t| SessionSwitchEvent\n\t| SessionBeforeBranchEvent\n\t| SessionBranchEvent\n\t| SessionBeforeCompactEvent\n\t| SessionCompactEvent\n\t| SessionShutdownEvent\n\t| SessionBeforeTreeEvent\n\t| SessionTreeEvent;\n\n/**\n * Event data for context event.\n * Fired before each LLM call, allowing hooks to modify context non-destructively.\n * Original session messages are NOT modified - only the messages sent to the LLM are affected.\n */\nexport interface ContextEvent {\n\ttype: \"context\";\n\t/** Messages about to be sent to the LLM (deep copy, safe to modify) */\n\tmessages: AgentMessage[];\n}\n\n/**\n * Event data for before_agent_start event.\n * Fired after user submits a prompt but before the agent loop starts.\n * Allows hooks to inject context that will be persisted and visible in TUI.\n */\nexport interface BeforeAgentStartEvent {\n\ttype: \"before_agent_start\";\n\t/** The user's prompt text */\n\tprompt: string;\n\t/** Any images attached to the prompt */\n\timages?: ImageContent[];\n}\n\n/**\n * Event data for agent_start event.\n * Fired when an agent loop starts (once per user prompt).\n */\nexport interface AgentStartEvent {\n\ttype: \"agent_start\";\n}\n\n/**\n * Event data for agent_end event.\n */\nexport interface AgentEndEvent {\n\ttype: \"agent_end\";\n\tmessages: AgentMessage[];\n}\n\n/**\n * Event data for turn_start event.\n */\nexport interface TurnStartEvent {\n\ttype: \"turn_start\";\n\tturnIndex: number;\n\ttimestamp: number;\n}\n\n/**\n * Event data for turn_end event.\n */\nexport interface TurnEndEvent {\n\ttype: \"turn_end\";\n\tturnIndex: number;\n\tmessage: AgentMessage;\n\ttoolResults: ToolResultMessage[];\n}\n\n/**\n * Event data for tool_call event.\n * Fired before a tool is executed. Hooks can block execution.\n */\nexport interface ToolCallEvent {\n\ttype: \"tool_call\";\n\t/** Tool name (e.g., \"bash\", \"edit\", \"write\") */\n\ttoolName: string;\n\t/** Tool call ID */\n\ttoolCallId: string;\n\t/** Tool input parameters */\n\tinput: Record<string, unknown>;\n}\n\n/**\n * Base interface for tool_result events.\n */\ninterface ToolResultEventBase {\n\ttype: \"tool_result\";\n\t/** Tool call ID */\n\ttoolCallId: string;\n\t/** Tool input parameters */\n\tinput: Record<string, unknown>;\n\t/** Full content array (text and images) */\n\tcontent: (TextContent | ImageContent)[];\n\t/** Whether the tool execution was an error */\n\tisError: boolean;\n}\n\n/** Tool result event for bash tool */\nexport interface BashToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"bash\";\n\tdetails: BashToolDetails | undefined;\n}\n\n/** Tool result event for read tool */\nexport interface ReadToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"read\";\n\tdetails: ReadToolDetails | undefined;\n}\n\n/** Tool result event for edit tool */\nexport interface EditToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"edit\";\n\tdetails: EditToolDetails | undefined;\n}\n\n/** Tool result event for write tool */\nexport interface WriteToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"write\";\n\tdetails: undefined;\n}\n\n/** Tool result event for grep tool */\nexport interface GrepToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"grep\";\n\tdetails: GrepToolDetails | undefined;\n}\n\n/** Tool result event for find tool */\nexport interface FindToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"find\";\n\tdetails: FindToolDetails | undefined;\n}\n\n/** Tool result event for ls tool */\nexport interface LsToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"ls\";\n\tdetails: LsToolDetails | undefined;\n}\n\n/** Tool result event for custom/unknown tools */\nexport interface CustomToolResultEvent extends ToolResultEventBase {\n\ttoolName: string;\n\tdetails: unknown;\n}\n\n/**\n * Event data for tool_result event.\n * Fired after a tool is executed. Hooks can modify the result.\n * Use toolName to discriminate and get typed details.\n */\nexport type ToolResultEvent =\n\t| BashToolResultEvent\n\t| ReadToolResultEvent\n\t| EditToolResultEvent\n\t| WriteToolResultEvent\n\t| GrepToolResultEvent\n\t| FindToolResultEvent\n\t| LsToolResultEvent\n\t| CustomToolResultEvent;\n\n// Type guards for narrowing ToolResultEvent to specific tool types\nexport function isBashToolResult(e: ToolResultEvent): e is BashToolResultEvent {\n\treturn e.toolName === \"bash\";\n}\nexport function isReadToolResult(e: ToolResultEvent): e is ReadToolResultEvent {\n\treturn e.toolName === \"read\";\n}\nexport function isEditToolResult(e: ToolResultEvent): e is EditToolResultEvent {\n\treturn e.toolName === \"edit\";\n}\nexport function isWriteToolResult(e: ToolResultEvent): e is WriteToolResultEvent {\n\treturn e.toolName === \"write\";\n}\nexport function isGrepToolResult(e: ToolResultEvent): e is GrepToolResultEvent {\n\treturn e.toolName === \"grep\";\n}\nexport function isFindToolResult(e: ToolResultEvent): e is FindToolResultEvent {\n\treturn e.toolName === \"find\";\n}\nexport function isLsToolResult(e: ToolResultEvent): e is LsToolResultEvent {\n\treturn e.toolName === \"ls\";\n}\n\n/**\n * Union of all hook event types.\n */\nexport type HookEvent =\n\t| SessionEvent\n\t| ContextEvent\n\t| BeforeAgentStartEvent\n\t| AgentStartEvent\n\t| AgentEndEvent\n\t| TurnStartEvent\n\t| TurnEndEvent\n\t| ToolCallEvent\n\t| ToolResultEvent;\n\n// ============================================================================\n// Event Results\n// ============================================================================\n\n/**\n * Return type for context event handlers.\n * Allows hooks to modify messages before they're sent to the LLM.\n */\nexport interface ContextEventResult {\n\t/** Modified messages to send instead of the original */\n\tmessages?: Message[];\n}\n\n/**\n * Return type for tool_call event handlers.\n * Allows hooks to block tool execution.\n */\nexport interface ToolCallEventResult {\n\t/** If true, block the tool from executing */\n\tblock?: boolean;\n\t/** Reason for blocking (returned to LLM as error) */\n\treason?: string;\n}\n\n/**\n * Return type for tool_result event handlers.\n * Allows hooks to modify tool results.\n */\nexport interface ToolResultEventResult {\n\t/** Replacement content array (text and images) */\n\tcontent?: (TextContent | ImageContent)[];\n\t/** Replacement details */\n\tdetails?: unknown;\n\t/** Override isError flag */\n\tisError?: boolean;\n}\n\n/**\n * Return type for before_agent_start event handlers.\n * Allows hooks to inject context before the agent runs.\n */\nexport interface BeforeAgentStartEventResult {\n\t/** Message to inject into context (persisted to session, visible in TUI) */\n\tmessage?: Pick<HookMessage, \"customType\" | \"content\" | \"display\" | \"details\">;\n}\n\n/** Return type for session_before_switch handlers */\nexport interface SessionBeforeSwitchResult {\n\t/** If true, cancel the switch */\n\tcancel?: boolean;\n}\n\n/** Return type for session_before_branch handlers */\nexport interface SessionBeforeBranchResult {\n\t/**\n\t * If true, abort the branch entirely. No new session file is created,\n\t * conversation stays unchanged.\n\t */\n\tcancel?: boolean;\n\t/**\n\t * If true, the branch proceeds (new session file created, session state updated)\n\t * but the in-memory conversation is NOT rewound to the branch point.\n\t *\n\t * Use case: git-checkpoint hook that restores code state separately.\n\t * The hook handles state restoration itself, so it doesn't want the\n\t * agent's conversation to be rewound (which would lose recent context).\n\t *\n\t * - `cancel: true` → nothing happens, user stays in current session\n\t * - `skipConversationRestore: true` → branch happens, but messages stay as-is\n\t * - neither → branch happens AND messages rewind to branch point (default)\n\t */\n\tskipConversationRestore?: boolean;\n}\n\n/** Return type for session_before_compact handlers */\nexport interface SessionBeforeCompactResult {\n\t/** If true, cancel the compaction */\n\tcancel?: boolean;\n\t/** Custom compaction result - SessionManager adds id/parentId */\n\tcompaction?: CompactionResult;\n}\n\n/** Return type for session_before_tree handlers */\nexport interface SessionBeforeTreeResult {\n\t/** If true, cancel the navigation entirely */\n\tcancel?: boolean;\n\t/**\n\t * Custom summary (skips default summarizer).\n\t * Only used if preparation.userWantsSummary is true.\n\t */\n\tsummary?: {\n\t\tsummary: string;\n\t\tdetails?: unknown;\n\t};\n}\n\n// ============================================================================\n// Hook API\n// ============================================================================\n\n/**\n * Handler function type for each event.\n * Handlers can return R, undefined, or void (bare return statements).\n */\n// biome-ignore lint/suspicious/noConfusingVoidType: void allows bare return statements in handlers\nexport type HookHandler<E, R = undefined> = (event: E, ctx: HookContext) => Promise<R | void> | R | void;\n\nexport interface HookMessageRenderOptions {\n\t/** Whether the view is expanded */\n\texpanded: boolean;\n}\n\n/**\n * Renderer for hook messages.\n * Hooks register these to provide custom TUI rendering for their message types.\n */\nexport type HookMessageRenderer<T = unknown> = (\n\tmessage: HookMessage<T>,\n\toptions: HookMessageRenderOptions,\n\ttheme: Theme,\n) => Component | undefined;\n\n/**\n * Command registration options.\n */\nexport interface RegisteredCommand {\n\tname: string;\n\tdescription?: string;\n\n\thandler: (args: string, ctx: HookCommandContext) => Promise<void>;\n}\n\n/**\n * HookAPI passed to hook factory functions.\n * Hooks use pi.on() to subscribe to events and pi.sendMessage() to inject messages.\n */\nexport interface HookAPI {\n\t// Session events\n\ton(event: \"session_start\", handler: HookHandler<SessionStartEvent>): void;\n\ton(event: \"session_before_switch\", handler: HookHandler<SessionBeforeSwitchEvent, SessionBeforeSwitchResult>): void;\n\ton(event: \"session_switch\", handler: HookHandler<SessionSwitchEvent>): void;\n\ton(event: \"session_before_branch\", handler: HookHandler<SessionBeforeBranchEvent, SessionBeforeBranchResult>): void;\n\ton(event: \"session_branch\", handler: HookHandler<SessionBranchEvent>): void;\n\ton(\n\t\tevent: \"session_before_compact\",\n\t\thandler: HookHandler<SessionBeforeCompactEvent, SessionBeforeCompactResult>,\n\t): void;\n\ton(event: \"session_compact\", handler: HookHandler<SessionCompactEvent>): void;\n\ton(event: \"session_shutdown\", handler: HookHandler<SessionShutdownEvent>): void;\n\ton(event: \"session_before_tree\", handler: HookHandler<SessionBeforeTreeEvent, SessionBeforeTreeResult>): void;\n\ton(event: \"session_tree\", handler: HookHandler<SessionTreeEvent>): void;\n\n\t// Context and agent events\n\ton(event: \"context\", handler: HookHandler<ContextEvent, ContextEventResult>): void;\n\ton(event: \"before_agent_start\", handler: HookHandler<BeforeAgentStartEvent, BeforeAgentStartEventResult>): void;\n\ton(event: \"agent_start\", handler: HookHandler<AgentStartEvent>): void;\n\ton(event: \"agent_end\", handler: HookHandler<AgentEndEvent>): void;\n\ton(event: \"turn_start\", handler: HookHandler<TurnStartEvent>): void;\n\ton(event: \"turn_end\", handler: HookHandler<TurnEndEvent>): void;\n\ton(event: \"tool_call\", handler: HookHandler<ToolCallEvent, ToolCallEventResult>): void;\n\ton(event: \"tool_result\", handler: HookHandler<ToolResultEvent, ToolResultEventResult>): void;\n\n\t/**\n\t * Send a custom message to the session. Creates a CustomMessageEntry that\n\t * participates in LLM context and can be displayed in the TUI.\n\t *\n\t * Use this when you want the LLM to see the message content.\n\t * For hook state that should NOT be sent to the LLM, use appendEntry() instead.\n\t *\n\t * @param message - The message to send\n\t * @param message.customType - Identifier for your hook (used for filtering on reload)\n\t * @param message.content - Message content (string or TextContent/ImageContent array)\n\t * @param message.display - Whether to show in TUI (true = styled display, false = hidden)\n\t * @param message.details - Optional hook-specific metadata (not sent to LLM)\n\t * @param options.triggerTurn - If true and agent is idle, triggers a new LLM turn. Default: false.\n\t * If agent is streaming, message is queued and triggerTurn is ignored.\n\t * @param options.deliverAs - How to deliver when agent is streaming. Default: \"steer\".\n\t * - \"steer\": Interrupt mid-run, delivered after current tool execution.\n\t * - \"followUp\": Wait until agent finishes all work before delivery.\n\t */\n\tsendMessage<T = unknown>(\n\t\tmessage: Pick<HookMessage<T>, \"customType\" | \"content\" | \"display\" | \"details\">,\n\t\toptions?: { triggerTurn?: boolean; deliverAs?: \"steer\" | \"followUp\" },\n\t): void;\n\n\t/**\n\t * Append a custom entry to the session for hook state persistence.\n\t * Creates a CustomEntry that does NOT participate in LLM context.\n\t *\n\t * Use this to store hook-specific data that should persist across session reloads\n\t * but should NOT be sent to the LLM. On reload, scan session entries for your\n\t * customType to reconstruct hook state.\n\t *\n\t * For messages that SHOULD be sent to the LLM, use sendMessage() instead.\n\t *\n\t * @param customType - Identifier for your hook (used for filtering on reload)\n\t * @param data - Hook-specific data to persist (must be JSON-serializable)\n\t *\n\t * @example\n\t * // Store permission state\n\t * pi.appendEntry(\"permissions\", { level: \"full\", grantedAt: Date.now() });\n\t *\n\t * // On reload, reconstruct state from entries\n\t * pi.on(\"session\", async (event, ctx) => {\n\t * if (event.reason === \"start\") {\n\t * const entries = event.sessionManager.getEntries();\n\t * const myEntries = entries.filter(e => e.type === \"custom\" && e.customType === \"permissions\");\n\t * // Reconstruct state from myEntries...\n\t * }\n\t * });\n\t */\n\tappendEntry<T = unknown>(customType: string, data?: T): void;\n\n\t/**\n\t * Register a custom renderer for CustomMessageEntry with a specific customType.\n\t * The renderer is called when rendering the entry in the TUI.\n\t * Return nothing to use the default renderer.\n\t */\n\tregisterMessageRenderer<T = unknown>(customType: string, renderer: HookMessageRenderer<T>): void;\n\n\t/**\n\t * Register a custom slash command.\n\t * Handler receives HookCommandContext with session control methods.\n\t */\n\tregisterCommand(name: string, options: { description?: string; handler: RegisteredCommand[\"handler\"] }): void;\n\n\t/**\n\t * Execute a shell command and return stdout/stderr/code.\n\t * Supports timeout and abort signal.\n\t */\n\texec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;\n}\n\n/**\n * Hook factory function type.\n * Hooks export a default function that receives the HookAPI.\n */\nexport type HookFactory = (pi: HookAPI) => void;\n\n// ============================================================================\n// Errors\n// ============================================================================\n\n/**\n * Error emitted when a hook fails.\n */\nexport interface HookError {\n\thookPath: string;\n\tevent: string;\n\terror: string;\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/hooks/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA4gBH,mEAAmE;AACnE,MAAM,UAAU,gBAAgB,CAAC,CAAkB,EAA4B;IAC9E,OAAO,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC;AAAA,CAC7B;AACD,MAAM,UAAU,gBAAgB,CAAC,CAAkB,EAA4B;IAC9E,OAAO,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC;AAAA,CAC7B;AACD,MAAM,UAAU,gBAAgB,CAAC,CAAkB,EAA4B;IAC9E,OAAO,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC;AAAA,CAC7B;AACD,MAAM,UAAU,iBAAiB,CAAC,CAAkB,EAA6B;IAChF,OAAO,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC;AAAA,CAC9B;AACD,MAAM,UAAU,gBAAgB,CAAC,CAAkB,EAA4B;IAC9E,OAAO,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC;AAAA,CAC7B;AACD,MAAM,UAAU,gBAAgB,CAAC,CAAkB,EAA4B;IAC9E,OAAO,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC;AAAA,CAC7B;AACD,MAAM,UAAU,cAAc,CAAC,CAAkB,EAA0B;IAC1E,OAAO,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC;AAAA,CAC3B","sourcesContent":["/**\n * Hook system types.\n *\n * Hooks are TypeScript modules that can subscribe to agent lifecycle events\n * and interact with the user via UI primitives.\n */\n\nimport type { AgentMessage } from \"@mariozechner/pi-agent-core\";\nimport type { ImageContent, Model, TextContent, ToolResultMessage } from \"@mariozechner/pi-ai\";\nimport type { Component, KeyId, TUI } from \"@mariozechner/pi-tui\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { CompactionPreparation, CompactionResult } from \"../compaction/index.js\";\nimport type { ExecOptions, ExecResult } from \"../exec.js\";\nimport type { HookMessage } from \"../messages.js\";\nimport type { ModelRegistry } from \"../model-registry.js\";\nimport type {\n\tBranchSummaryEntry,\n\tCompactionEntry,\n\tReadonlySessionManager,\n\tSessionEntry,\n\tSessionManager,\n} from \"../session-manager.js\";\n\nimport type { EditToolDetails } from \"../tools/edit.js\";\nimport type {\n\tBashToolDetails,\n\tFindToolDetails,\n\tGrepToolDetails,\n\tLsToolDetails,\n\tReadToolDetails,\n} from \"../tools/index.js\";\n\n// Re-export for backward compatibility\nexport type { ExecOptions, ExecResult } from \"../exec.js\";\n\n/**\n * UI context for hooks to request interactive UI from the harness.\n * Each mode (interactive, RPC, print) provides its own implementation.\n */\nexport interface HookUIContext {\n\t/**\n\t * Show a selector and return the user's choice.\n\t * @param title - Title to display\n\t * @param options - Array of string options\n\t * @returns Selected option string, or null if cancelled\n\t */\n\tselect(title: string, options: string[]): Promise<string | undefined>;\n\n\t/**\n\t * Show a confirmation dialog.\n\t * @returns true if confirmed, false if cancelled\n\t */\n\tconfirm(title: string, message: string): Promise<boolean>;\n\n\t/**\n\t * Show a text input dialog.\n\t * @returns User input, or undefined if cancelled\n\t */\n\tinput(title: string, placeholder?: string): Promise<string | undefined>;\n\n\t/**\n\t * Show a notification to the user.\n\t */\n\tnotify(message: string, type?: \"info\" | \"warning\" | \"error\"): void;\n\n\t/**\n\t * Set status text in the footer/status bar.\n\t * Pass undefined as text to clear the status for this key.\n\t * Text can include ANSI escape codes for styling.\n\t * Note: Newlines, tabs, and carriage returns are replaced with spaces.\n\t * The combined status line is truncated to terminal width.\n\t * @param key - Unique key to identify this status (e.g., hook name)\n\t * @param text - Status text to display, or undefined to clear\n\t */\n\tsetStatus(key: string, text: string | undefined): void;\n\n\t/**\n\t * Set a widget to display in the status area (above the editor, below \"Working...\" indicator).\n\t * Supports multi-line content. Pass undefined to clear.\n\t * Text can include ANSI escape codes for styling.\n\t *\n\t * Accepts either an array of styled strings, or a factory function that creates a Component.\n\t *\n\t * @param key - Unique key to identify this widget (e.g., hook name)\n\t * @param content - Array of lines to display, or undefined to clear\n\t *\n\t * @example\n\t * // Show a todo list with styled strings\n\t * ctx.ui.setWidget(\"plan-todos\", [\n\t * theme.fg(\"accent\", \"Plan Progress:\"),\n\t * \"☑ \" + theme.fg(\"muted\", theme.strikethrough(\"Step 1: Read files\")),\n\t * \"☐ Step 2: Modify code\",\n\t * \"☐ Step 3: Run tests\",\n\t * ]);\n\t *\n\t * // Clear the widget\n\t * ctx.ui.setWidget(\"plan-todos\", undefined);\n\t */\n\tsetWidget(key: string, content: string[] | undefined): void;\n\n\t/**\n\t * Set a custom component as a widget (above the editor, below \"Working...\" indicator).\n\t * Unlike custom(), this does NOT take keyboard focus - the editor remains focused.\n\t * Pass undefined to clear the widget.\n\t *\n\t * The component should implement render(width) and optionally dispose().\n\t * Components are rendered inline without taking focus - they cannot handle keyboard input.\n\t *\n\t * @param key - Unique key to identify this widget (e.g., hook name)\n\t * @param content - Factory function that creates the component, or undefined to clear\n\t *\n\t * @example\n\t * // Show a custom progress component\n\t * ctx.ui.setWidget(\"my-progress\", (tui, theme) => {\n\t * return new MyProgressComponent(tui, theme);\n\t * });\n\t *\n\t * // Clear the widget\n\t * ctx.ui.setWidget(\"my-progress\", undefined);\n\t */\n\tsetWidget(key: string, content: ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) | undefined): void;\n\n\t/**\n\t * Show a custom component with keyboard focus.\n\t * The factory receives TUI, theme, and a done() callback to close the component.\n\t * Can be async for fire-and-forget work (don't await the work, just start it).\n\t *\n\t * @param factory - Function that creates the component. Call done() when finished.\n\t * @returns Promise that resolves with the value passed to done()\n\t *\n\t * @example\n\t * // Sync factory\n\t * const result = await ctx.ui.custom((tui, theme, done) => {\n\t * const component = new MyComponent(tui, theme);\n\t * component.onFinish = (value) => done(value);\n\t * return component;\n\t * });\n\t *\n\t * // Async factory with fire-and-forget work\n\t * const result = await ctx.ui.custom(async (tui, theme, done) => {\n\t * const loader = new CancellableLoader(tui, theme.fg(\"accent\"), theme.fg(\"muted\"), \"Working...\");\n\t * loader.onAbort = () => done(null);\n\t * doWork(loader.signal).then(done); // Don't await - fire and forget\n\t * return loader;\n\t * });\n\t */\n\tcustom<T>(\n\t\tfactory: (\n\t\t\ttui: TUI,\n\t\t\ttheme: Theme,\n\t\t\tdone: (result: T) => void,\n\t\t) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,\n\t): Promise<T>;\n\n\t/**\n\t * Set the text in the core input editor.\n\t * Use this to pre-fill the input box with generated content (e.g., prompt templates, extracted questions).\n\t * @param text - Text to set in the editor\n\t */\n\tsetEditorText(text: string): void;\n\n\t/**\n\t * Get the current text from the core input editor.\n\t * @returns Current editor text\n\t */\n\tgetEditorText(): string;\n\n\t/**\n\t * Show a multi-line editor for text editing.\n\t * Supports Ctrl+G to open external editor ($VISUAL or $EDITOR).\n\t * @param title - Title describing what is being edited\n\t * @param prefill - Optional initial text\n\t * @returns Edited text, or undefined if cancelled (Escape)\n\t */\n\teditor(title: string, prefill?: string): Promise<string | undefined>;\n\n\t/**\n\t * Get the current theme for styling text with ANSI codes.\n\t * Use theme.fg() and theme.bg() to style status text.\n\t *\n\t * @example\n\t * const theme = ctx.ui.theme;\n\t * ctx.ui.setStatus(\"my-hook\", theme.fg(\"success\", \"✓\") + \" Ready\");\n\t */\n\treadonly theme: Theme;\n}\n\n/**\n * Context passed to hook event handlers.\n * For command handlers, see HookCommandContext which extends this with session control methods.\n */\nexport interface HookContext {\n\t/** UI methods for user interaction */\n\tui: HookUIContext;\n\t/** Whether UI is available (false in print mode) */\n\thasUI: boolean;\n\t/** Current working directory */\n\tcwd: string;\n\t/** Session manager (read-only) - use pi.sendMessage()/pi.appendEntry() for writes */\n\tsessionManager: ReadonlySessionManager;\n\t/** Model registry - use for API key resolution and model retrieval */\n\tmodelRegistry: ModelRegistry;\n\t/** Current model (may be undefined if no model is selected yet) */\n\tmodel: Model<any> | undefined;\n\t/** Whether the agent is idle (not streaming) */\n\tisIdle(): boolean;\n\t/** Abort the current agent operation (fire-and-forget, does not wait) */\n\tabort(): void;\n\t/** Whether there are queued messages waiting to be processed */\n\thasPendingMessages(): boolean;\n}\n\n/**\n * Extended context for slash command handlers.\n * Includes session control methods that are only safe in user-initiated commands.\n *\n * These methods are not available in event handlers because they can cause\n * deadlocks when called from within the agent loop (e.g., tool_call, context events).\n */\nexport interface HookCommandContext extends HookContext {\n\t/** Wait for the agent to finish streaming */\n\twaitForIdle(): Promise<void>;\n\n\t/**\n\t * Start a new session, optionally with a setup callback to initialize it.\n\t * The setup callback receives a writable SessionManager for the new session.\n\t *\n\t * @param options.parentSession - Path to parent session for lineage tracking\n\t * @param options.setup - Async callback to initialize the new session (e.g., append messages)\n\t * @returns Object with `cancelled: true` if a hook cancelled the new session\n\t *\n\t * @example\n\t * // Handoff: summarize current session and start fresh with context\n\t * await ctx.newSession({\n\t * parentSession: ctx.sessionManager.getSessionFile(),\n\t * setup: async (sm) => {\n\t * sm.appendMessage({ role: \"user\", content: [{ type: \"text\", text: summary }] });\n\t * }\n\t * });\n\t */\n\tnewSession(options?: {\n\t\tparentSession?: string;\n\t\tsetup?: (sessionManager: SessionManager) => Promise<void>;\n\t}): Promise<{ cancelled: boolean }>;\n\n\t/**\n\t * Branch from a specific entry, creating a new session file.\n\t *\n\t * @param entryId - ID of the entry to branch from\n\t * @returns Object with `cancelled: true` if a hook cancelled the branch\n\t */\n\tbranch(entryId: string): Promise<{ cancelled: boolean }>;\n\n\t/**\n\t * Navigate to a different point in the session tree (in-place).\n\t *\n\t * @param targetId - ID of the entry to navigate to\n\t * @param options.summarize - Whether to summarize the abandoned branch\n\t * @returns Object with `cancelled: true` if a hook cancelled the navigation\n\t */\n\tnavigateTree(targetId: string, options?: { summarize?: boolean }): Promise<{ cancelled: boolean }>;\n}\n\n// ============================================================================\n// Session Events\n// ============================================================================\n\n/** Fired on initial session load */\nexport interface SessionStartEvent {\n\ttype: \"session_start\";\n}\n\n/** Fired before switching to another session (can be cancelled) */\nexport interface SessionBeforeSwitchEvent {\n\ttype: \"session_before_switch\";\n\t/** Reason for the switch */\n\treason: \"new\" | \"resume\";\n\t/** Session file we're switching to (only for \"resume\") */\n\ttargetSessionFile?: string;\n}\n\n/** Fired after switching to another session */\nexport interface SessionSwitchEvent {\n\ttype: \"session_switch\";\n\t/** Reason for the switch */\n\treason: \"new\" | \"resume\";\n\t/** Session file we came from */\n\tpreviousSessionFile: string | undefined;\n}\n\n/** Fired before branching a session (can be cancelled) */\nexport interface SessionBeforeBranchEvent {\n\ttype: \"session_before_branch\";\n\t/** ID of the entry to branch from */\n\tentryId: string;\n}\n\n/** Fired after branching a session */\nexport interface SessionBranchEvent {\n\ttype: \"session_branch\";\n\tpreviousSessionFile: string | undefined;\n}\n\n/** Fired before context compaction (can be cancelled or customized) */\nexport interface SessionBeforeCompactEvent {\n\ttype: \"session_before_compact\";\n\t/** Compaction preparation with messages to summarize, file ops, previous summary, etc. */\n\tpreparation: CompactionPreparation;\n\t/** Branch entries (root to current leaf). Use to inspect custom state or previous compactions. */\n\tbranchEntries: SessionEntry[];\n\t/** Optional user-provided instructions for the summary */\n\tcustomInstructions?: string;\n\t/** Abort signal - hooks should pass this to LLM calls and check it periodically */\n\tsignal: AbortSignal;\n}\n\n/** Fired after context compaction */\nexport interface SessionCompactEvent {\n\ttype: \"session_compact\";\n\tcompactionEntry: CompactionEntry;\n\t/** Whether the compaction entry was provided by a hook */\n\tfromHook: boolean;\n}\n\n/** Fired on process exit (SIGINT/SIGTERM) */\nexport interface SessionShutdownEvent {\n\ttype: \"session_shutdown\";\n}\n\n/** Preparation data for tree navigation (used by session_before_tree event) */\nexport interface TreePreparation {\n\t/** Node being switched to */\n\ttargetId: string;\n\t/** Current active leaf (being abandoned), null if no current position */\n\toldLeafId: string | null;\n\t/** Common ancestor of target and old leaf, null if no common ancestor */\n\tcommonAncestorId: string | null;\n\t/** Entries to summarize (old leaf back to common ancestor or compaction) */\n\tentriesToSummarize: SessionEntry[];\n\t/** Whether user chose to summarize */\n\tuserWantsSummary: boolean;\n}\n\n/** Fired before navigating to a different node in the session tree (can be cancelled) */\nexport interface SessionBeforeTreeEvent {\n\ttype: \"session_before_tree\";\n\t/** Preparation data for the navigation */\n\tpreparation: TreePreparation;\n\t/** Abort signal - honors Escape during summarization (model available via ctx.model) */\n\tsignal: AbortSignal;\n}\n\n/** Fired after navigating to a different node in the session tree */\nexport interface SessionTreeEvent {\n\ttype: \"session_tree\";\n\t/** The new active leaf, null if navigated to before first entry */\n\tnewLeafId: string | null;\n\t/** Previous active leaf, null if there was no position */\n\toldLeafId: string | null;\n\t/** Branch summary entry if one was created */\n\tsummaryEntry?: BranchSummaryEntry;\n\t/** Whether summary came from hook */\n\tfromHook?: boolean;\n}\n\n/** Union of all session event types */\nexport type SessionEvent =\n\t| SessionStartEvent\n\t| SessionBeforeSwitchEvent\n\t| SessionSwitchEvent\n\t| SessionBeforeBranchEvent\n\t| SessionBranchEvent\n\t| SessionBeforeCompactEvent\n\t| SessionCompactEvent\n\t| SessionShutdownEvent\n\t| SessionBeforeTreeEvent\n\t| SessionTreeEvent;\n\n/**\n * Event data for context event.\n * Fired before each LLM call, allowing hooks to modify context non-destructively.\n * Original session messages are NOT modified - only the messages sent to the LLM are affected.\n */\nexport interface ContextEvent {\n\ttype: \"context\";\n\t/** Messages about to be sent to the LLM (deep copy, safe to modify) */\n\tmessages: AgentMessage[];\n}\n\n/**\n * Event data for before_agent_start event.\n * Fired after user submits a prompt but before the agent loop starts.\n * Allows hooks to inject context that will be persisted and visible in TUI.\n */\nexport interface BeforeAgentStartEvent {\n\ttype: \"before_agent_start\";\n\t/** The user's prompt text */\n\tprompt: string;\n\t/** Any images attached to the prompt */\n\timages?: ImageContent[];\n}\n\n/**\n * Event data for agent_start event.\n * Fired when an agent loop starts (once per user prompt).\n */\nexport interface AgentStartEvent {\n\ttype: \"agent_start\";\n}\n\n/**\n * Event data for agent_end event.\n */\nexport interface AgentEndEvent {\n\ttype: \"agent_end\";\n\tmessages: AgentMessage[];\n}\n\n/**\n * Event data for turn_start event.\n */\nexport interface TurnStartEvent {\n\ttype: \"turn_start\";\n\tturnIndex: number;\n\ttimestamp: number;\n}\n\n/**\n * Event data for turn_end event.\n */\nexport interface TurnEndEvent {\n\ttype: \"turn_end\";\n\tturnIndex: number;\n\tmessage: AgentMessage;\n\ttoolResults: ToolResultMessage[];\n}\n\n/**\n * Event data for tool_call event.\n * Fired before a tool is executed. Hooks can block execution.\n */\nexport interface ToolCallEvent {\n\ttype: \"tool_call\";\n\t/** Tool name (e.g., \"bash\", \"edit\", \"write\") */\n\ttoolName: string;\n\t/** Tool call ID */\n\ttoolCallId: string;\n\t/** Tool input parameters */\n\tinput: Record<string, unknown>;\n}\n\n/**\n * Base interface for tool_result events.\n */\ninterface ToolResultEventBase {\n\ttype: \"tool_result\";\n\t/** Tool call ID */\n\ttoolCallId: string;\n\t/** Tool input parameters */\n\tinput: Record<string, unknown>;\n\t/** Full content array (text and images) */\n\tcontent: (TextContent | ImageContent)[];\n\t/** Whether the tool execution was an error */\n\tisError: boolean;\n}\n\n/** Tool result event for bash tool */\nexport interface BashToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"bash\";\n\tdetails: BashToolDetails | undefined;\n}\n\n/** Tool result event for read tool */\nexport interface ReadToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"read\";\n\tdetails: ReadToolDetails | undefined;\n}\n\n/** Tool result event for edit tool */\nexport interface EditToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"edit\";\n\tdetails: EditToolDetails | undefined;\n}\n\n/** Tool result event for write tool */\nexport interface WriteToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"write\";\n\tdetails: undefined;\n}\n\n/** Tool result event for grep tool */\nexport interface GrepToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"grep\";\n\tdetails: GrepToolDetails | undefined;\n}\n\n/** Tool result event for find tool */\nexport interface FindToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"find\";\n\tdetails: FindToolDetails | undefined;\n}\n\n/** Tool result event for ls tool */\nexport interface LsToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"ls\";\n\tdetails: LsToolDetails | undefined;\n}\n\n/** Tool result event for custom/unknown tools */\nexport interface CustomToolResultEvent extends ToolResultEventBase {\n\ttoolName: string;\n\tdetails: unknown;\n}\n\n/**\n * Event data for tool_result event.\n * Fired after a tool is executed. Hooks can modify the result.\n * Use toolName to discriminate and get typed details.\n */\nexport type ToolResultEvent =\n\t| BashToolResultEvent\n\t| ReadToolResultEvent\n\t| EditToolResultEvent\n\t| WriteToolResultEvent\n\t| GrepToolResultEvent\n\t| FindToolResultEvent\n\t| LsToolResultEvent\n\t| CustomToolResultEvent;\n\n// Type guards for narrowing ToolResultEvent to specific tool types\nexport function isBashToolResult(e: ToolResultEvent): e is BashToolResultEvent {\n\treturn e.toolName === \"bash\";\n}\nexport function isReadToolResult(e: ToolResultEvent): e is ReadToolResultEvent {\n\treturn e.toolName === \"read\";\n}\nexport function isEditToolResult(e: ToolResultEvent): e is EditToolResultEvent {\n\treturn e.toolName === \"edit\";\n}\nexport function isWriteToolResult(e: ToolResultEvent): e is WriteToolResultEvent {\n\treturn e.toolName === \"write\";\n}\nexport function isGrepToolResult(e: ToolResultEvent): e is GrepToolResultEvent {\n\treturn e.toolName === \"grep\";\n}\nexport function isFindToolResult(e: ToolResultEvent): e is FindToolResultEvent {\n\treturn e.toolName === \"find\";\n}\nexport function isLsToolResult(e: ToolResultEvent): e is LsToolResultEvent {\n\treturn e.toolName === \"ls\";\n}\n\n/**\n * Union of all hook event types.\n */\nexport type HookEvent =\n\t| SessionEvent\n\t| ContextEvent\n\t| BeforeAgentStartEvent\n\t| AgentStartEvent\n\t| AgentEndEvent\n\t| TurnStartEvent\n\t| TurnEndEvent\n\t| ToolCallEvent\n\t| ToolResultEvent;\n\n// ============================================================================\n// Event Results\n// ============================================================================\n\n/**\n * Return type for context event handlers.\n * Allows hooks to modify messages before they're sent to the LLM.\n */\nexport interface ContextEventResult {\n\t/** Modified messages to send instead of the original */\n\tmessages?: AgentMessage[];\n}\n\n/**\n * Return type for tool_call event handlers.\n * Allows hooks to block tool execution.\n */\nexport interface ToolCallEventResult {\n\t/** If true, block the tool from executing */\n\tblock?: boolean;\n\t/** Reason for blocking (returned to LLM as error) */\n\treason?: string;\n}\n\n/**\n * Return type for tool_result event handlers.\n * Allows hooks to modify tool results.\n */\nexport interface ToolResultEventResult {\n\t/** Replacement content array (text and images) */\n\tcontent?: (TextContent | ImageContent)[];\n\t/** Replacement details */\n\tdetails?: unknown;\n\t/** Override isError flag */\n\tisError?: boolean;\n}\n\n/**\n * Return type for before_agent_start event handlers.\n * Allows hooks to inject context before the agent runs.\n */\nexport interface BeforeAgentStartEventResult {\n\t/** Message to inject into context (persisted to session, visible in TUI) */\n\tmessage?: Pick<HookMessage, \"customType\" | \"content\" | \"display\" | \"details\">;\n\t/** Text to append to the system prompt for this agent run */\n\tsystemPromptAppend?: string;\n}\n\n/** Return type for session_before_switch handlers */\nexport interface SessionBeforeSwitchResult {\n\t/** If true, cancel the switch */\n\tcancel?: boolean;\n}\n\n/** Return type for session_before_branch handlers */\nexport interface SessionBeforeBranchResult {\n\t/**\n\t * If true, abort the branch entirely. No new session file is created,\n\t * conversation stays unchanged.\n\t */\n\tcancel?: boolean;\n\t/**\n\t * If true, the branch proceeds (new session file created, session state updated)\n\t * but the in-memory conversation is NOT rewound to the branch point.\n\t *\n\t * Use case: git-checkpoint hook that restores code state separately.\n\t * The hook handles state restoration itself, so it doesn't want the\n\t * agent's conversation to be rewound (which would lose recent context).\n\t *\n\t * - `cancel: true` → nothing happens, user stays in current session\n\t * - `skipConversationRestore: true` → branch happens, but messages stay as-is\n\t * - neither → branch happens AND messages rewind to branch point (default)\n\t */\n\tskipConversationRestore?: boolean;\n}\n\n/** Return type for session_before_compact handlers */\nexport interface SessionBeforeCompactResult {\n\t/** If true, cancel the compaction */\n\tcancel?: boolean;\n\t/** Custom compaction result - SessionManager adds id/parentId */\n\tcompaction?: CompactionResult;\n}\n\n/** Return type for session_before_tree handlers */\nexport interface SessionBeforeTreeResult {\n\t/** If true, cancel the navigation entirely */\n\tcancel?: boolean;\n\t/**\n\t * Custom summary (skips default summarizer).\n\t * Only used if preparation.userWantsSummary is true.\n\t */\n\tsummary?: {\n\t\tsummary: string;\n\t\tdetails?: unknown;\n\t};\n}\n\n// ============================================================================\n// Hook API\n// ============================================================================\n\n/**\n * Handler function type for each event.\n * Handlers can return R, undefined, or void (bare return statements).\n */\n// biome-ignore lint/suspicious/noConfusingVoidType: void allows bare return statements in handlers\nexport type HookHandler<E, R = undefined> = (event: E, ctx: HookContext) => Promise<R | void> | R | void;\n\nexport interface HookMessageRenderOptions {\n\t/** Whether the view is expanded */\n\texpanded: boolean;\n}\n\n/**\n * Renderer for hook messages.\n * Hooks register these to provide custom TUI rendering for their message types.\n */\nexport type HookMessageRenderer<T = unknown> = (\n\tmessage: HookMessage<T>,\n\toptions: HookMessageRenderOptions,\n\ttheme: Theme,\n) => Component | undefined;\n\n/**\n * Command registration options.\n */\nexport interface RegisteredCommand {\n\tname: string;\n\tdescription?: string;\n\n\thandler: (args: string, ctx: HookCommandContext) => Promise<void>;\n}\n\n/**\n * HookAPI passed to hook factory functions.\n * Hooks use pi.on() to subscribe to events and pi.sendMessage() to inject messages.\n */\nexport interface HookAPI {\n\t// Session events\n\ton(event: \"session_start\", handler: HookHandler<SessionStartEvent>): void;\n\ton(event: \"session_before_switch\", handler: HookHandler<SessionBeforeSwitchEvent, SessionBeforeSwitchResult>): void;\n\ton(event: \"session_switch\", handler: HookHandler<SessionSwitchEvent>): void;\n\ton(event: \"session_before_branch\", handler: HookHandler<SessionBeforeBranchEvent, SessionBeforeBranchResult>): void;\n\ton(event: \"session_branch\", handler: HookHandler<SessionBranchEvent>): void;\n\ton(\n\t\tevent: \"session_before_compact\",\n\t\thandler: HookHandler<SessionBeforeCompactEvent, SessionBeforeCompactResult>,\n\t): void;\n\ton(event: \"session_compact\", handler: HookHandler<SessionCompactEvent>): void;\n\ton(event: \"session_shutdown\", handler: HookHandler<SessionShutdownEvent>): void;\n\ton(event: \"session_before_tree\", handler: HookHandler<SessionBeforeTreeEvent, SessionBeforeTreeResult>): void;\n\ton(event: \"session_tree\", handler: HookHandler<SessionTreeEvent>): void;\n\n\t// Context and agent events\n\ton(event: \"context\", handler: HookHandler<ContextEvent, ContextEventResult>): void;\n\ton(event: \"before_agent_start\", handler: HookHandler<BeforeAgentStartEvent, BeforeAgentStartEventResult>): void;\n\ton(event: \"agent_start\", handler: HookHandler<AgentStartEvent>): void;\n\ton(event: \"agent_end\", handler: HookHandler<AgentEndEvent>): void;\n\ton(event: \"turn_start\", handler: HookHandler<TurnStartEvent>): void;\n\ton(event: \"turn_end\", handler: HookHandler<TurnEndEvent>): void;\n\ton(event: \"tool_call\", handler: HookHandler<ToolCallEvent, ToolCallEventResult>): void;\n\ton(event: \"tool_result\", handler: HookHandler<ToolResultEvent, ToolResultEventResult>): void;\n\n\t/**\n\t * Send a custom message to the session. Creates a CustomMessageEntry that\n\t * participates in LLM context and can be displayed in the TUI.\n\t *\n\t * Use this when you want the LLM to see the message content.\n\t * For hook state that should NOT be sent to the LLM, use appendEntry() instead.\n\t *\n\t * @param message - The message to send\n\t * @param message.customType - Identifier for your hook (used for filtering on reload)\n\t * @param message.content - Message content (string or TextContent/ImageContent array)\n\t * @param message.display - Whether to show in TUI (true = styled display, false = hidden)\n\t * @param message.details - Optional hook-specific metadata (not sent to LLM)\n\t * @param options.triggerTurn - If true and agent is idle, triggers a new LLM turn. Default: false.\n\t * If agent is streaming, message is queued and triggerTurn is ignored.\n\t * @param options.deliverAs - How to deliver when agent is streaming. Default: \"steer\".\n\t * - \"steer\": Interrupt mid-run, delivered after current tool execution.\n\t * - \"followUp\": Wait until agent finishes all work before delivery.\n\t */\n\tsendMessage<T = unknown>(\n\t\tmessage: Pick<HookMessage<T>, \"customType\" | \"content\" | \"display\" | \"details\">,\n\t\toptions?: { triggerTurn?: boolean; deliverAs?: \"steer\" | \"followUp\" },\n\t): void;\n\n\t/**\n\t * Append a custom entry to the session for hook state persistence.\n\t * Creates a CustomEntry that does NOT participate in LLM context.\n\t *\n\t * Use this to store hook-specific data that should persist across session reloads\n\t * but should NOT be sent to the LLM. On reload, scan session entries for your\n\t * customType to reconstruct hook state.\n\t *\n\t * For messages that SHOULD be sent to the LLM, use sendMessage() instead.\n\t *\n\t * @param customType - Identifier for your hook (used for filtering on reload)\n\t * @param data - Hook-specific data to persist (must be JSON-serializable)\n\t *\n\t * @example\n\t * // Store permission state\n\t * pi.appendEntry(\"permissions\", { level: \"full\", grantedAt: Date.now() });\n\t *\n\t * // On reload, reconstruct state from entries\n\t * pi.on(\"session\", async (event, ctx) => {\n\t * if (event.reason === \"start\") {\n\t * const entries = event.sessionManager.getEntries();\n\t * const myEntries = entries.filter(e => e.type === \"custom\" && e.customType === \"permissions\");\n\t * // Reconstruct state from myEntries...\n\t * }\n\t * });\n\t */\n\tappendEntry<T = unknown>(customType: string, data?: T): void;\n\n\t/**\n\t * Register a custom renderer for CustomMessageEntry with a specific customType.\n\t * The renderer is called when rendering the entry in the TUI.\n\t * Return nothing to use the default renderer.\n\t */\n\tregisterMessageRenderer<T = unknown>(customType: string, renderer: HookMessageRenderer<T>): void;\n\n\t/**\n\t * Register a custom slash command.\n\t * Handler receives HookCommandContext with session control methods.\n\t */\n\tregisterCommand(name: string, options: { description?: string; handler: RegisteredCommand[\"handler\"] }): void;\n\n\t/**\n\t * Execute a shell command and return stdout/stderr/code.\n\t * Supports timeout and abort signal.\n\t */\n\texec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;\n\n\t/**\n\t * Get the list of currently active tool names.\n\t * @returns Array of tool names (e.g., [\"read\", \"bash\", \"edit\", \"write\"])\n\t */\n\tgetActiveTools(): string[];\n\n\t/**\n\t * Get all configured tools (built-in via --tools or default, plus custom tools).\n\t * @returns Array of all tool names\n\t */\n\tgetAllTools(): string[];\n\n\t/**\n\t * Set the active tools by name.\n\t * Both built-in and custom tools can be enabled/disabled.\n\t * Changes take effect on the next agent turn.\n\t * Note: This will invalidate prompt caching for the next request.\n\t *\n\t * @param toolNames - Array of tool names to enable (e.g., [\"read\", \"bash\", \"grep\", \"find\", \"ls\"])\n\t *\n\t * @example\n\t * // Switch to read-only mode (plan mode)\n\t * pi.setActiveTools([\"read\", \"bash\", \"grep\", \"find\", \"ls\"]);\n\t *\n\t * // Restore full access\n\t * pi.setActiveTools([\"read\", \"bash\", \"edit\", \"write\"]);\n\t */\n\tsetActiveTools(toolNames: string[]): void;\n\n\t/**\n\t * Register a CLI flag for this hook.\n\t * Flags are parsed from command line and values accessible via getFlag().\n\t *\n\t * @param name - Flag name (will be --name on CLI)\n\t * @param options - Flag configuration\n\t *\n\t * @example\n\t * pi.registerFlag(\"plan\", {\n\t * description: \"Start in plan mode (read-only)\",\n\t * type: \"boolean\",\n\t * });\n\t */\n\tregisterFlag(\n\t\tname: string,\n\t\toptions: {\n\t\t\t/** Description shown in --help */\n\t\t\tdescription?: string;\n\t\t\t/** Flag type: boolean (--flag) or string (--flag value) */\n\t\t\ttype: \"boolean\" | \"string\";\n\t\t\t/** Default value */\n\t\t\tdefault?: boolean | string;\n\t\t},\n\t): void;\n\n\t/**\n\t * Get the value of a CLI flag registered by this hook.\n\t * Returns undefined if flag was not provided and has no default.\n\t *\n\t * @param name - Flag name (without --)\n\t * @returns Flag value, or undefined\n\t *\n\t * @example\n\t * if (pi.getFlag(\"plan\")) {\n\t * // plan mode enabled\n\t * }\n\t */\n\tgetFlag(name: string): boolean | string | undefined;\n\n\t/**\n\t * Register a keyboard shortcut for this hook.\n\t * The handler is called when the shortcut is pressed in interactive mode.\n\t *\n\t * @param shortcut - Key identifier (e.g., Key.shift(\"p\"), \"ctrl+x\")\n\t * @param options - Shortcut configuration\n\t *\n\t * @example\n\t * import { Key } from \"@mariozechner/pi-tui\";\n\t *\n\t * pi.registerShortcut(Key.shift(\"p\"), {\n\t * description: \"Toggle plan mode\",\n\t * handler: async (ctx) => {\n\t * // toggle plan mode\n\t * },\n\t * });\n\t */\n\tregisterShortcut(\n\t\tshortcut: KeyId,\n\t\toptions: {\n\t\t\t/** Description shown in help */\n\t\t\tdescription?: string;\n\t\t\t/** Handler called when shortcut is pressed */\n\t\t\thandler: (ctx: HookContext) => Promise<void> | void;\n\t\t},\n\t): void;\n}\n\n/**\n * Hook factory function type.\n * Hooks export a default function that receives the HookAPI.\n */\nexport type HookFactory = (pi: HookAPI) => void;\n\n// ============================================================================\n// Errors\n// ============================================================================\n\n/**\n * Error emitted when a hook fails.\n */\nexport interface HookError {\n\thookPath: string;\n\tevent: string;\n\terror: string;\n\tstack?: string;\n}\n"]}
@@ -0,0 +1,59 @@
1
+ import { type EditorAction, type KeyId } from "@mariozechner/pi-tui";
2
+ /**
3
+ * Application-level actions (coding agent specific).
4
+ */
5
+ export type AppAction = "interrupt" | "clear" | "exit" | "suspend" | "cycleThinkingLevel" | "cycleModelForward" | "cycleModelBackward" | "selectModel" | "expandTools" | "toggleThinking" | "externalEditor" | "followUp";
6
+ /**
7
+ * All configurable actions.
8
+ */
9
+ export type KeyAction = AppAction | EditorAction;
10
+ /**
11
+ * Full keybindings configuration (app + editor actions).
12
+ */
13
+ export type KeybindingsConfig = {
14
+ [K in KeyAction]?: KeyId | KeyId[];
15
+ };
16
+ /**
17
+ * Default application keybindings.
18
+ */
19
+ export declare const DEFAULT_APP_KEYBINDINGS: Record<AppAction, KeyId | KeyId[]>;
20
+ /**
21
+ * All default keybindings (app + editor).
22
+ */
23
+ export declare const DEFAULT_KEYBINDINGS: Required<KeybindingsConfig>;
24
+ /**
25
+ * Manages all keybindings (app + editor).
26
+ */
27
+ export declare class KeybindingsManager {
28
+ private config;
29
+ private appActionToKeys;
30
+ private constructor();
31
+ /**
32
+ * Create from config file and set up editor keybindings.
33
+ */
34
+ static create(agentDir?: string): KeybindingsManager;
35
+ /**
36
+ * Create in-memory.
37
+ */
38
+ static inMemory(config?: KeybindingsConfig): KeybindingsManager;
39
+ private static loadFromFile;
40
+ private buildMaps;
41
+ /**
42
+ * Check if input matches an app action.
43
+ */
44
+ matches(data: string, action: AppAction): boolean;
45
+ /**
46
+ * Get keys bound to an app action.
47
+ */
48
+ getKeys(action: AppAction): KeyId[];
49
+ /**
50
+ * Get display string for an action.
51
+ */
52
+ getDisplayString(action: AppAction): string;
53
+ /**
54
+ * Get the full effective config.
55
+ */
56
+ getEffectiveConfig(): Required<KeybindingsConfig>;
57
+ }
58
+ export type { EditorAction, KeyId };
59
+ //# sourceMappingURL=keybindings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keybindings.d.ts","sourceRoot":"","sources":["../../src/core/keybindings.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,KAAK,YAAY,EAGjB,KAAK,KAAK,EAGV,MAAM,sBAAsB,CAAC;AAK9B;;GAEG;AACH,MAAM,MAAM,SAAS,GAClB,WAAW,GACX,OAAO,GACP,MAAM,GACN,SAAS,GACT,oBAAoB,GACpB,mBAAmB,GACnB,oBAAoB,GACpB,aAAa,GACb,aAAa,GACb,gBAAgB,GAChB,gBAAgB,GAChB,UAAU,CAAC;AAEd;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,YAAY,CAAC;AAEjD;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG;KAC9B,CAAC,IAAI,SAAS,CAAC,CAAC,EAAE,KAAK,GAAG,KAAK,EAAE;CAClC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAAC,SAAS,EAAE,KAAK,GAAG,KAAK,EAAE,CAatE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,iBAAiB,CAG3D,CAAC;AAsBF;;GAEG;AACH,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,eAAe,CAA0B;IAEjD,OAAO,eAIN;IAED;;OAEG;IACH,MAAM,CAAC,MAAM,CAAC,QAAQ,GAAE,MAAsB,GAAG,kBAAkB,CAelE;IAED;;OAEG;IACH,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAE,iBAAsB,GAAG,kBAAkB,CAElE;IAED,OAAO,CAAC,MAAM,CAAC,YAAY;IAS3B,OAAO,CAAC,SAAS;IAiBjB;;OAEG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAOhD;IAED;;OAEG;IACH,OAAO,CAAC,MAAM,EAAE,SAAS,GAAG,KAAK,EAAE,CAElC;IAED;;OAEG;IACH,gBAAgB,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAK1C;IAED;;OAEG;IACH,kBAAkB,IAAI,QAAQ,CAAC,iBAAiB,CAAC,CAQhD;CACD;AAGD,YAAY,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC","sourcesContent":["import {\n\tDEFAULT_EDITOR_KEYBINDINGS,\n\ttype EditorAction,\n\ttype EditorKeybindingsConfig,\n\tEditorKeybindingsManager,\n\ttype KeyId,\n\tmatchesKey,\n\tsetEditorKeybindings,\n} from \"@mariozechner/pi-tui\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.js\";\n\n/**\n * Application-level actions (coding agent specific).\n */\nexport type AppAction =\n\t| \"interrupt\"\n\t| \"clear\"\n\t| \"exit\"\n\t| \"suspend\"\n\t| \"cycleThinkingLevel\"\n\t| \"cycleModelForward\"\n\t| \"cycleModelBackward\"\n\t| \"selectModel\"\n\t| \"expandTools\"\n\t| \"toggleThinking\"\n\t| \"externalEditor\"\n\t| \"followUp\";\n\n/**\n * All configurable actions.\n */\nexport type KeyAction = AppAction | EditorAction;\n\n/**\n * Full keybindings configuration (app + editor actions).\n */\nexport type KeybindingsConfig = {\n\t[K in KeyAction]?: KeyId | KeyId[];\n};\n\n/**\n * Default application keybindings.\n */\nexport const DEFAULT_APP_KEYBINDINGS: Record<AppAction, KeyId | KeyId[]> = {\n\tinterrupt: \"escape\",\n\tclear: \"ctrl+c\",\n\texit: \"ctrl+d\",\n\tsuspend: \"ctrl+z\",\n\tcycleThinkingLevel: \"shift+tab\",\n\tcycleModelForward: \"ctrl+p\",\n\tcycleModelBackward: \"shift+ctrl+p\",\n\tselectModel: \"ctrl+l\",\n\texpandTools: \"ctrl+o\",\n\ttoggleThinking: \"ctrl+t\",\n\texternalEditor: \"ctrl+g\",\n\tfollowUp: \"alt+enter\",\n};\n\n/**\n * All default keybindings (app + editor).\n */\nexport const DEFAULT_KEYBINDINGS: Required<KeybindingsConfig> = {\n\t...DEFAULT_EDITOR_KEYBINDINGS,\n\t...DEFAULT_APP_KEYBINDINGS,\n};\n\n// App actions list for type checking\nconst APP_ACTIONS: AppAction[] = [\n\t\"interrupt\",\n\t\"clear\",\n\t\"exit\",\n\t\"suspend\",\n\t\"cycleThinkingLevel\",\n\t\"cycleModelForward\",\n\t\"cycleModelBackward\",\n\t\"selectModel\",\n\t\"expandTools\",\n\t\"toggleThinking\",\n\t\"externalEditor\",\n\t\"followUp\",\n];\n\nfunction isAppAction(action: string): action is AppAction {\n\treturn APP_ACTIONS.includes(action as AppAction);\n}\n\n/**\n * Manages all keybindings (app + editor).\n */\nexport class KeybindingsManager {\n\tprivate config: KeybindingsConfig;\n\tprivate appActionToKeys: Map<AppAction, KeyId[]>;\n\n\tprivate constructor(config: KeybindingsConfig) {\n\t\tthis.config = config;\n\t\tthis.appActionToKeys = new Map();\n\t\tthis.buildMaps();\n\t}\n\n\t/**\n\t * Create from config file and set up editor keybindings.\n\t */\n\tstatic create(agentDir: string = getAgentDir()): KeybindingsManager {\n\t\tconst configPath = join(agentDir, \"keybindings.json\");\n\t\tconst config = KeybindingsManager.loadFromFile(configPath);\n\t\tconst manager = new KeybindingsManager(config);\n\n\t\t// Set up editor keybindings globally\n\t\tconst editorConfig: EditorKeybindingsConfig = {};\n\t\tfor (const [action, keys] of Object.entries(config)) {\n\t\t\tif (!isAppAction(action)) {\n\t\t\t\teditorConfig[action as EditorAction] = keys;\n\t\t\t}\n\t\t}\n\t\tsetEditorKeybindings(new EditorKeybindingsManager(editorConfig));\n\n\t\treturn manager;\n\t}\n\n\t/**\n\t * Create in-memory.\n\t */\n\tstatic inMemory(config: KeybindingsConfig = {}): KeybindingsManager {\n\t\treturn new KeybindingsManager(config);\n\t}\n\n\tprivate static loadFromFile(path: string): KeybindingsConfig {\n\t\tif (!existsSync(path)) return {};\n\t\ttry {\n\t\t\treturn JSON.parse(readFileSync(path, \"utf-8\"));\n\t\t} catch {\n\t\t\treturn {};\n\t\t}\n\t}\n\n\tprivate buildMaps(): void {\n\t\tthis.appActionToKeys.clear();\n\n\t\t// Set defaults for app actions\n\t\tfor (const [action, keys] of Object.entries(DEFAULT_APP_KEYBINDINGS)) {\n\t\t\tconst keyArray = Array.isArray(keys) ? keys : [keys];\n\t\t\tthis.appActionToKeys.set(action as AppAction, [...keyArray]);\n\t\t}\n\n\t\t// Override with user config (app actions only)\n\t\tfor (const [action, keys] of Object.entries(this.config)) {\n\t\t\tif (keys === undefined || !isAppAction(action)) continue;\n\t\t\tconst keyArray = Array.isArray(keys) ? keys : [keys];\n\t\t\tthis.appActionToKeys.set(action, keyArray);\n\t\t}\n\t}\n\n\t/**\n\t * Check if input matches an app action.\n\t */\n\tmatches(data: string, action: AppAction): boolean {\n\t\tconst keys = this.appActionToKeys.get(action);\n\t\tif (!keys) return false;\n\t\tfor (const key of keys) {\n\t\t\tif (matchesKey(data, key)) return true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Get keys bound to an app action.\n\t */\n\tgetKeys(action: AppAction): KeyId[] {\n\t\treturn this.appActionToKeys.get(action) ?? [];\n\t}\n\n\t/**\n\t * Get display string for an action.\n\t */\n\tgetDisplayString(action: AppAction): string {\n\t\tconst keys = this.getKeys(action);\n\t\tif (keys.length === 0) return \"\";\n\t\tif (keys.length === 1) return keys[0]!;\n\t\treturn keys.join(\"/\");\n\t}\n\n\t/**\n\t * Get the full effective config.\n\t */\n\tgetEffectiveConfig(): Required<KeybindingsConfig> {\n\t\tconst result = { ...DEFAULT_KEYBINDINGS };\n\t\tfor (const [action, keys] of Object.entries(this.config)) {\n\t\t\tif (keys !== undefined) {\n\t\t\t\t(result as KeybindingsConfig)[action as KeyAction] = keys;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n}\n\n// Re-export for convenience\nexport type { EditorAction, KeyId };\n"]}
@@ -0,0 +1,149 @@
1
+ import { DEFAULT_EDITOR_KEYBINDINGS, EditorKeybindingsManager, matchesKey, setEditorKeybindings, } from "@mariozechner/pi-tui";
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { getAgentDir } from "../config.js";
5
+ /**
6
+ * Default application keybindings.
7
+ */
8
+ export const DEFAULT_APP_KEYBINDINGS = {
9
+ interrupt: "escape",
10
+ clear: "ctrl+c",
11
+ exit: "ctrl+d",
12
+ suspend: "ctrl+z",
13
+ cycleThinkingLevel: "shift+tab",
14
+ cycleModelForward: "ctrl+p",
15
+ cycleModelBackward: "shift+ctrl+p",
16
+ selectModel: "ctrl+l",
17
+ expandTools: "ctrl+o",
18
+ toggleThinking: "ctrl+t",
19
+ externalEditor: "ctrl+g",
20
+ followUp: "alt+enter",
21
+ };
22
+ /**
23
+ * All default keybindings (app + editor).
24
+ */
25
+ export const DEFAULT_KEYBINDINGS = {
26
+ ...DEFAULT_EDITOR_KEYBINDINGS,
27
+ ...DEFAULT_APP_KEYBINDINGS,
28
+ };
29
+ // App actions list for type checking
30
+ const APP_ACTIONS = [
31
+ "interrupt",
32
+ "clear",
33
+ "exit",
34
+ "suspend",
35
+ "cycleThinkingLevel",
36
+ "cycleModelForward",
37
+ "cycleModelBackward",
38
+ "selectModel",
39
+ "expandTools",
40
+ "toggleThinking",
41
+ "externalEditor",
42
+ "followUp",
43
+ ];
44
+ function isAppAction(action) {
45
+ return APP_ACTIONS.includes(action);
46
+ }
47
+ /**
48
+ * Manages all keybindings (app + editor).
49
+ */
50
+ export class KeybindingsManager {
51
+ config;
52
+ appActionToKeys;
53
+ constructor(config) {
54
+ this.config = config;
55
+ this.appActionToKeys = new Map();
56
+ this.buildMaps();
57
+ }
58
+ /**
59
+ * Create from config file and set up editor keybindings.
60
+ */
61
+ static create(agentDir = getAgentDir()) {
62
+ const configPath = join(agentDir, "keybindings.json");
63
+ const config = KeybindingsManager.loadFromFile(configPath);
64
+ const manager = new KeybindingsManager(config);
65
+ // Set up editor keybindings globally
66
+ const editorConfig = {};
67
+ for (const [action, keys] of Object.entries(config)) {
68
+ if (!isAppAction(action)) {
69
+ editorConfig[action] = keys;
70
+ }
71
+ }
72
+ setEditorKeybindings(new EditorKeybindingsManager(editorConfig));
73
+ return manager;
74
+ }
75
+ /**
76
+ * Create in-memory.
77
+ */
78
+ static inMemory(config = {}) {
79
+ return new KeybindingsManager(config);
80
+ }
81
+ static loadFromFile(path) {
82
+ if (!existsSync(path))
83
+ return {};
84
+ try {
85
+ return JSON.parse(readFileSync(path, "utf-8"));
86
+ }
87
+ catch {
88
+ return {};
89
+ }
90
+ }
91
+ buildMaps() {
92
+ this.appActionToKeys.clear();
93
+ // Set defaults for app actions
94
+ for (const [action, keys] of Object.entries(DEFAULT_APP_KEYBINDINGS)) {
95
+ const keyArray = Array.isArray(keys) ? keys : [keys];
96
+ this.appActionToKeys.set(action, [...keyArray]);
97
+ }
98
+ // Override with user config (app actions only)
99
+ for (const [action, keys] of Object.entries(this.config)) {
100
+ if (keys === undefined || !isAppAction(action))
101
+ continue;
102
+ const keyArray = Array.isArray(keys) ? keys : [keys];
103
+ this.appActionToKeys.set(action, keyArray);
104
+ }
105
+ }
106
+ /**
107
+ * Check if input matches an app action.
108
+ */
109
+ matches(data, action) {
110
+ const keys = this.appActionToKeys.get(action);
111
+ if (!keys)
112
+ return false;
113
+ for (const key of keys) {
114
+ if (matchesKey(data, key))
115
+ return true;
116
+ }
117
+ return false;
118
+ }
119
+ /**
120
+ * Get keys bound to an app action.
121
+ */
122
+ getKeys(action) {
123
+ return this.appActionToKeys.get(action) ?? [];
124
+ }
125
+ /**
126
+ * Get display string for an action.
127
+ */
128
+ getDisplayString(action) {
129
+ const keys = this.getKeys(action);
130
+ if (keys.length === 0)
131
+ return "";
132
+ if (keys.length === 1)
133
+ return keys[0];
134
+ return keys.join("/");
135
+ }
136
+ /**
137
+ * Get the full effective config.
138
+ */
139
+ getEffectiveConfig() {
140
+ const result = { ...DEFAULT_KEYBINDINGS };
141
+ for (const [action, keys] of Object.entries(this.config)) {
142
+ if (keys !== undefined) {
143
+ result[action] = keys;
144
+ }
145
+ }
146
+ return result;
147
+ }
148
+ }
149
+ //# sourceMappingURL=keybindings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keybindings.js","sourceRoot":"","sources":["../../src/core/keybindings.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\tDEFAULT_EDITOR_KEYBINDINGS,\n\ttype EditorAction,\n\ttype EditorKeybindingsConfig,\n\tEditorKeybindingsManager,\n\ttype KeyId,\n\tmatchesKey,\n\tsetEditorKeybindings,\n} from \"@mariozechner/pi-tui\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.js\";\n\n/**\n * Application-level actions (coding agent specific).\n */\nexport type AppAction =\n\t| \"interrupt\"\n\t| \"clear\"\n\t| \"exit\"\n\t| \"suspend\"\n\t| \"cycleThinkingLevel\"\n\t| \"cycleModelForward\"\n\t| \"cycleModelBackward\"\n\t| \"selectModel\"\n\t| \"expandTools\"\n\t| \"toggleThinking\"\n\t| \"externalEditor\"\n\t| \"followUp\";\n\n/**\n * All configurable actions.\n */\nexport type KeyAction = AppAction | EditorAction;\n\n/**\n * Full keybindings configuration (app + editor actions).\n */\nexport type KeybindingsConfig = {\n\t[K in KeyAction]?: KeyId | KeyId[];\n};\n\n/**\n * Default application keybindings.\n */\nexport const DEFAULT_APP_KEYBINDINGS: Record<AppAction, KeyId | KeyId[]> = {\n\tinterrupt: \"escape\",\n\tclear: \"ctrl+c\",\n\texit: \"ctrl+d\",\n\tsuspend: \"ctrl+z\",\n\tcycleThinkingLevel: \"shift+tab\",\n\tcycleModelForward: \"ctrl+p\",\n\tcycleModelBackward: \"shift+ctrl+p\",\n\tselectModel: \"ctrl+l\",\n\texpandTools: \"ctrl+o\",\n\ttoggleThinking: \"ctrl+t\",\n\texternalEditor: \"ctrl+g\",\n\tfollowUp: \"alt+enter\",\n};\n\n/**\n * All default keybindings (app + editor).\n */\nexport const DEFAULT_KEYBINDINGS: Required<KeybindingsConfig> = {\n\t...DEFAULT_EDITOR_KEYBINDINGS,\n\t...DEFAULT_APP_KEYBINDINGS,\n};\n\n// App actions list for type checking\nconst APP_ACTIONS: AppAction[] = [\n\t\"interrupt\",\n\t\"clear\",\n\t\"exit\",\n\t\"suspend\",\n\t\"cycleThinkingLevel\",\n\t\"cycleModelForward\",\n\t\"cycleModelBackward\",\n\t\"selectModel\",\n\t\"expandTools\",\n\t\"toggleThinking\",\n\t\"externalEditor\",\n\t\"followUp\",\n];\n\nfunction isAppAction(action: string): action is AppAction {\n\treturn APP_ACTIONS.includes(action as AppAction);\n}\n\n/**\n * Manages all keybindings (app + editor).\n */\nexport class KeybindingsManager {\n\tprivate config: KeybindingsConfig;\n\tprivate appActionToKeys: Map<AppAction, KeyId[]>;\n\n\tprivate constructor(config: KeybindingsConfig) {\n\t\tthis.config = config;\n\t\tthis.appActionToKeys = new Map();\n\t\tthis.buildMaps();\n\t}\n\n\t/**\n\t * Create from config file and set up editor keybindings.\n\t */\n\tstatic create(agentDir: string = getAgentDir()): KeybindingsManager {\n\t\tconst configPath = join(agentDir, \"keybindings.json\");\n\t\tconst config = KeybindingsManager.loadFromFile(configPath);\n\t\tconst manager = new KeybindingsManager(config);\n\n\t\t// Set up editor keybindings globally\n\t\tconst editorConfig: EditorKeybindingsConfig = {};\n\t\tfor (const [action, keys] of Object.entries(config)) {\n\t\t\tif (!isAppAction(action)) {\n\t\t\t\teditorConfig[action as EditorAction] = keys;\n\t\t\t}\n\t\t}\n\t\tsetEditorKeybindings(new EditorKeybindingsManager(editorConfig));\n\n\t\treturn manager;\n\t}\n\n\t/**\n\t * Create in-memory.\n\t */\n\tstatic inMemory(config: KeybindingsConfig = {}): KeybindingsManager {\n\t\treturn new KeybindingsManager(config);\n\t}\n\n\tprivate static loadFromFile(path: string): KeybindingsConfig {\n\t\tif (!existsSync(path)) return {};\n\t\ttry {\n\t\t\treturn JSON.parse(readFileSync(path, \"utf-8\"));\n\t\t} catch {\n\t\t\treturn {};\n\t\t}\n\t}\n\n\tprivate buildMaps(): void {\n\t\tthis.appActionToKeys.clear();\n\n\t\t// Set defaults for app actions\n\t\tfor (const [action, keys] of Object.entries(DEFAULT_APP_KEYBINDINGS)) {\n\t\t\tconst keyArray = Array.isArray(keys) ? keys : [keys];\n\t\t\tthis.appActionToKeys.set(action as AppAction, [...keyArray]);\n\t\t}\n\n\t\t// Override with user config (app actions only)\n\t\tfor (const [action, keys] of Object.entries(this.config)) {\n\t\t\tif (keys === undefined || !isAppAction(action)) continue;\n\t\t\tconst keyArray = Array.isArray(keys) ? keys : [keys];\n\t\t\tthis.appActionToKeys.set(action, keyArray);\n\t\t}\n\t}\n\n\t/**\n\t * Check if input matches an app action.\n\t */\n\tmatches(data: string, action: AppAction): boolean {\n\t\tconst keys = this.appActionToKeys.get(action);\n\t\tif (!keys) return false;\n\t\tfor (const key of keys) {\n\t\t\tif (matchesKey(data, key)) return true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Get keys bound to an app action.\n\t */\n\tgetKeys(action: AppAction): KeyId[] {\n\t\treturn this.appActionToKeys.get(action) ?? [];\n\t}\n\n\t/**\n\t * Get display string for an action.\n\t */\n\tgetDisplayString(action: AppAction): string {\n\t\tconst keys = this.getKeys(action);\n\t\tif (keys.length === 0) return \"\";\n\t\tif (keys.length === 1) return keys[0]!;\n\t\treturn keys.join(\"/\");\n\t}\n\n\t/**\n\t * Get the full effective config.\n\t */\n\tgetEffectiveConfig(): Required<KeybindingsConfig> {\n\t\tconst result = { ...DEFAULT_KEYBINDINGS };\n\t\tfor (const [action, keys] of Object.entries(this.config)) {\n\t\t\tif (keys !== undefined) {\n\t\t\t\t(result as KeybindingsConfig)[action as KeyAction] = keys;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n}\n\n// Re-export for convenience\nexport type { EditorAction, KeyId };\n"]}
@@ -34,6 +34,7 @@ import { AgentSession } from "./agent-session.js";
34
34
  import { AuthStorage } from "./auth-storage.js";
35
35
  import { type CustomToolsLoadResult } from "./custom-tools/index.js";
36
36
  import type { CustomTool } from "./custom-tools/types.js";
37
+ import { type LoadedHook } from "./hooks/index.js";
37
38
  import type { HookFactory } from "./hooks/types.js";
38
39
  import { ModelRegistry } from "./model-registry.js";
39
40
  import { SessionManager } from "./session-manager.js";
@@ -77,6 +78,8 @@ export interface CreateAgentSessionOptions {
77
78
  }>;
78
79
  /** Additional hook paths to load (merged with discovery). */
79
80
  additionalHookPaths?: string[];
81
+ /** Pre-loaded hooks (skips loading, used when hooks were loaded early for CLI flags). */
82
+ preloadedHooks?: LoadedHook[];
80
83
  /** Skills. Default: discovered from multiple locations */
81
84
  skills?: Skill[];
82
85
  /** Context files (AGENTS.md content). Default: discovered walking up from cwd */
@@ -1 +1 @@
1
- {"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/core/sdk.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SDK for programmatic usage of AgentSession.\n *\n * Provides a factory function and discovery helpers that allow full control\n * over agent configuration, or sensible defaults that match CLI behavior.\n *\n * @example\n * ```typescript\n * // Minimal - everything auto-discovered\n * const session = await createAgentSession();\n *\n * // With custom hooks\n * const session = await createAgentSession({\n * hooks: [\n * ...await discoverHooks(),\n * { factory: myHookFactory },\n * ],\n * });\n *\n * // Full control\n * const session = await createAgentSession({\n * model: myModel,\n * getApiKey: async () => process.env.MY_KEY,\n * tools: [readTool, bashTool],\n * hooks: [],\n * skills: [],\n * sessionFile: false,\n * });\n * ```\n */\n\nimport { Agent, type ThinkingLevel } from \"@mariozechner/pi-agent-core\";\nimport type { Model } from \"@mariozechner/pi-ai\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.js\";\nimport { AgentSession } from \"./agent-session.js\";\nimport { AuthStorage } from \"./auth-storage.js\";\nimport {\n\ttype CustomToolsLoadResult,\n\tdiscoverAndLoadCustomTools,\n\ttype LoadedCustomTool,\n\twrapCustomTools,\n} from \"./custom-tools/index.js\";\nimport type { CustomTool } from \"./custom-tools/types.js\";\nimport { discoverAndLoadHooks, HookRunner, type LoadedHook, wrapToolsWithHooks } from \"./hooks/index.js\";\nimport type { HookFactory } from \"./hooks/types.js\";\nimport { convertToLlm } from \"./messages.js\";\nimport { ModelRegistry } from \"./model-registry.js\";\nimport { SessionManager } from \"./session-manager.js\";\nimport { type Settings, SettingsManager, type SkillsSettings } from \"./settings-manager.js\";\nimport { loadSkills as loadSkillsInternal, type Skill } from \"./skills.js\";\nimport { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal } from \"./slash-commands.js\";\nimport {\n\tbuildSystemPrompt as buildSystemPromptInternal,\n\tloadProjectContextFiles as loadContextFilesInternal,\n} from \"./system-prompt.js\";\nimport { time } from \"./timings.js\";\nimport {\n\tallTools,\n\tbashTool,\n\tcodingTools,\n\tcreateBashTool,\n\tcreateCodingTools,\n\tcreateEditTool,\n\tcreateFindTool,\n\tcreateGrepTool,\n\tcreateLsTool,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateWriteTool,\n\teditTool,\n\tfindTool,\n\tgrepTool,\n\tlsTool,\n\treadOnlyTools,\n\treadTool,\n\ttype Tool,\n\twriteTool,\n} from \"./tools/index.js\";\n\n// Types\n\nexport interface CreateAgentSessionOptions {\n\t/** Working directory for project-local discovery. Default: process.cwd() */\n\tcwd?: string;\n\t/** Global config directory. Default: ~/.pi/agent */\n\tagentDir?: string;\n\n\t/** Auth storage for credentials. Default: discoverAuthStorage(agentDir) */\n\tauthStorage?: AuthStorage;\n\t/** Model registry. Default: discoverModels(authStorage, agentDir) */\n\tmodelRegistry?: ModelRegistry;\n\n\t/** Model to use. Default: from settings, else first available */\n\tmodel?: Model<any>;\n\t/** Thinking level. Default: from settings, else 'off' (clamped to model capabilities) */\n\tthinkingLevel?: ThinkingLevel;\n\t/** Models available for cycling (Ctrl+P in interactive mode) */\n\tscopedModels?: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }>;\n\n\t/** System prompt. String replaces default, function receives default and returns final. */\n\tsystemPrompt?: string | ((defaultPrompt: string) => string);\n\n\t/** Built-in tools to use. Default: codingTools [read, bash, edit, write] */\n\ttools?: Tool[];\n\t/** Custom tools (replaces discovery). */\n\tcustomTools?: Array<{ path?: string; tool: CustomTool }>;\n\t/** Additional custom tool paths to load (merged with discovery). */\n\tadditionalCustomToolPaths?: string[];\n\n\t/** Hooks (replaces discovery). */\n\thooks?: Array<{ path?: string; factory: HookFactory }>;\n\t/** Additional hook paths to load (merged with discovery). */\n\tadditionalHookPaths?: string[];\n\n\t/** Skills. Default: discovered from multiple locations */\n\tskills?: Skill[];\n\t/** Context files (AGENTS.md content). Default: discovered walking up from cwd */\n\tcontextFiles?: Array<{ path: string; content: string }>;\n\t/** Slash commands. Default: discovered from cwd/.pi/commands/ + agentDir/commands/ */\n\tslashCommands?: FileSlashCommand[];\n\n\t/** Session manager. Default: SessionManager.create(cwd) */\n\tsessionManager?: SessionManager;\n\n\t/** Settings manager. Default: SettingsManager.create(cwd, agentDir) */\n\tsettingsManager?: SettingsManager;\n}\n\n/** Result from createAgentSession */\nexport interface CreateAgentSessionResult {\n\t/** The created session */\n\tsession: AgentSession;\n\t/** Custom tools result (for UI context setup in interactive mode) */\n\tcustomToolsResult: CustomToolsLoadResult;\n\t/** Warning if session was restored with a different model than saved */\n\tmodelFallbackMessage?: string;\n}\n\n// Re-exports\n\nexport type { CustomTool } from \"./custom-tools/types.js\";\nexport type { HookAPI, HookCommandContext, HookContext, HookFactory } from \"./hooks/types.js\";\nexport type { Settings, SkillsSettings } from \"./settings-manager.js\";\nexport type { Skill } from \"./skills.js\";\nexport type { FileSlashCommand } from \"./slash-commands.js\";\nexport type { Tool } from \"./tools/index.js\";\n\nexport {\n\t// Pre-built tools (use process.cwd())\n\treadTool,\n\tbashTool,\n\teditTool,\n\twriteTool,\n\tgrepTool,\n\tfindTool,\n\tlsTool,\n\tcodingTools,\n\treadOnlyTools,\n\tallTools as allBuiltInTools,\n\t// Tool factories (for custom cwd)\n\tcreateCodingTools,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateBashTool,\n\tcreateEditTool,\n\tcreateWriteTool,\n\tcreateGrepTool,\n\tcreateFindTool,\n\tcreateLsTool,\n};\n\n// Helper Functions\n\nfunction getDefaultAgentDir(): string {\n\treturn getAgentDir();\n}\n\n// Discovery Functions\n\n/**\n * Create an AuthStorage instance for the given agent directory.\n */\nexport function discoverAuthStorage(agentDir: string = getDefaultAgentDir()): AuthStorage {\n\treturn new AuthStorage(join(agentDir, \"auth.json\"));\n}\n\n/**\n * Create a ModelRegistry for the given agent directory.\n */\nexport function discoverModels(authStorage: AuthStorage, agentDir: string = getDefaultAgentDir()): ModelRegistry {\n\treturn new ModelRegistry(authStorage, join(agentDir, \"models.json\"));\n}\n\n/**\n * Discover hooks from cwd and agentDir.\n */\nexport async function discoverHooks(\n\tcwd?: string,\n\tagentDir?: string,\n): Promise<Array<{ path: string; factory: HookFactory }>> {\n\tconst resolvedCwd = cwd ?? process.cwd();\n\tconst resolvedAgentDir = agentDir ?? getDefaultAgentDir();\n\n\tconst { hooks, errors } = await discoverAndLoadHooks([], resolvedCwd, resolvedAgentDir);\n\n\t// Log errors but don't fail\n\tfor (const { path, error } of errors) {\n\t\tconsole.error(`Failed to load hook \"${path}\": ${error}`);\n\t}\n\n\treturn hooks.map((h) => ({\n\t\tpath: h.path,\n\t\tfactory: createFactoryFromLoadedHook(h),\n\t}));\n}\n\n/**\n * Discover custom tools from cwd and agentDir.\n */\nexport async function discoverCustomTools(\n\tcwd?: string,\n\tagentDir?: string,\n): Promise<Array<{ path: string; tool: CustomTool }>> {\n\tconst resolvedCwd = cwd ?? process.cwd();\n\tconst resolvedAgentDir = agentDir ?? getDefaultAgentDir();\n\n\tconst { tools, errors } = await discoverAndLoadCustomTools([], resolvedCwd, Object.keys(allTools), resolvedAgentDir);\n\n\t// Log errors but don't fail\n\tfor (const { path, error } of errors) {\n\t\tconsole.error(`Failed to load custom tool \"${path}\": ${error}`);\n\t}\n\n\treturn tools.map((t) => ({\n\t\tpath: t.path,\n\t\ttool: t.tool,\n\t}));\n}\n\n/**\n * Discover skills from cwd and agentDir.\n */\nexport function discoverSkills(cwd?: string, agentDir?: string, settings?: SkillsSettings): Skill[] {\n\tconst { skills } = loadSkillsInternal({\n\t\t...settings,\n\t\tcwd: cwd ?? process.cwd(),\n\t\tagentDir: agentDir ?? getDefaultAgentDir(),\n\t});\n\treturn skills;\n}\n\n/**\n * Discover context files (AGENTS.md) walking up from cwd.\n */\nexport function discoverContextFiles(cwd?: string, agentDir?: string): Array<{ path: string; content: string }> {\n\treturn loadContextFilesInternal({\n\t\tcwd: cwd ?? process.cwd(),\n\t\tagentDir: agentDir ?? getDefaultAgentDir(),\n\t});\n}\n\n/**\n * Discover slash commands from cwd and agentDir.\n */\nexport function discoverSlashCommands(cwd?: string, agentDir?: string): FileSlashCommand[] {\n\treturn loadSlashCommandsInternal({\n\t\tcwd: cwd ?? process.cwd(),\n\t\tagentDir: agentDir ?? getDefaultAgentDir(),\n\t});\n}\n\n// API Key Helpers\n\n// System Prompt\n\nexport interface BuildSystemPromptOptions {\n\ttools?: Tool[];\n\tskills?: Skill[];\n\tcontextFiles?: Array<{ path: string; content: string }>;\n\tcwd?: string;\n\tappendPrompt?: string;\n}\n\n/**\n * Build the default system prompt.\n */\nexport function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {\n\treturn buildSystemPromptInternal({\n\t\tcwd: options.cwd,\n\t\tskills: options.skills,\n\t\tcontextFiles: options.contextFiles,\n\t\tappendSystemPrompt: options.appendPrompt,\n\t});\n}\n\n// Settings\n\n/**\n * Load settings from agentDir/settings.json merged with cwd/.pi/settings.json.\n */\nexport function loadSettings(cwd?: string, agentDir?: string): Settings {\n\tconst manager = SettingsManager.create(cwd ?? process.cwd(), agentDir ?? getDefaultAgentDir());\n\treturn {\n\t\tdefaultProvider: manager.getDefaultProvider(),\n\t\tdefaultModel: manager.getDefaultModel(),\n\t\tdefaultThinkingLevel: manager.getDefaultThinkingLevel(),\n\t\tsteeringMode: manager.getSteeringMode(),\n\t\tfollowUpMode: manager.getFollowUpMode(),\n\t\ttheme: manager.getTheme(),\n\t\tcompaction: manager.getCompactionSettings(),\n\t\tretry: manager.getRetrySettings(),\n\t\thideThinkingBlock: manager.getHideThinkingBlock(),\n\t\tshellPath: manager.getShellPath(),\n\t\tcollapseChangelog: manager.getCollapseChangelog(),\n\t\thooks: manager.getHookPaths(),\n\t\tcustomTools: manager.getCustomToolPaths(),\n\t\tskills: manager.getSkillsSettings(),\n\t\tterminal: { showImages: manager.getShowImages() },\n\t};\n}\n\n// Internal Helpers\n\n/**\n * Create a HookFactory from a LoadedHook.\n * This allows mixing discovered hooks with inline hooks.\n */\nfunction createFactoryFromLoadedHook(loaded: LoadedHook): HookFactory {\n\treturn (api) => {\n\t\tfor (const [eventType, handlers] of loaded.handlers) {\n\t\t\tfor (const handler of handlers) {\n\t\t\t\tapi.on(eventType as any, handler as any);\n\t\t\t}\n\t\t}\n\t};\n}\n\n/**\n * Convert hook definitions to LoadedHooks for the HookRunner.\n */\nfunction createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; factory: HookFactory }>): LoadedHook[] {\n\treturn definitions.map((def) => {\n\t\tconst handlers = new Map<string, Array<(...args: unknown[]) => Promise<unknown>>>();\n\t\tconst messageRenderers = new Map<string, any>();\n\t\tconst commands = new Map<string, any>();\n\t\tlet sendMessageHandler: (\n\t\t\tmessage: any,\n\t\t\toptions?: { triggerTurn?: boolean; deliverAs?: \"steer\" | \"followUp\" },\n\t\t) => void = () => {};\n\t\tlet appendEntryHandler: (customType: string, data?: any) => void = () => {};\n\t\tlet newSessionHandler: (options?: any) => Promise<{ cancelled: boolean }> = async () => ({ cancelled: false });\n\t\tlet branchHandler: (entryId: string) => Promise<{ cancelled: boolean }> = async () => ({ cancelled: false });\n\t\tlet navigateTreeHandler: (targetId: string, options?: any) => Promise<{ cancelled: boolean }> = async () => ({\n\t\t\tcancelled: false,\n\t\t});\n\n\t\tconst api = {\n\t\t\ton: (event: string, handler: (...args: unknown[]) => Promise<unknown>) => {\n\t\t\t\tconst list = handlers.get(event) ?? [];\n\t\t\t\tlist.push(handler);\n\t\t\t\thandlers.set(event, list);\n\t\t\t},\n\t\t\tsendMessage: (message: any, options?: { triggerTurn?: boolean; deliverAs?: \"steer\" | \"followUp\" }) => {\n\t\t\t\tsendMessageHandler(message, options);\n\t\t\t},\n\t\t\tappendEntry: (customType: string, data?: any) => {\n\t\t\t\tappendEntryHandler(customType, data);\n\t\t\t},\n\t\t\tregisterMessageRenderer: (customType: string, renderer: any) => {\n\t\t\t\tmessageRenderers.set(customType, renderer);\n\t\t\t},\n\t\t\tregisterCommand: (name: string, options: any) => {\n\t\t\t\tcommands.set(name, { name, ...options });\n\t\t\t},\n\t\t\tnewSession: (options?: any) => newSessionHandler(options),\n\t\t\tbranch: (entryId: string) => branchHandler(entryId),\n\t\t\tnavigateTree: (targetId: string, options?: any) => navigateTreeHandler(targetId, options),\n\t\t};\n\n\t\tdef.factory(api as any);\n\n\t\treturn {\n\t\t\tpath: def.path ?? \"<inline>\",\n\t\t\tresolvedPath: def.path ?? \"<inline>\",\n\t\t\thandlers,\n\t\t\tmessageRenderers,\n\t\t\tcommands,\n\t\t\tsetSendMessageHandler: (\n\t\t\t\thandler: (message: any, options?: { triggerTurn?: boolean; deliverAs?: \"steer\" | \"followUp\" }) => void,\n\t\t\t) => {\n\t\t\t\tsendMessageHandler = handler;\n\t\t\t},\n\t\t\tsetAppendEntryHandler: (handler: (customType: string, data?: any) => void) => {\n\t\t\t\tappendEntryHandler = handler;\n\t\t\t},\n\t\t\tsetNewSessionHandler: (handler: (options?: any) => Promise<{ cancelled: boolean }>) => {\n\t\t\t\tnewSessionHandler = handler;\n\t\t\t},\n\t\t\tsetBranchHandler: (handler: (entryId: string) => Promise<{ cancelled: boolean }>) => {\n\t\t\t\tbranchHandler = handler;\n\t\t\t},\n\t\t\tsetNavigateTreeHandler: (handler: (targetId: string, options?: any) => Promise<{ cancelled: boolean }>) => {\n\t\t\t\tnavigateTreeHandler = handler;\n\t\t\t},\n\t\t};\n\t});\n}\n\n// Factory\n\n/**\n * Create an AgentSession with the specified options.\n *\n * @example\n * ```typescript\n * // Minimal - uses defaults\n * const { session } = await createAgentSession();\n *\n * // With explicit model\n * import { getModel } from '@mariozechner/pi-ai';\n * const { session } = await createAgentSession({\n * model: getModel('anthropic', 'claude-opus-4-5'),\n * thinkingLevel: 'high',\n * });\n *\n * // Continue previous session\n * const { session, modelFallbackMessage } = await createAgentSession({\n * continueSession: true,\n * });\n *\n * // Full control\n * const { session } = await createAgentSession({\n * model: myModel,\n * getApiKey: async () => process.env.MY_KEY,\n * systemPrompt: 'You are helpful.',\n * tools: [readTool, bashTool],\n * hooks: [],\n * skills: [],\n * sessionManager: SessionManager.inMemory(),\n * });\n * ```\n */\nexport async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {\n\tconst cwd = options.cwd ?? process.cwd();\n\tconst agentDir = options.agentDir ?? getDefaultAgentDir();\n\n\t// Use provided or create AuthStorage and ModelRegistry\n\tconst authStorage = options.authStorage ?? discoverAuthStorage(agentDir);\n\tconst modelRegistry = options.modelRegistry ?? discoverModels(authStorage, agentDir);\n\ttime(\"discoverModels\");\n\n\tconst settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);\n\ttime(\"settingsManager\");\n\tconst sessionManager = options.sessionManager ?? SessionManager.create(cwd);\n\ttime(\"sessionManager\");\n\n\t// Check if session has existing data to restore\n\tconst existingSession = sessionManager.buildSessionContext();\n\ttime(\"loadSession\");\n\tconst hasExistingSession = existingSession.messages.length > 0;\n\n\tlet model = options.model;\n\tlet modelFallbackMessage: string | undefined;\n\n\t// If session has data, try to restore model from it\n\tif (!model && hasExistingSession && existingSession.model) {\n\t\tconst restoredModel = modelRegistry.find(existingSession.model.provider, existingSession.model.modelId);\n\t\tif (restoredModel && (await modelRegistry.getApiKey(restoredModel))) {\n\t\t\tmodel = restoredModel;\n\t\t}\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`;\n\t\t}\n\t}\n\n\t// If still no model, try settings default\n\tif (!model) {\n\t\tconst defaultProvider = settingsManager.getDefaultProvider();\n\t\tconst defaultModelId = settingsManager.getDefaultModel();\n\t\tif (defaultProvider && defaultModelId) {\n\t\t\tconst settingsModel = modelRegistry.find(defaultProvider, defaultModelId);\n\t\t\tif (settingsModel && (await modelRegistry.getApiKey(settingsModel))) {\n\t\t\t\tmodel = settingsModel;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Fall back to first available model with a valid API key\n\tif (!model) {\n\t\tfor (const m of modelRegistry.getAll()) {\n\t\t\tif (await modelRegistry.getApiKey(m)) {\n\t\t\t\tmodel = m;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\ttime(\"findAvailableModel\");\n\t\tif (model) {\n\t\t\tif (modelFallbackMessage) {\n\t\t\t\tmodelFallbackMessage += `. Using ${model.provider}/${model.id}`;\n\t\t\t}\n\t\t} else {\n\t\t\t// No models available - set message so user knows to /login or configure keys\n\t\t\tmodelFallbackMessage = \"No models available. Use /login or set an API key environment variable.\";\n\t\t}\n\t}\n\n\tlet thinkingLevel = options.thinkingLevel;\n\n\t// If session has data, restore thinking level from it\n\tif (thinkingLevel === undefined && hasExistingSession) {\n\t\tthinkingLevel = existingSession.thinkingLevel as ThinkingLevel;\n\t}\n\n\t// Fall back to settings default\n\tif (thinkingLevel === undefined) {\n\t\tthinkingLevel = settingsManager.getDefaultThinkingLevel() ?? \"off\";\n\t}\n\n\t// Clamp to model capabilities\n\tif (!model || !model.reasoning) {\n\t\tthinkingLevel = \"off\";\n\t}\n\n\tconst skills = options.skills ?? discoverSkills(cwd, agentDir, settingsManager.getSkillsSettings());\n\ttime(\"discoverSkills\");\n\n\tconst contextFiles = options.contextFiles ?? discoverContextFiles(cwd, agentDir);\n\ttime(\"discoverContextFiles\");\n\n\tconst autoResizeImages = settingsManager.getImageAutoResize();\n\tconst builtInTools = options.tools ?? createCodingTools(cwd, { read: { autoResizeImages } });\n\ttime(\"createCodingTools\");\n\n\tlet customToolsResult: CustomToolsLoadResult;\n\tif (options.customTools !== undefined) {\n\t\t// Use provided custom tools\n\t\tconst loadedTools: LoadedCustomTool[] = options.customTools.map((ct) => ({\n\t\t\tpath: ct.path ?? \"<inline>\",\n\t\t\tresolvedPath: ct.path ?? \"<inline>\",\n\t\t\ttool: ct.tool,\n\t\t}));\n\t\tcustomToolsResult = {\n\t\t\ttools: loadedTools,\n\t\t\terrors: [],\n\t\t\tsetUIContext: () => {},\n\t\t};\n\t} else {\n\t\t// Discover custom tools, merging with additional paths\n\t\tconst configuredPaths = [...settingsManager.getCustomToolPaths(), ...(options.additionalCustomToolPaths ?? [])];\n\t\tcustomToolsResult = await discoverAndLoadCustomTools(configuredPaths, cwd, Object.keys(allTools), agentDir);\n\t\ttime(\"discoverAndLoadCustomTools\");\n\t\tfor (const { path, error } of customToolsResult.errors) {\n\t\t\tconsole.error(`Failed to load custom tool \"${path}\": ${error}`);\n\t\t}\n\t}\n\n\tlet hookRunner: HookRunner | undefined;\n\tif (options.hooks !== undefined) {\n\t\tif (options.hooks.length > 0) {\n\t\t\tconst loadedHooks = createLoadedHooksFromDefinitions(options.hooks);\n\t\t\thookRunner = new HookRunner(loadedHooks, cwd, sessionManager, modelRegistry);\n\t\t}\n\t} else {\n\t\t// Discover hooks, merging with additional paths\n\t\tconst configuredPaths = [...settingsManager.getHookPaths(), ...(options.additionalHookPaths ?? [])];\n\t\tconst { hooks, errors } = await discoverAndLoadHooks(configuredPaths, cwd, agentDir);\n\t\ttime(\"discoverAndLoadHooks\");\n\t\tfor (const { path, error } of errors) {\n\t\t\tconsole.error(`Failed to load hook \"${path}\": ${error}`);\n\t\t}\n\t\tif (hooks.length > 0) {\n\t\t\thookRunner = new HookRunner(hooks, cwd, sessionManager, modelRegistry);\n\t\t}\n\t}\n\n\t// Wrap custom tools with context getter (agent/session assigned below, accessed at execute time)\n\tlet agent: Agent;\n\tlet session: AgentSession;\n\tconst wrappedCustomTools = wrapCustomTools(customToolsResult.tools, () => ({\n\t\tsessionManager,\n\t\tmodelRegistry,\n\t\tmodel: agent.state.model,\n\t\tisIdle: () => !session.isStreaming,\n\t\thasPendingMessages: () => session.pendingMessageCount > 0,\n\t\tabort: () => {\n\t\t\tsession.abort();\n\t\t},\n\t}));\n\n\tlet allToolsArray: Tool[] = [...builtInTools, ...wrappedCustomTools];\n\ttime(\"combineTools\");\n\tif (hookRunner) {\n\t\tallToolsArray = wrapToolsWithHooks(allToolsArray, hookRunner) as Tool[];\n\t}\n\n\tlet systemPrompt: string;\n\tconst defaultPrompt = buildSystemPromptInternal({\n\t\tcwd,\n\t\tagentDir,\n\t\tskills,\n\t\tcontextFiles,\n\t});\n\ttime(\"buildSystemPrompt\");\n\n\tif (options.systemPrompt === undefined) {\n\t\tsystemPrompt = defaultPrompt;\n\t} else if (typeof options.systemPrompt === \"string\") {\n\t\tsystemPrompt = buildSystemPromptInternal({\n\t\t\tcwd,\n\t\t\tagentDir,\n\t\t\tskills,\n\t\t\tcontextFiles,\n\t\t\tcustomPrompt: options.systemPrompt,\n\t\t});\n\t} else {\n\t\tsystemPrompt = options.systemPrompt(defaultPrompt);\n\t}\n\n\tconst slashCommands = options.slashCommands ?? discoverSlashCommands(cwd, agentDir);\n\ttime(\"discoverSlashCommands\");\n\n\tagent = new Agent({\n\t\tinitialState: {\n\t\t\tsystemPrompt,\n\t\t\tmodel,\n\t\t\tthinkingLevel,\n\t\t\ttools: allToolsArray,\n\t\t},\n\t\tconvertToLlm,\n\t\ttransformContext: hookRunner\n\t\t\t? async (messages) => {\n\t\t\t\t\treturn hookRunner.emitContext(messages);\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tsteeringMode: settingsManager.getSteeringMode(),\n\t\tfollowUpMode: settingsManager.getFollowUpMode(),\n\t\tgetApiKey: async () => {\n\t\t\tconst currentModel = agent.state.model;\n\t\t\tif (!currentModel) {\n\t\t\t\tthrow new Error(\"No model selected\");\n\t\t\t}\n\t\t\tconst key = await modelRegistry.getApiKey(currentModel);\n\t\t\tif (!key) {\n\t\t\t\tthrow new Error(`No API key found for provider \"${currentModel.provider}\"`);\n\t\t\t}\n\t\t\treturn key;\n\t\t},\n\t});\n\ttime(\"createAgent\");\n\n\t// Restore messages if session has existing data\n\tif (hasExistingSession) {\n\t\tagent.replaceMessages(existingSession.messages);\n\t} else {\n\t\t// Save initial model and thinking level for new sessions so they can be restored on resume\n\t\tif (model) {\n\t\t\tsessionManager.appendModelChange(model.provider, model.id);\n\t\t}\n\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t}\n\n\tsession = new AgentSession({\n\t\tagent,\n\t\tsessionManager,\n\t\tsettingsManager,\n\t\tscopedModels: options.scopedModels,\n\t\tfileCommands: slashCommands,\n\t\thookRunner,\n\t\tcustomTools: customToolsResult.tools,\n\t\tskillsSettings: settingsManager.getSkillsSettings(),\n\t\tmodelRegistry,\n\t});\n\ttime(\"createAgentSession\");\n\n\treturn {\n\t\tsession,\n\t\tcustomToolsResult,\n\t\tmodelFallbackMessage,\n\t};\n}\n"]}
1
+ {"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/core/sdk.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SDK for programmatic usage of AgentSession.\n *\n * Provides a factory function and discovery helpers that allow full control\n * over agent configuration, or sensible defaults that match CLI behavior.\n *\n * @example\n * ```typescript\n * // Minimal - everything auto-discovered\n * const session = await createAgentSession();\n *\n * // With custom hooks\n * const session = await createAgentSession({\n * hooks: [\n * ...await discoverHooks(),\n * { factory: myHookFactory },\n * ],\n * });\n *\n * // Full control\n * const session = await createAgentSession({\n * model: myModel,\n * getApiKey: async () => process.env.MY_KEY,\n * tools: [readTool, bashTool],\n * hooks: [],\n * skills: [],\n * sessionFile: false,\n * });\n * ```\n */\n\nimport { Agent, type AgentTool, type ThinkingLevel } from \"@mariozechner/pi-agent-core\";\nimport type { Model } from \"@mariozechner/pi-ai\";\nimport type { KeyId } from \"@mariozechner/pi-tui\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.js\";\nimport { AgentSession } from \"./agent-session.js\";\nimport { AuthStorage } from \"./auth-storage.js\";\nimport {\n\ttype CustomToolsLoadResult,\n\tdiscoverAndLoadCustomTools,\n\ttype LoadedCustomTool,\n\twrapCustomTools,\n} from \"./custom-tools/index.js\";\nimport type { CustomTool } from \"./custom-tools/types.js\";\nimport { discoverAndLoadHooks, HookRunner, type LoadedHook, wrapToolsWithHooks } from \"./hooks/index.js\";\nimport type { HookFactory } from \"./hooks/types.js\";\nimport { convertToLlm } from \"./messages.js\";\nimport { ModelRegistry } from \"./model-registry.js\";\nimport { SessionManager } from \"./session-manager.js\";\nimport { type Settings, SettingsManager, type SkillsSettings } from \"./settings-manager.js\";\nimport { loadSkills as loadSkillsInternal, type Skill } from \"./skills.js\";\nimport { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal } from \"./slash-commands.js\";\nimport {\n\tbuildSystemPrompt as buildSystemPromptInternal,\n\tloadProjectContextFiles as loadContextFilesInternal,\n} from \"./system-prompt.js\";\nimport { time } from \"./timings.js\";\nimport {\n\tallTools,\n\tbashTool,\n\tcodingTools,\n\tcreateAllTools,\n\tcreateBashTool,\n\tcreateCodingTools,\n\tcreateEditTool,\n\tcreateFindTool,\n\tcreateGrepTool,\n\tcreateLsTool,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateWriteTool,\n\teditTool,\n\tfindTool,\n\tgrepTool,\n\tlsTool,\n\treadOnlyTools,\n\treadTool,\n\ttype Tool,\n\ttype ToolName,\n\twriteTool,\n} from \"./tools/index.js\";\n\n// Types\n\nexport interface CreateAgentSessionOptions {\n\t/** Working directory for project-local discovery. Default: process.cwd() */\n\tcwd?: string;\n\t/** Global config directory. Default: ~/.pi/agent */\n\tagentDir?: string;\n\n\t/** Auth storage for credentials. Default: discoverAuthStorage(agentDir) */\n\tauthStorage?: AuthStorage;\n\t/** Model registry. Default: discoverModels(authStorage, agentDir) */\n\tmodelRegistry?: ModelRegistry;\n\n\t/** Model to use. Default: from settings, else first available */\n\tmodel?: Model<any>;\n\t/** Thinking level. Default: from settings, else 'off' (clamped to model capabilities) */\n\tthinkingLevel?: ThinkingLevel;\n\t/** Models available for cycling (Ctrl+P in interactive mode) */\n\tscopedModels?: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }>;\n\n\t/** System prompt. String replaces default, function receives default and returns final. */\n\tsystemPrompt?: string | ((defaultPrompt: string) => string);\n\n\t/** Built-in tools to use. Default: codingTools [read, bash, edit, write] */\n\ttools?: Tool[];\n\t/** Custom tools (replaces discovery). */\n\tcustomTools?: Array<{ path?: string; tool: CustomTool }>;\n\t/** Additional custom tool paths to load (merged with discovery). */\n\tadditionalCustomToolPaths?: string[];\n\n\t/** Hooks (replaces discovery). */\n\thooks?: Array<{ path?: string; factory: HookFactory }>;\n\t/** Additional hook paths to load (merged with discovery). */\n\tadditionalHookPaths?: string[];\n\t/** Pre-loaded hooks (skips loading, used when hooks were loaded early for CLI flags). */\n\tpreloadedHooks?: LoadedHook[];\n\n\t/** Skills. Default: discovered from multiple locations */\n\tskills?: Skill[];\n\t/** Context files (AGENTS.md content). Default: discovered walking up from cwd */\n\tcontextFiles?: Array<{ path: string; content: string }>;\n\t/** Slash commands. Default: discovered from cwd/.pi/commands/ + agentDir/commands/ */\n\tslashCommands?: FileSlashCommand[];\n\n\t/** Session manager. Default: SessionManager.create(cwd) */\n\tsessionManager?: SessionManager;\n\n\t/** Settings manager. Default: SettingsManager.create(cwd, agentDir) */\n\tsettingsManager?: SettingsManager;\n}\n\n/** Result from createAgentSession */\nexport interface CreateAgentSessionResult {\n\t/** The created session */\n\tsession: AgentSession;\n\t/** Custom tools result (for UI context setup in interactive mode) */\n\tcustomToolsResult: CustomToolsLoadResult;\n\t/** Warning if session was restored with a different model than saved */\n\tmodelFallbackMessage?: string;\n}\n\n// Re-exports\n\nexport type { CustomTool } from \"./custom-tools/types.js\";\nexport type { HookAPI, HookCommandContext, HookContext, HookFactory } from \"./hooks/types.js\";\nexport type { Settings, SkillsSettings } from \"./settings-manager.js\";\nexport type { Skill } from \"./skills.js\";\nexport type { FileSlashCommand } from \"./slash-commands.js\";\nexport type { Tool } from \"./tools/index.js\";\n\nexport {\n\t// Pre-built tools (use process.cwd())\n\treadTool,\n\tbashTool,\n\teditTool,\n\twriteTool,\n\tgrepTool,\n\tfindTool,\n\tlsTool,\n\tcodingTools,\n\treadOnlyTools,\n\tallTools as allBuiltInTools,\n\t// Tool factories (for custom cwd)\n\tcreateCodingTools,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateBashTool,\n\tcreateEditTool,\n\tcreateWriteTool,\n\tcreateGrepTool,\n\tcreateFindTool,\n\tcreateLsTool,\n};\n\n// Helper Functions\n\nfunction getDefaultAgentDir(): string {\n\treturn getAgentDir();\n}\n\n// Discovery Functions\n\n/**\n * Create an AuthStorage instance for the given agent directory.\n */\nexport function discoverAuthStorage(agentDir: string = getDefaultAgentDir()): AuthStorage {\n\treturn new AuthStorage(join(agentDir, \"auth.json\"));\n}\n\n/**\n * Create a ModelRegistry for the given agent directory.\n */\nexport function discoverModels(authStorage: AuthStorage, agentDir: string = getDefaultAgentDir()): ModelRegistry {\n\treturn new ModelRegistry(authStorage, join(agentDir, \"models.json\"));\n}\n\n/**\n * Discover hooks from cwd and agentDir.\n */\nexport async function discoverHooks(\n\tcwd?: string,\n\tagentDir?: string,\n): Promise<Array<{ path: string; factory: HookFactory }>> {\n\tconst resolvedCwd = cwd ?? process.cwd();\n\tconst resolvedAgentDir = agentDir ?? getDefaultAgentDir();\n\n\tconst { hooks, errors } = await discoverAndLoadHooks([], resolvedCwd, resolvedAgentDir);\n\n\t// Log errors but don't fail\n\tfor (const { path, error } of errors) {\n\t\tconsole.error(`Failed to load hook \"${path}\": ${error}`);\n\t}\n\n\treturn hooks.map((h) => ({\n\t\tpath: h.path,\n\t\tfactory: createFactoryFromLoadedHook(h),\n\t}));\n}\n\n/**\n * Discover custom tools from cwd and agentDir.\n */\nexport async function discoverCustomTools(\n\tcwd?: string,\n\tagentDir?: string,\n): Promise<Array<{ path: string; tool: CustomTool }>> {\n\tconst resolvedCwd = cwd ?? process.cwd();\n\tconst resolvedAgentDir = agentDir ?? getDefaultAgentDir();\n\n\tconst { tools, errors } = await discoverAndLoadCustomTools([], resolvedCwd, Object.keys(allTools), resolvedAgentDir);\n\n\t// Log errors but don't fail\n\tfor (const { path, error } of errors) {\n\t\tconsole.error(`Failed to load custom tool \"${path}\": ${error}`);\n\t}\n\n\treturn tools.map((t) => ({\n\t\tpath: t.path,\n\t\ttool: t.tool,\n\t}));\n}\n\n/**\n * Discover skills from cwd and agentDir.\n */\nexport function discoverSkills(cwd?: string, agentDir?: string, settings?: SkillsSettings): Skill[] {\n\tconst { skills } = loadSkillsInternal({\n\t\t...settings,\n\t\tcwd: cwd ?? process.cwd(),\n\t\tagentDir: agentDir ?? getDefaultAgentDir(),\n\t});\n\treturn skills;\n}\n\n/**\n * Discover context files (AGENTS.md) walking up from cwd.\n */\nexport function discoverContextFiles(cwd?: string, agentDir?: string): Array<{ path: string; content: string }> {\n\treturn loadContextFilesInternal({\n\t\tcwd: cwd ?? process.cwd(),\n\t\tagentDir: agentDir ?? getDefaultAgentDir(),\n\t});\n}\n\n/**\n * Discover slash commands from cwd and agentDir.\n */\nexport function discoverSlashCommands(cwd?: string, agentDir?: string): FileSlashCommand[] {\n\treturn loadSlashCommandsInternal({\n\t\tcwd: cwd ?? process.cwd(),\n\t\tagentDir: agentDir ?? getDefaultAgentDir(),\n\t});\n}\n\n// API Key Helpers\n\n// System Prompt\n\nexport interface BuildSystemPromptOptions {\n\ttools?: Tool[];\n\tskills?: Skill[];\n\tcontextFiles?: Array<{ path: string; content: string }>;\n\tcwd?: string;\n\tappendPrompt?: string;\n}\n\n/**\n * Build the default system prompt.\n */\nexport function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {\n\treturn buildSystemPromptInternal({\n\t\tcwd: options.cwd,\n\t\tskills: options.skills,\n\t\tcontextFiles: options.contextFiles,\n\t\tappendSystemPrompt: options.appendPrompt,\n\t});\n}\n\n// Settings\n\n/**\n * Load settings from agentDir/settings.json merged with cwd/.pi/settings.json.\n */\nexport function loadSettings(cwd?: string, agentDir?: string): Settings {\n\tconst manager = SettingsManager.create(cwd ?? process.cwd(), agentDir ?? getDefaultAgentDir());\n\treturn {\n\t\tdefaultProvider: manager.getDefaultProvider(),\n\t\tdefaultModel: manager.getDefaultModel(),\n\t\tdefaultThinkingLevel: manager.getDefaultThinkingLevel(),\n\t\tsteeringMode: manager.getSteeringMode(),\n\t\tfollowUpMode: manager.getFollowUpMode(),\n\t\ttheme: manager.getTheme(),\n\t\tcompaction: manager.getCompactionSettings(),\n\t\tretry: manager.getRetrySettings(),\n\t\thideThinkingBlock: manager.getHideThinkingBlock(),\n\t\tshellPath: manager.getShellPath(),\n\t\tcollapseChangelog: manager.getCollapseChangelog(),\n\t\thooks: manager.getHookPaths(),\n\t\tcustomTools: manager.getCustomToolPaths(),\n\t\tskills: manager.getSkillsSettings(),\n\t\tterminal: { showImages: manager.getShowImages() },\n\t};\n}\n\n// Internal Helpers\n\n/**\n * Create a HookFactory from a LoadedHook.\n * This allows mixing discovered hooks with inline hooks.\n */\nfunction createFactoryFromLoadedHook(loaded: LoadedHook): HookFactory {\n\treturn (api) => {\n\t\tfor (const [eventType, handlers] of loaded.handlers) {\n\t\t\tfor (const handler of handlers) {\n\t\t\t\tapi.on(eventType as any, handler as any);\n\t\t\t}\n\t\t}\n\t};\n}\n\n/**\n * Convert hook definitions to LoadedHooks for the HookRunner.\n */\nfunction createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; factory: HookFactory }>): LoadedHook[] {\n\treturn definitions.map((def) => {\n\t\tconst hookPath = def.path ?? \"<inline>\";\n\t\tconst handlers = new Map<string, Array<(...args: unknown[]) => Promise<unknown>>>();\n\t\tconst messageRenderers = new Map<string, any>();\n\t\tconst commands = new Map<string, any>();\n\t\tconst flags = new Map<string, any>();\n\t\tconst flagValues = new Map<string, boolean | string>();\n\t\tconst shortcuts = new Map<KeyId, any>();\n\t\tlet sendMessageHandler: (\n\t\t\tmessage: any,\n\t\t\toptions?: { triggerTurn?: boolean; deliverAs?: \"steer\" | \"followUp\" },\n\t\t) => void = () => {};\n\t\tlet appendEntryHandler: (customType: string, data?: any) => void = () => {};\n\t\tlet getActiveToolsHandler: () => string[] = () => [];\n\t\tlet getAllToolsHandler: () => string[] = () => [];\n\t\tlet setActiveToolsHandler: (toolNames: string[]) => void = () => {};\n\t\tlet newSessionHandler: (options?: any) => Promise<{ cancelled: boolean }> = async () => ({ cancelled: false });\n\t\tlet branchHandler: (entryId: string) => Promise<{ cancelled: boolean }> = async () => ({ cancelled: false });\n\t\tlet navigateTreeHandler: (targetId: string, options?: any) => Promise<{ cancelled: boolean }> = async () => ({\n\t\t\tcancelled: false,\n\t\t});\n\n\t\tconst api = {\n\t\t\ton: (event: string, handler: (...args: unknown[]) => Promise<unknown>) => {\n\t\t\t\tconst list = handlers.get(event) ?? [];\n\t\t\t\tlist.push(handler);\n\t\t\t\thandlers.set(event, list);\n\t\t\t},\n\t\t\tsendMessage: (message: any, options?: { triggerTurn?: boolean; deliverAs?: \"steer\" | \"followUp\" }) => {\n\t\t\t\tsendMessageHandler(message, options);\n\t\t\t},\n\t\t\tappendEntry: (customType: string, data?: any) => {\n\t\t\t\tappendEntryHandler(customType, data);\n\t\t\t},\n\t\t\tregisterMessageRenderer: (customType: string, renderer: any) => {\n\t\t\t\tmessageRenderers.set(customType, renderer);\n\t\t\t},\n\t\t\tregisterCommand: (name: string, options: any) => {\n\t\t\t\tcommands.set(name, { name, ...options });\n\t\t\t},\n\t\t\tregisterFlag: (name: string, options: any) => {\n\t\t\t\tflags.set(name, { name, hookPath, ...options });\n\t\t\t\tif (options.default !== undefined) {\n\t\t\t\t\tflagValues.set(name, options.default);\n\t\t\t\t}\n\t\t\t},\n\t\t\tgetFlag: (name: string) => flagValues.get(name),\n\t\t\tregisterShortcut: (shortcut: KeyId, options: any) => {\n\t\t\t\tshortcuts.set(shortcut, { shortcut, hookPath, ...options });\n\t\t\t},\n\t\t\tnewSession: (options?: any) => newSessionHandler(options),\n\t\t\tbranch: (entryId: string) => branchHandler(entryId),\n\t\t\tnavigateTree: (targetId: string, options?: any) => navigateTreeHandler(targetId, options),\n\t\t\tgetActiveTools: () => getActiveToolsHandler(),\n\t\t\tgetAllTools: () => getAllToolsHandler(),\n\t\t\tsetActiveTools: (toolNames: string[]) => setActiveToolsHandler(toolNames),\n\t\t};\n\n\t\tdef.factory(api as any);\n\n\t\treturn {\n\t\t\tpath: hookPath,\n\t\t\tresolvedPath: hookPath,\n\t\t\thandlers,\n\t\t\tmessageRenderers,\n\t\t\tcommands,\n\t\t\tflags,\n\t\t\tflagValues,\n\t\t\tshortcuts,\n\t\t\tsetSendMessageHandler: (\n\t\t\t\thandler: (message: any, options?: { triggerTurn?: boolean; deliverAs?: \"steer\" | \"followUp\" }) => void,\n\t\t\t) => {\n\t\t\t\tsendMessageHandler = handler;\n\t\t\t},\n\t\t\tsetAppendEntryHandler: (handler: (customType: string, data?: any) => void) => {\n\t\t\t\tappendEntryHandler = handler;\n\t\t\t},\n\t\t\tsetNewSessionHandler: (handler: (options?: any) => Promise<{ cancelled: boolean }>) => {\n\t\t\t\tnewSessionHandler = handler;\n\t\t\t},\n\t\t\tsetBranchHandler: (handler: (entryId: string) => Promise<{ cancelled: boolean }>) => {\n\t\t\t\tbranchHandler = handler;\n\t\t\t},\n\t\t\tsetNavigateTreeHandler: (handler: (targetId: string, options?: any) => Promise<{ cancelled: boolean }>) => {\n\t\t\t\tnavigateTreeHandler = handler;\n\t\t\t},\n\t\t\tsetGetActiveToolsHandler: (handler: () => string[]) => {\n\t\t\t\tgetActiveToolsHandler = handler;\n\t\t\t},\n\t\t\tsetGetAllToolsHandler: (handler: () => string[]) => {\n\t\t\t\tgetAllToolsHandler = handler;\n\t\t\t},\n\t\t\tsetSetActiveToolsHandler: (handler: (toolNames: string[]) => void) => {\n\t\t\t\tsetActiveToolsHandler = handler;\n\t\t\t},\n\t\t\tsetFlagValue: (name: string, value: boolean | string) => {\n\t\t\t\tflagValues.set(name, value);\n\t\t\t},\n\t\t};\n\t});\n}\n\n// Factory\n\n/**\n * Create an AgentSession with the specified options.\n *\n * @example\n * ```typescript\n * // Minimal - uses defaults\n * const { session } = await createAgentSession();\n *\n * // With explicit model\n * import { getModel } from '@mariozechner/pi-ai';\n * const { session } = await createAgentSession({\n * model: getModel('anthropic', 'claude-opus-4-5'),\n * thinkingLevel: 'high',\n * });\n *\n * // Continue previous session\n * const { session, modelFallbackMessage } = await createAgentSession({\n * continueSession: true,\n * });\n *\n * // Full control\n * const { session } = await createAgentSession({\n * model: myModel,\n * getApiKey: async () => process.env.MY_KEY,\n * systemPrompt: 'You are helpful.',\n * tools: [readTool, bashTool],\n * hooks: [],\n * skills: [],\n * sessionManager: SessionManager.inMemory(),\n * });\n * ```\n */\nexport async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {\n\tconst cwd = options.cwd ?? process.cwd();\n\tconst agentDir = options.agentDir ?? getDefaultAgentDir();\n\n\t// Use provided or create AuthStorage and ModelRegistry\n\tconst authStorage = options.authStorage ?? discoverAuthStorage(agentDir);\n\tconst modelRegistry = options.modelRegistry ?? discoverModels(authStorage, agentDir);\n\ttime(\"discoverModels\");\n\n\tconst settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);\n\ttime(\"settingsManager\");\n\tconst sessionManager = options.sessionManager ?? SessionManager.create(cwd);\n\ttime(\"sessionManager\");\n\n\t// Check if session has existing data to restore\n\tconst existingSession = sessionManager.buildSessionContext();\n\ttime(\"loadSession\");\n\tconst hasExistingSession = existingSession.messages.length > 0;\n\n\tlet model = options.model;\n\tlet modelFallbackMessage: string | undefined;\n\n\t// If session has data, try to restore model from it\n\tif (!model && hasExistingSession && existingSession.model) {\n\t\tconst restoredModel = modelRegistry.find(existingSession.model.provider, existingSession.model.modelId);\n\t\tif (restoredModel && (await modelRegistry.getApiKey(restoredModel))) {\n\t\t\tmodel = restoredModel;\n\t\t}\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`;\n\t\t}\n\t}\n\n\t// If still no model, try settings default\n\tif (!model) {\n\t\tconst defaultProvider = settingsManager.getDefaultProvider();\n\t\tconst defaultModelId = settingsManager.getDefaultModel();\n\t\tif (defaultProvider && defaultModelId) {\n\t\t\tconst settingsModel = modelRegistry.find(defaultProvider, defaultModelId);\n\t\t\tif (settingsModel && (await modelRegistry.getApiKey(settingsModel))) {\n\t\t\t\tmodel = settingsModel;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Fall back to first available model with a valid API key\n\tif (!model) {\n\t\tfor (const m of modelRegistry.getAll()) {\n\t\t\tif (await modelRegistry.getApiKey(m)) {\n\t\t\t\tmodel = m;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\ttime(\"findAvailableModel\");\n\t\tif (model) {\n\t\t\tif (modelFallbackMessage) {\n\t\t\t\tmodelFallbackMessage += `. Using ${model.provider}/${model.id}`;\n\t\t\t}\n\t\t} else {\n\t\t\t// No models available - set message so user knows to /login or configure keys\n\t\t\tmodelFallbackMessage = \"No models available. Use /login or set an API key environment variable.\";\n\t\t}\n\t}\n\n\tlet thinkingLevel = options.thinkingLevel;\n\n\t// If session has data, restore thinking level from it\n\tif (thinkingLevel === undefined && hasExistingSession) {\n\t\tthinkingLevel = existingSession.thinkingLevel as ThinkingLevel;\n\t}\n\n\t// Fall back to settings default\n\tif (thinkingLevel === undefined) {\n\t\tthinkingLevel = settingsManager.getDefaultThinkingLevel() ?? \"off\";\n\t}\n\n\t// Clamp to model capabilities\n\tif (!model || !model.reasoning) {\n\t\tthinkingLevel = \"off\";\n\t}\n\n\tconst skills = options.skills ?? discoverSkills(cwd, agentDir, settingsManager.getSkillsSettings());\n\ttime(\"discoverSkills\");\n\n\tconst contextFiles = options.contextFiles ?? discoverContextFiles(cwd, agentDir);\n\ttime(\"discoverContextFiles\");\n\n\tconst autoResizeImages = settingsManager.getImageAutoResize();\n\t// Create ALL built-in tools for the registry (hooks can enable any of them)\n\tconst allBuiltInToolsMap = createAllTools(cwd, { read: { autoResizeImages } });\n\t// Determine initially active built-in tools (default: read, bash, edit, write)\n\tconst defaultActiveToolNames: ToolName[] = [\"read\", \"bash\", \"edit\", \"write\"];\n\tconst initialActiveToolNames: ToolName[] = options.tools\n\t\t? options.tools.map((t) => t.name).filter((n): n is ToolName => n in allBuiltInToolsMap)\n\t\t: defaultActiveToolNames;\n\tconst initialActiveBuiltInTools = initialActiveToolNames.map((name) => allBuiltInToolsMap[name]);\n\ttime(\"createAllTools\");\n\n\tlet customToolsResult: CustomToolsLoadResult;\n\tif (options.customTools !== undefined) {\n\t\t// Use provided custom tools\n\t\tconst loadedTools: LoadedCustomTool[] = options.customTools.map((ct) => ({\n\t\t\tpath: ct.path ?? \"<inline>\",\n\t\t\tresolvedPath: ct.path ?? \"<inline>\",\n\t\t\ttool: ct.tool,\n\t\t}));\n\t\tcustomToolsResult = {\n\t\t\ttools: loadedTools,\n\t\t\terrors: [],\n\t\t\tsetUIContext: () => {},\n\t\t};\n\t} else {\n\t\t// Discover custom tools, merging with additional paths\n\t\tconst configuredPaths = [...settingsManager.getCustomToolPaths(), ...(options.additionalCustomToolPaths ?? [])];\n\t\tcustomToolsResult = await discoverAndLoadCustomTools(configuredPaths, cwd, Object.keys(allTools), agentDir);\n\t\ttime(\"discoverAndLoadCustomTools\");\n\t\tfor (const { path, error } of customToolsResult.errors) {\n\t\t\tconsole.error(`Failed to load custom tool \"${path}\": ${error}`);\n\t\t}\n\t}\n\n\tlet hookRunner: HookRunner | undefined;\n\tif (options.preloadedHooks !== undefined && options.preloadedHooks.length > 0) {\n\t\t// Use pre-loaded hooks (from early CLI flag discovery)\n\t\thookRunner = new HookRunner(options.preloadedHooks, cwd, sessionManager, modelRegistry);\n\t} else if (options.hooks !== undefined) {\n\t\tif (options.hooks.length > 0) {\n\t\t\tconst loadedHooks = createLoadedHooksFromDefinitions(options.hooks);\n\t\t\thookRunner = new HookRunner(loadedHooks, cwd, sessionManager, modelRegistry);\n\t\t}\n\t} else {\n\t\t// Discover hooks, merging with additional paths\n\t\tconst configuredPaths = [...settingsManager.getHookPaths(), ...(options.additionalHookPaths ?? [])];\n\t\tconst { hooks, errors } = await discoverAndLoadHooks(configuredPaths, cwd, agentDir);\n\t\ttime(\"discoverAndLoadHooks\");\n\t\tfor (const { path, error } of errors) {\n\t\t\tconsole.error(`Failed to load hook \"${path}\": ${error}`);\n\t\t}\n\t\tif (hooks.length > 0) {\n\t\t\thookRunner = new HookRunner(hooks, cwd, sessionManager, modelRegistry);\n\t\t}\n\t}\n\n\t// Wrap custom tools with context getter (agent/session assigned below, accessed at execute time)\n\tlet agent: Agent;\n\tlet session: AgentSession;\n\tconst wrappedCustomTools = wrapCustomTools(customToolsResult.tools, () => ({\n\t\tsessionManager,\n\t\tmodelRegistry,\n\t\tmodel: agent.state.model,\n\t\tisIdle: () => !session.isStreaming,\n\t\thasPendingMessages: () => session.pendingMessageCount > 0,\n\t\tabort: () => {\n\t\t\tsession.abort();\n\t\t},\n\t}));\n\n\t// Create tool registry mapping name -> tool (for hook getTools/setTools)\n\t// Registry contains ALL built-in tools so hooks can enable any of them\n\tconst toolRegistry = new Map<string, AgentTool>();\n\tfor (const [name, tool] of Object.entries(allBuiltInToolsMap)) {\n\t\ttoolRegistry.set(name, tool as AgentTool);\n\t}\n\tfor (const tool of wrappedCustomTools as AgentTool[]) {\n\t\ttoolRegistry.set(tool.name, tool);\n\t}\n\n\t// Initially active tools = active built-in + custom\n\tlet activeToolsArray: Tool[] = [...initialActiveBuiltInTools, ...wrappedCustomTools];\n\ttime(\"combineTools\");\n\n\t// Wrap tools with hooks if available\n\tlet wrappedToolRegistry: Map<string, AgentTool> | undefined;\n\tif (hookRunner) {\n\t\tactiveToolsArray = wrapToolsWithHooks(activeToolsArray as AgentTool[], hookRunner);\n\t\t// Wrap ALL registry tools (not just active) so hooks can enable any\n\t\tconst allRegistryTools = Array.from(toolRegistry.values());\n\t\tconst wrappedAllTools = wrapToolsWithHooks(allRegistryTools, hookRunner);\n\t\twrappedToolRegistry = new Map<string, AgentTool>();\n\t\tfor (const tool of wrappedAllTools) {\n\t\t\twrappedToolRegistry.set(tool.name, tool);\n\t\t}\n\t}\n\n\t// Function to rebuild system prompt when tools change\n\t// Captures static options (cwd, agentDir, skills, contextFiles, customPrompt)\n\tconst rebuildSystemPrompt = (toolNames: string[]): string => {\n\t\t// Filter to valid tool names\n\t\tconst validToolNames = toolNames.filter((n): n is ToolName => n in allBuiltInToolsMap);\n\t\tconst defaultPrompt = buildSystemPromptInternal({\n\t\t\tcwd,\n\t\t\tagentDir,\n\t\t\tskills,\n\t\t\tcontextFiles,\n\t\t\tselectedTools: validToolNames,\n\t\t});\n\n\t\tif (options.systemPrompt === undefined) {\n\t\t\treturn defaultPrompt;\n\t\t} else if (typeof options.systemPrompt === \"string\") {\n\t\t\treturn buildSystemPromptInternal({\n\t\t\t\tcwd,\n\t\t\t\tagentDir,\n\t\t\t\tskills,\n\t\t\t\tcontextFiles,\n\t\t\t\tselectedTools: validToolNames,\n\t\t\t\tcustomPrompt: options.systemPrompt,\n\t\t\t});\n\t\t} else {\n\t\t\treturn options.systemPrompt(defaultPrompt);\n\t\t}\n\t};\n\n\tconst systemPrompt = rebuildSystemPrompt(initialActiveToolNames);\n\ttime(\"buildSystemPrompt\");\n\n\tconst slashCommands = options.slashCommands ?? discoverSlashCommands(cwd, agentDir);\n\ttime(\"discoverSlashCommands\");\n\n\tagent = new Agent({\n\t\tinitialState: {\n\t\t\tsystemPrompt,\n\t\t\tmodel,\n\t\t\tthinkingLevel,\n\t\t\ttools: activeToolsArray,\n\t\t},\n\t\tconvertToLlm,\n\t\ttransformContext: hookRunner\n\t\t\t? async (messages) => {\n\t\t\t\t\treturn hookRunner.emitContext(messages);\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tsteeringMode: settingsManager.getSteeringMode(),\n\t\tfollowUpMode: settingsManager.getFollowUpMode(),\n\t\tgetApiKey: async () => {\n\t\t\tconst currentModel = agent.state.model;\n\t\t\tif (!currentModel) {\n\t\t\t\tthrow new Error(\"No model selected\");\n\t\t\t}\n\t\t\tconst key = await modelRegistry.getApiKey(currentModel);\n\t\t\tif (!key) {\n\t\t\t\tthrow new Error(`No API key found for provider \"${currentModel.provider}\"`);\n\t\t\t}\n\t\t\treturn key;\n\t\t},\n\t});\n\ttime(\"createAgent\");\n\n\t// Restore messages if session has existing data\n\tif (hasExistingSession) {\n\t\tagent.replaceMessages(existingSession.messages);\n\t} else {\n\t\t// Save initial model and thinking level for new sessions so they can be restored on resume\n\t\tif (model) {\n\t\t\tsessionManager.appendModelChange(model.provider, model.id);\n\t\t}\n\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t}\n\n\tsession = new AgentSession({\n\t\tagent,\n\t\tsessionManager,\n\t\tsettingsManager,\n\t\tscopedModels: options.scopedModels,\n\t\tfileCommands: slashCommands,\n\t\thookRunner,\n\t\tcustomTools: customToolsResult.tools,\n\t\tskillsSettings: settingsManager.getSkillsSettings(),\n\t\tmodelRegistry,\n\t\ttoolRegistry: wrappedToolRegistry ?? toolRegistry,\n\t\trebuildSystemPrompt,\n\t});\n\ttime(\"createAgentSession\");\n\n\treturn {\n\t\tsession,\n\t\tcustomToolsResult,\n\t\tmodelFallbackMessage,\n\t};\n}\n"]}