@oh-my-pi/pi-coding-agent 3.15.0 → 3.20.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 (193) hide show
  1. package/CHANGELOG.md +61 -1
  2. package/docs/extensions.md +1055 -0
  3. package/docs/rpc.md +69 -13
  4. package/docs/session-tree-plan.md +1 -1
  5. package/examples/extensions/README.md +141 -0
  6. package/examples/extensions/api-demo.ts +87 -0
  7. package/examples/extensions/chalk-logger.ts +26 -0
  8. package/examples/extensions/hello.ts +33 -0
  9. package/examples/extensions/pirate.ts +44 -0
  10. package/examples/extensions/plan-mode.ts +551 -0
  11. package/examples/extensions/subagent/agents/reviewer.md +35 -0
  12. package/examples/extensions/todo.ts +299 -0
  13. package/examples/extensions/tools.ts +145 -0
  14. package/examples/extensions/with-deps/index.ts +36 -0
  15. package/examples/extensions/with-deps/package-lock.json +31 -0
  16. package/examples/extensions/with-deps/package.json +16 -0
  17. package/examples/sdk/02-custom-model.ts +3 -3
  18. package/examples/sdk/05-tools.ts +7 -3
  19. package/examples/sdk/06-extensions.ts +81 -0
  20. package/examples/sdk/06-hooks.ts +14 -13
  21. package/examples/sdk/08-prompt-templates.ts +42 -0
  22. package/examples/sdk/08-slash-commands.ts +17 -12
  23. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  24. package/examples/sdk/12-full-control.ts +6 -6
  25. package/package.json +11 -7
  26. package/src/capability/extension-module.ts +34 -0
  27. package/src/cli/args.ts +22 -7
  28. package/src/cli/file-processor.ts +38 -67
  29. package/src/cli/list-models.ts +1 -1
  30. package/src/config.ts +25 -14
  31. package/src/core/agent-session.ts +505 -242
  32. package/src/core/auth-storage.ts +33 -21
  33. package/src/core/compaction/branch-summarization.ts +4 -4
  34. package/src/core/compaction/compaction.ts +3 -3
  35. package/src/core/custom-commands/bundled/wt/index.ts +430 -0
  36. package/src/core/custom-commands/loader.ts +9 -0
  37. package/src/core/custom-tools/wrapper.ts +5 -0
  38. package/src/core/event-bus.ts +59 -0
  39. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  40. package/src/core/export-html/vendor/marked.min.js +6 -0
  41. package/src/core/extensions/index.ts +100 -0
  42. package/src/core/extensions/loader.ts +501 -0
  43. package/src/core/extensions/runner.ts +477 -0
  44. package/src/core/extensions/types.ts +712 -0
  45. package/src/core/extensions/wrapper.ts +147 -0
  46. package/src/core/hooks/types.ts +2 -2
  47. package/src/core/index.ts +10 -21
  48. package/src/core/keybindings.ts +199 -0
  49. package/src/core/messages.ts +26 -7
  50. package/src/core/model-registry.ts +123 -46
  51. package/src/core/model-resolver.ts +7 -5
  52. package/src/core/prompt-templates.ts +242 -0
  53. package/src/core/sdk.ts +378 -295
  54. package/src/core/session-manager.ts +72 -58
  55. package/src/core/settings-manager.ts +118 -22
  56. package/src/core/system-prompt.ts +24 -1
  57. package/src/core/terminal-notify.ts +37 -0
  58. package/src/core/tools/context.ts +4 -4
  59. package/src/core/tools/exa/mcp-client.ts +5 -4
  60. package/src/core/tools/exa/render.ts +176 -131
  61. package/src/core/tools/gemini-image.ts +361 -0
  62. package/src/core/tools/git.ts +216 -0
  63. package/src/core/tools/index.ts +28 -15
  64. package/src/core/tools/lsp/config.ts +5 -4
  65. package/src/core/tools/lsp/index.ts +17 -12
  66. package/src/core/tools/lsp/render.ts +39 -47
  67. package/src/core/tools/read.ts +66 -29
  68. package/src/core/tools/render-utils.ts +268 -0
  69. package/src/core/tools/renderers.ts +243 -225
  70. package/src/core/tools/task/discovery.ts +2 -2
  71. package/src/core/tools/task/executor.ts +66 -58
  72. package/src/core/tools/task/index.ts +29 -10
  73. package/src/core/tools/task/model-resolver.ts +8 -13
  74. package/src/core/tools/task/omp-command.ts +24 -0
  75. package/src/core/tools/task/render.ts +35 -60
  76. package/src/core/tools/task/types.ts +3 -0
  77. package/src/core/tools/web-fetch.ts +29 -28
  78. package/src/core/tools/web-search/index.ts +6 -5
  79. package/src/core/tools/web-search/providers/exa.ts +6 -5
  80. package/src/core/tools/web-search/render.ts +66 -111
  81. package/src/core/voice-controller.ts +135 -0
  82. package/src/core/voice-supervisor.ts +1003 -0
  83. package/src/core/voice.ts +308 -0
  84. package/src/discovery/builtin.ts +75 -1
  85. package/src/discovery/claude.ts +47 -1
  86. package/src/discovery/codex.ts +54 -2
  87. package/src/discovery/gemini.ts +55 -2
  88. package/src/discovery/helpers.ts +100 -1
  89. package/src/discovery/index.ts +2 -0
  90. package/src/index.ts +14 -9
  91. package/src/lib/worktree/collapse.ts +179 -0
  92. package/src/lib/worktree/constants.ts +14 -0
  93. package/src/lib/worktree/errors.ts +23 -0
  94. package/src/lib/worktree/git.ts +110 -0
  95. package/src/lib/worktree/index.ts +23 -0
  96. package/src/lib/worktree/operations.ts +216 -0
  97. package/src/lib/worktree/session.ts +114 -0
  98. package/src/lib/worktree/stats.ts +67 -0
  99. package/src/main.ts +61 -37
  100. package/src/migrations.ts +37 -7
  101. package/src/modes/interactive/components/bash-execution.ts +6 -4
  102. package/src/modes/interactive/components/custom-editor.ts +55 -0
  103. package/src/modes/interactive/components/custom-message.ts +95 -0
  104. package/src/modes/interactive/components/extensions/extension-list.ts +5 -0
  105. package/src/modes/interactive/components/extensions/inspector-panel.ts +18 -12
  106. package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
  107. package/src/modes/interactive/components/extensions/types.ts +1 -0
  108. package/src/modes/interactive/components/footer.ts +324 -0
  109. package/src/modes/interactive/components/hook-editor.ts +1 -0
  110. package/src/modes/interactive/components/hook-selector.ts +3 -3
  111. package/src/modes/interactive/components/model-selector.ts +7 -6
  112. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  113. package/src/modes/interactive/components/settings-defs.ts +55 -6
  114. package/src/modes/interactive/components/status-line/separators.ts +4 -4
  115. package/src/modes/interactive/components/status-line.ts +45 -35
  116. package/src/modes/interactive/components/tool-execution.ts +95 -23
  117. package/src/modes/interactive/interactive-mode.ts +644 -113
  118. package/src/modes/interactive/theme/defaults/alabaster.json +99 -0
  119. package/src/modes/interactive/theme/defaults/amethyst.json +103 -0
  120. package/src/modes/interactive/theme/defaults/anthracite.json +100 -0
  121. package/src/modes/interactive/theme/defaults/basalt.json +90 -0
  122. package/src/modes/interactive/theme/defaults/birch.json +101 -0
  123. package/src/modes/interactive/theme/defaults/dark-abyss.json +97 -0
  124. package/src/modes/interactive/theme/defaults/dark-aurora.json +94 -0
  125. package/src/modes/interactive/theme/defaults/dark-cavern.json +97 -0
  126. package/src/modes/interactive/theme/defaults/dark-copper.json +94 -0
  127. package/src/modes/interactive/theme/defaults/dark-cosmos.json +96 -0
  128. package/src/modes/interactive/theme/defaults/dark-eclipse.json +97 -0
  129. package/src/modes/interactive/theme/defaults/dark-ember.json +94 -0
  130. package/src/modes/interactive/theme/defaults/dark-equinox.json +96 -0
  131. package/src/modes/interactive/theme/defaults/dark-lavender.json +94 -0
  132. package/src/modes/interactive/theme/defaults/dark-lunar.json +95 -0
  133. package/src/modes/interactive/theme/defaults/dark-midnight.json +94 -0
  134. package/src/modes/interactive/theme/defaults/dark-nebula.json +96 -0
  135. package/src/modes/interactive/theme/defaults/dark-rainforest.json +97 -0
  136. package/src/modes/interactive/theme/defaults/dark-reef.json +97 -0
  137. package/src/modes/interactive/theme/defaults/dark-sakura.json +94 -0
  138. package/src/modes/interactive/theme/defaults/dark-slate.json +94 -0
  139. package/src/modes/interactive/theme/defaults/dark-solstice.json +96 -0
  140. package/src/modes/interactive/theme/defaults/dark-starfall.json +97 -0
  141. package/src/modes/interactive/theme/defaults/dark-swamp.json +96 -0
  142. package/src/modes/interactive/theme/defaults/dark-taiga.json +97 -0
  143. package/src/modes/interactive/theme/defaults/dark-terminal.json +94 -0
  144. package/src/modes/interactive/theme/defaults/dark-tundra.json +97 -0
  145. package/src/modes/interactive/theme/defaults/dark-twilight.json +97 -0
  146. package/src/modes/interactive/theme/defaults/dark-volcanic.json +97 -0
  147. package/src/modes/interactive/theme/defaults/graphite.json +99 -0
  148. package/src/modes/interactive/theme/defaults/index.ts +128 -0
  149. package/src/modes/interactive/theme/defaults/light-aurora-day.json +97 -0
  150. package/src/modes/interactive/theme/defaults/light-canyon.json +97 -0
  151. package/src/modes/interactive/theme/defaults/light-cirrus.json +96 -0
  152. package/src/modes/interactive/theme/defaults/light-coral.json +94 -0
  153. package/src/modes/interactive/theme/defaults/light-dawn.json +96 -0
  154. package/src/modes/interactive/theme/defaults/light-dunes.json +97 -0
  155. package/src/modes/interactive/theme/defaults/light-eucalyptus.json +94 -0
  156. package/src/modes/interactive/theme/defaults/light-frost.json +94 -0
  157. package/src/modes/interactive/theme/defaults/light-glacier.json +97 -0
  158. package/src/modes/interactive/theme/defaults/light-haze.json +96 -0
  159. package/src/modes/interactive/theme/defaults/light-honeycomb.json +94 -0
  160. package/src/modes/interactive/theme/defaults/light-lagoon.json +97 -0
  161. package/src/modes/interactive/theme/defaults/light-lavender.json +94 -0
  162. package/src/modes/interactive/theme/defaults/light-meadow.json +97 -0
  163. package/src/modes/interactive/theme/defaults/light-mint.json +94 -0
  164. package/src/modes/interactive/theme/defaults/light-opal.json +97 -0
  165. package/src/modes/interactive/theme/defaults/light-orchard.json +97 -0
  166. package/src/modes/interactive/theme/defaults/light-paper.json +94 -0
  167. package/src/modes/interactive/theme/defaults/light-prism.json +96 -0
  168. package/src/modes/interactive/theme/defaults/light-sand.json +94 -0
  169. package/src/modes/interactive/theme/defaults/light-savanna.json +97 -0
  170. package/src/modes/interactive/theme/defaults/light-soleil.json +96 -0
  171. package/src/modes/interactive/theme/defaults/light-wetland.json +97 -0
  172. package/src/modes/interactive/theme/defaults/light-zenith.json +95 -0
  173. package/src/modes/interactive/theme/defaults/limestone.json +100 -0
  174. package/src/modes/interactive/theme/defaults/mahogany.json +104 -0
  175. package/src/modes/interactive/theme/defaults/marble.json +99 -0
  176. package/src/modes/interactive/theme/defaults/obsidian.json +90 -0
  177. package/src/modes/interactive/theme/defaults/onyx.json +90 -0
  178. package/src/modes/interactive/theme/defaults/pearl.json +99 -0
  179. package/src/modes/interactive/theme/defaults/porcelain.json +90 -0
  180. package/src/modes/interactive/theme/defaults/quartz.json +102 -0
  181. package/src/modes/interactive/theme/defaults/sandstone.json +101 -0
  182. package/src/modes/interactive/theme/defaults/titanium.json +89 -0
  183. package/src/modes/print-mode.ts +14 -72
  184. package/src/modes/rpc/rpc-client.ts +23 -9
  185. package/src/modes/rpc/rpc-mode.ts +137 -125
  186. package/src/modes/rpc/rpc-types.ts +46 -24
  187. package/src/prompts/task.md +1 -0
  188. package/src/prompts/tools/gemini-image.md +4 -0
  189. package/src/prompts/tools/git.md +9 -0
  190. package/src/prompts/voice-summary.md +12 -0
  191. package/src/utils/image-convert.ts +26 -0
  192. package/src/utils/image-resize.ts +215 -0
  193. package/src/utils/shell-snapshot.ts +22 -20
package/src/core/sdk.ts CHANGED
@@ -9,12 +9,9 @@
9
9
  * // Minimal - everything auto-discovered
10
10
  * const session = await createAgentSession();
11
11
  *
12
- * // With custom hooks
12
+ * // With custom extensions
13
13
  * const session = await createAgentSession({
14
- * hooks: [
15
- * ...await discoverHooks(),
16
- * { factory: myHookFactory },
17
- * ],
14
+ * extensions: [myExtensionFactory],
18
15
  * });
19
16
  *
20
17
  * // Full control
@@ -22,7 +19,7 @@
22
19
  * model: myModel,
23
20
  * getApiKey: async () => process.env.MY_KEY,
24
21
  * tools: [readTool, bashTool],
25
- * hooks: [],
22
+ * extensions: [],
26
23
  * skills: [],
27
24
  * sessionFile: false,
28
25
  * });
@@ -30,52 +27,64 @@
30
27
  */
31
28
 
32
29
  import { join } from "node:path";
33
- import { Agent, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
30
+ import { Agent, type AgentTool, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
34
31
  import type { Model } from "@oh-my-pi/pi-ai";
32
+ import type { Component } from "@oh-my-pi/pi-tui";
35
33
  import chalk from "chalk";
36
34
  // Import discovery to register all providers on startup
37
35
  import "../discovery";
38
36
  import { loadSync as loadCapability } from "../capability/index";
39
37
  import { type Rule, ruleCapability } from "../capability/rule";
40
38
  import { getAgentDir, getConfigDirPaths } from "../config";
39
+ import { initializeWithSettings } from "../discovery";
41
40
  import { AgentSession } from "./agent-session";
42
41
  import { AuthStorage } from "./auth-storage";
43
42
  import {
44
43
  type CustomCommandsLoadResult,
45
44
  loadCustomCommands as loadCustomCommandsInternal,
46
45
  } from "./custom-commands/index";
46
+ import type { CustomTool, CustomToolContext, CustomToolSessionEvent } from "./custom-tools/types";
47
+ import { createEventBus, type EventBus } from "./event-bus";
47
48
  import {
48
- type CustomToolsLoadResult,
49
- discoverAndLoadCustomTools,
50
- type LoadedCustomTool,
51
- wrapCustomTools,
52
- } from "./custom-tools/index";
53
- import type { CustomTool } from "./custom-tools/types";
54
- import { discoverAndLoadHooks, HookRunner, type LoadedHook, wrapToolsWithHooks } from "./hooks/index";
55
- import type { HookFactory } from "./hooks/types";
49
+ discoverAndLoadExtensions,
50
+ type ExtensionContext,
51
+ type ExtensionFactory,
52
+ ExtensionRunner,
53
+ type LoadExtensionsResult,
54
+ type LoadedExtension,
55
+ loadExtensionFromFactory,
56
+ type ToolDefinition,
57
+ wrapRegisteredTools,
58
+ wrapToolsWithExtensions,
59
+ } from "./extensions/index";
56
60
  import { logger } from "./logger";
57
61
  import { discoverAndLoadMCPTools, type MCPManager, type MCPToolsLoadResult } from "./mcp/index";
58
62
  import { convertToLlm } from "./messages";
59
63
  import { ModelRegistry } from "./model-registry";
64
+ import { parseModelString } from "./model-resolver";
65
+ import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate } from "./prompt-templates";
60
66
  import { SessionManager } from "./session-manager";
61
- import { type CommandsSettings, type Settings, SettingsManager, type SkillsSettings } from "./settings-manager";
67
+ import { type Settings, SettingsManager, type SkillsSettings } from "./settings-manager";
62
68
  import { loadSkills as loadSkillsInternal, type Skill } from "./skills";
63
- import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal } from "./slash-commands";
64
69
  import {
65
70
  buildSystemPrompt as buildSystemPromptInternal,
66
71
  loadProjectContextFiles as loadContextFilesInternal,
67
72
  } from "./system-prompt";
68
73
  import { time } from "./timings";
69
74
  import { createToolContextStore } from "./tools/context";
75
+ import { getGeminiImageTools } from "./tools/gemini-image";
70
76
  import {
71
77
  allTools,
72
78
  applyBashInterception,
79
+ baseCodingToolNames,
73
80
  bashTool,
74
81
  codingTools,
82
+ createAllTools,
75
83
  createBashTool,
76
84
  createCodingTools,
77
85
  createEditTool,
78
86
  createFindTool,
87
+ createGitTool,
79
88
  createGrepTool,
80
89
  createLsTool,
81
90
  createReadOnlyTools,
@@ -85,11 +94,14 @@ import {
85
94
  editTool,
86
95
  filterRulebookRules,
87
96
  findTool,
97
+ getWebSearchTools,
98
+ gitTool,
88
99
  grepTool,
89
100
  lsTool,
90
101
  readOnlyTools,
91
102
  readTool,
92
103
  type Tool,
104
+ type ToolName,
93
105
  warmupLspServers,
94
106
  writeTool,
95
107
  } from "./tools/index";
@@ -118,24 +130,29 @@ export interface CreateAgentSessionOptions {
118
130
  /** System prompt. String replaces default, function receives default and returns final. */
119
131
  systemPrompt?: string | ((defaultPrompt: string) => string);
120
132
 
121
- /** Built-in tools to use. Default: codingTools [read, bash, edit, write] */
133
+ /** Built-in tools to use. Default: all coding tools (read, bash, edit, write, grep, find, ls, lsp, notebook, output, task, web_fetch, web_search) */
122
134
  tools?: Tool[];
123
- /** Custom tools (replaces discovery). */
124
- customTools?: Array<{ path?: string; tool: CustomTool }>;
125
- /** Additional custom tool paths to load (merged with discovery). */
126
- additionalCustomToolPaths?: string[];
127
-
128
- /** Hooks (replaces discovery). */
129
- hooks?: Array<{ path?: string; factory: HookFactory }>;
130
- /** Additional hook paths to load (merged with discovery). */
131
- additionalHookPaths?: string[];
135
+ /** Custom tools to register (in addition to built-in tools). Accepts both CustomTool and ToolDefinition. */
136
+ customTools?: (CustomTool | ToolDefinition)[];
137
+ /** Inline extensions (merged with discovery). */
138
+ extensions?: ExtensionFactory[];
139
+ /** Additional extension paths to load (merged with discovery). */
140
+ additionalExtensionPaths?: string[];
141
+ /**
142
+ * Pre-loaded extensions (skips file discovery).
143
+ * @internal Used by CLI when extensions are loaded early to parse custom flags.
144
+ */
145
+ preloadedExtensions?: LoadedExtension[];
146
+
147
+ /** Shared event bus for tool/extension communication. Default: creates new bus. */
148
+ eventBus?: EventBus;
132
149
 
133
150
  /** Skills. Default: discovered from multiple locations */
134
151
  skills?: Skill[];
135
152
  /** Context files (AGENTS.md content). Default: discovered walking up from cwd */
136
153
  contextFiles?: Array<{ path: string; content: string }>;
137
- /** Slash commands. Default: discovered from cwd/.omp/commands/ + agentDir/commands/ */
138
- slashCommands?: FileSlashCommand[];
154
+ /** Prompt templates. Default: discovered from cwd/.omp/prompts/ + agentDir/prompts/ */
155
+ promptTemplates?: PromptTemplate[];
139
156
 
140
157
  /** Enable MCP server discovery from .mcp.json files. Default: true */
141
158
  enableMCP?: boolean;
@@ -157,8 +174,8 @@ export interface CreateAgentSessionOptions {
157
174
  export interface CreateAgentSessionResult {
158
175
  /** The created session */
159
176
  session: AgentSession;
160
- /** Custom tools result (for UI context setup in interactive mode) */
161
- customToolsResult: CustomToolsLoadResult;
177
+ /** Extensions result (for UI context setup in interactive mode) */
178
+ extensionsResult: LoadExtensionsResult;
162
179
  /** MCP manager for server lifecycle management (undefined if MCP disabled) */
163
180
  mcpManager?: MCPManager;
164
181
  /** Warning if session was restored with a different model than saved */
@@ -170,12 +187,18 @@ export interface CreateAgentSessionResult {
170
187
  // Re-exports
171
188
 
172
189
  export type { CustomCommand, CustomCommandFactory } from "./custom-commands/types";
173
- export type { CustomTool } from "./custom-tools/types";
174
- export type { HookAPI, HookCommandContext, HookContext, HookFactory } from "./hooks/types";
190
+ export type { CustomTool, CustomToolFactory } from "./custom-tools/types";
191
+ export type {
192
+ ExtensionAPI,
193
+ ExtensionCommandContext,
194
+ ExtensionContext,
195
+ ExtensionFactory,
196
+ ToolDefinition,
197
+ } from "./extensions/index";
175
198
  export type { MCPManager, MCPServerConfig, MCPServerConnection, MCPToolsLoadResult } from "./mcp/index";
199
+ export type { PromptTemplate } from "./prompt-templates";
176
200
  export type { Settings, SkillsSettings } from "./settings-manager";
177
201
  export type { Skill } from "./skills";
178
- export type { FileSlashCommand } from "./slash-commands";
179
202
  export type { Tool } from "./tools/index";
180
203
 
181
204
  export {
@@ -186,6 +209,7 @@ export {
186
209
  writeTool,
187
210
  grepTool,
188
211
  findTool,
212
+ gitTool,
189
213
  lsTool,
190
214
  codingTools,
191
215
  readOnlyTools,
@@ -199,6 +223,7 @@ export {
199
223
  createWriteTool,
200
224
  createGrepTool,
201
225
  createFindTool,
226
+ createGitTool,
202
227
  createLsTool,
203
228
  };
204
229
 
@@ -214,7 +239,7 @@ function getDefaultAgentDir(): string {
214
239
  * Create an AuthStorage instance with fallback support.
215
240
  * Reads from primary path first, then falls back to legacy paths (.pi, .claude).
216
241
  */
217
- export function discoverAuthStorage(agentDir: string = getDefaultAgentDir()): AuthStorage {
242
+ export async function discoverAuthStorage(agentDir: string = getDefaultAgentDir()): Promise<AuthStorage> {
218
243
  const primaryPath = join(agentDir, "auth.json");
219
244
  // Get all auth.json paths (user-level only), excluding the primary
220
245
  const allPaths = getConfigDirPaths("auth.json", { project: false });
@@ -222,14 +247,19 @@ export function discoverAuthStorage(agentDir: string = getDefaultAgentDir()): Au
222
247
 
223
248
  logger.debug("discoverAuthStorage", { agentDir, primaryPath, allPaths, fallbackPaths });
224
249
 
225
- return new AuthStorage(primaryPath, fallbackPaths);
250
+ const storage = new AuthStorage(primaryPath, fallbackPaths);
251
+ await storage.reload();
252
+ return storage;
226
253
  }
227
254
 
228
255
  /**
229
256
  * Create a ModelRegistry with fallback support.
230
257
  * Reads from primary path first, then falls back to legacy paths (.pi, .claude).
231
258
  */
232
- export function discoverModels(authStorage: AuthStorage, agentDir: string = getDefaultAgentDir()): ModelRegistry {
259
+ export async function discoverModels(
260
+ authStorage: AuthStorage,
261
+ agentDir: string = getDefaultAgentDir(),
262
+ ): Promise<ModelRegistry> {
233
263
  const primaryPath = join(agentDir, "models.json");
234
264
  // Get all models.json paths (user-level only), excluding the primary
235
265
  const allPaths = getConfigDirPaths("models.json", { project: false });
@@ -237,51 +267,18 @@ export function discoverModels(authStorage: AuthStorage, agentDir: string = getD
237
267
 
238
268
  logger.debug("discoverModels", { primaryPath, fallbackPaths });
239
269
 
240
- return new ModelRegistry(authStorage, primaryPath, fallbackPaths);
270
+ const registry = new ModelRegistry(authStorage, primaryPath, fallbackPaths);
271
+ await registry.refresh();
272
+ return registry;
241
273
  }
242
274
 
243
275
  /**
244
- * Discover hooks from cwd and agentDir.
276
+ * Discover extensions from cwd.
245
277
  */
246
- export async function discoverHooks(
247
- cwd?: string,
248
- _agentDir?: string,
249
- ): Promise<Array<{ path: string; factory: HookFactory }>> {
278
+ export async function discoverExtensions(cwd?: string): Promise<LoadExtensionsResult> {
250
279
  const resolvedCwd = cwd ?? process.cwd();
251
280
 
252
- const { hooks, errors } = await discoverAndLoadHooks([], resolvedCwd);
253
-
254
- // Log errors but don't fail
255
- for (const { path, error } of errors) {
256
- console.error(`Failed to load hook "${path}": ${error}`);
257
- }
258
-
259
- return hooks.map((h) => ({
260
- path: h.path,
261
- factory: createFactoryFromLoadedHook(h),
262
- }));
263
- }
264
-
265
- /**
266
- * Discover custom tools from cwd and agentDir.
267
- */
268
- export async function discoverCustomTools(
269
- cwd?: string,
270
- _agentDir?: string,
271
- ): Promise<Array<{ path: string; tool: CustomTool }>> {
272
- const resolvedCwd = cwd ?? process.cwd();
273
-
274
- const { tools, errors } = await discoverAndLoadCustomTools([], resolvedCwd, Object.keys(allTools));
275
-
276
- // Log errors but don't fail
277
- for (const { path, error } of errors) {
278
- console.error(`Failed to load custom tool "${path}": ${error}`);
279
- }
280
-
281
- return tools.map((t) => ({
282
- path: t.path,
283
- tool: t.tool,
284
- }));
281
+ return discoverAndLoadExtensions([], resolvedCwd);
285
282
  }
286
283
 
287
284
  /**
@@ -309,15 +306,12 @@ export function discoverContextFiles(
309
306
  }
310
307
 
311
308
  /**
312
- * Discover slash commands from cwd and agentDir.
309
+ * Discover prompt templates from cwd and agentDir.
313
310
  */
314
- export function discoverSlashCommands(
315
- cwd?: string,
316
- _agentDir?: string,
317
- _settings?: CommandsSettings,
318
- ): FileSlashCommand[] {
319
- return loadSlashCommandsInternal({
311
+ export async function discoverPromptTemplates(cwd?: string, agentDir?: string): Promise<PromptTemplate[]> {
312
+ return await loadPromptTemplatesInternal({
320
313
  cwd: cwd ?? process.cwd(),
314
+ agentDir: agentDir ?? getDefaultAgentDir(),
321
315
  });
322
316
  }
323
317
 
@@ -377,15 +371,16 @@ export function loadSettings(cwd?: string, agentDir?: string): Settings {
377
371
  return {
378
372
  modelRoles: manager.getModelRoles(),
379
373
  defaultThinkingLevel: manager.getDefaultThinkingLevel(),
380
- queueMode: manager.getQueueMode(),
374
+ steeringMode: manager.getSteeringMode(),
375
+ followUpMode: manager.getFollowUpMode(),
376
+ interruptMode: manager.getInterruptMode(),
381
377
  theme: manager.getTheme(),
382
378
  compaction: manager.getCompactionSettings(),
383
379
  retry: manager.getRetrySettings(),
384
380
  hideThinkingBlock: manager.getHideThinkingBlock(),
385
381
  shellPath: manager.getShellPath(),
386
382
  collapseChangelog: manager.getCollapseChangelog(),
387
- hooks: manager.getHookPaths(),
388
- customTools: manager.getCustomToolPaths(),
383
+ extensions: manager.getExtensionPaths(),
389
384
  skills: manager.getSkillsSettings(),
390
385
  terminal: { showImages: manager.getShowImages() },
391
386
  };
@@ -393,84 +388,85 @@ export function loadSettings(cwd?: string, agentDir?: string): Settings {
393
388
 
394
389
  // Internal Helpers
395
390
 
396
- /**
397
- * Create a HookFactory from a LoadedHook.
398
- * This allows mixing discovered hooks with inline hooks.
399
- */
400
- function createFactoryFromLoadedHook(loaded: LoadedHook): HookFactory {
401
- return (api) => {
402
- for (const [eventType, handlers] of loaded.handlers) {
403
- for (const handler of handlers) {
404
- api.on(eventType as any, handler as any);
405
- }
406
- }
391
+ function createCustomToolContext(ctx: ExtensionContext): CustomToolContext {
392
+ return {
393
+ sessionManager: ctx.sessionManager,
394
+ modelRegistry: ctx.modelRegistry,
395
+ model: ctx.model,
396
+ isIdle: ctx.isIdle,
397
+ hasQueuedMessages: ctx.hasPendingMessages,
398
+ abort: ctx.abort,
407
399
  };
408
400
  }
409
401
 
410
- /**
411
- * Convert hook definitions to LoadedHooks for the HookRunner.
412
- */
413
- function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; factory: HookFactory }>): LoadedHook[] {
414
- return definitions.map((def) => {
415
- const handlers = new Map<string, Array<(...args: unknown[]) => Promise<unknown>>>();
416
- const messageRenderers = new Map<string, any>();
417
- const commands = new Map<string, any>();
418
- let sendMessageHandler: (message: any, triggerTurn?: boolean) => void = () => {};
419
- let appendEntryHandler: (customType: string, data?: any) => void = () => {};
420
- let newSessionHandler: (options?: any) => Promise<{ cancelled: boolean }> = async () => ({ cancelled: false });
421
- let branchHandler: (entryId: string) => Promise<{ cancelled: boolean }> = async () => ({ cancelled: false });
422
- let navigateTreeHandler: (targetId: string, options?: any) => Promise<{ cancelled: boolean }> = async () => ({
423
- cancelled: false,
424
- });
402
+ function isCustomTool(tool: CustomTool | ToolDefinition): tool is CustomTool {
403
+ // To distinguish, we mark converted tools with a hidden symbol property.
404
+ // If the tool doesn't have this marker, it's a CustomTool that needs conversion.
405
+ return !(tool as any).__isToolDefinition;
406
+ }
425
407
 
426
- const api = {
427
- on: (event: string, handler: (...args: unknown[]) => Promise<unknown>) => {
428
- const list = handlers.get(event) ?? [];
429
- list.push(handler);
430
- handlers.set(event, list);
431
- },
432
- sendMessage: (message: any, triggerTurn?: boolean) => {
433
- sendMessageHandler(message, triggerTurn);
434
- },
435
- appendEntry: (customType: string, data?: any) => {
436
- appendEntryHandler(customType, data);
437
- },
438
- registerMessageRenderer: (customType: string, renderer: any) => {
439
- messageRenderers.set(customType, renderer);
440
- },
441
- registerCommand: (name: string, options: any) => {
442
- commands.set(name, { name, ...options });
443
- },
444
- newSession: (options?: any) => newSessionHandler(options),
445
- branch: (entryId: string) => branchHandler(entryId),
446
- navigateTree: (targetId: string, options?: any) => navigateTreeHandler(targetId, options),
447
- };
408
+ const TOOL_DEFINITION_MARKER = Symbol("__isToolDefinition");
409
+
410
+ function customToolToDefinition(tool: CustomTool): ToolDefinition {
411
+ const definition: ToolDefinition & { [TOOL_DEFINITION_MARKER]: true } = {
412
+ name: tool.name,
413
+ label: tool.label,
414
+ description: tool.description,
415
+ parameters: tool.parameters,
416
+ hidden: tool.hidden,
417
+ execute: (toolCallId, params, onUpdate, ctx, signal) =>
418
+ tool.execute(toolCallId, params, onUpdate, createCustomToolContext(ctx), signal),
419
+ onSession: tool.onSession ? (event, ctx) => tool.onSession?.(event, createCustomToolContext(ctx)) : undefined,
420
+ renderCall: tool.renderCall,
421
+ renderResult: tool.renderResult
422
+ ? (result, options, theme): Component => {
423
+ const component = tool.renderResult?.(
424
+ result,
425
+ { expanded: options.expanded, isPartial: options.isPartial, spinnerFrame: options.spinnerFrame },
426
+ theme,
427
+ );
428
+ // Return empty component if undefined to match Component type requirement
429
+ return component ?? ({ render: () => [] } as unknown as Component);
430
+ }
431
+ : undefined,
432
+ [TOOL_DEFINITION_MARKER]: true,
433
+ };
434
+ return definition;
435
+ }
448
436
 
449
- def.factory(api as any);
437
+ function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory {
438
+ return (api) => {
439
+ for (const tool of tools) {
440
+ api.registerTool(customToolToDefinition(tool));
441
+ }
450
442
 
451
- return {
452
- path: def.path ?? "<inline>",
453
- resolvedPath: def.path ?? "<inline>",
454
- handlers,
455
- messageRenderers,
456
- commands,
457
- setSendMessageHandler: (handler: (message: any, triggerTurn?: boolean) => void) => {
458
- sendMessageHandler = handler;
459
- },
460
- setAppendEntryHandler: (handler: (customType: string, data?: any) => void) => {
461
- appendEntryHandler = handler;
462
- },
463
- setNewSessionHandler: (handler: (options?: any) => Promise<{ cancelled: boolean }>) => {
464
- newSessionHandler = handler;
465
- },
466
- setBranchHandler: (handler: (entryId: string) => Promise<{ cancelled: boolean }>) => {
467
- branchHandler = handler;
468
- },
469
- setNavigateTreeHandler: (handler: (targetId: string, options?: any) => Promise<{ cancelled: boolean }>) => {
470
- navigateTreeHandler = handler;
471
- },
443
+ const runOnSession = async (event: CustomToolSessionEvent, ctx: ExtensionContext) => {
444
+ for (const tool of tools) {
445
+ if (!tool.onSession) continue;
446
+ try {
447
+ await tool.onSession(event, createCustomToolContext(ctx));
448
+ } catch (err) {
449
+ logger.warn("Custom tool onSession error", { tool: tool.name, error: String(err) });
450
+ }
451
+ }
472
452
  };
473
- });
453
+
454
+ api.on("session_start", async (_event, ctx) =>
455
+ runOnSession({ reason: "start", previousSessionFile: undefined }, ctx),
456
+ );
457
+ api.on("session_switch", async (event, ctx) =>
458
+ runOnSession({ reason: "switch", previousSessionFile: event.previousSessionFile }, ctx),
459
+ );
460
+ api.on("session_branch", async (event, ctx) =>
461
+ runOnSession({ reason: "branch", previousSessionFile: event.previousSessionFile }, ctx),
462
+ );
463
+ api.on("session_tree", async (_event, ctx) =>
464
+ runOnSession({ reason: "tree", previousSessionFile: undefined }, ctx),
465
+ );
466
+ api.on("session_shutdown", async (_event, ctx) =>
467
+ runOnSession({ reason: "shutdown", previousSessionFile: undefined }, ctx),
468
+ );
469
+ };
474
470
  }
475
471
 
476
472
  // Factory
@@ -501,7 +497,6 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa
501
497
  * getApiKey: async () => process.env.MY_KEY,
502
498
  * systemPrompt: 'You are helpful.',
503
499
  * tools: [readTool, bashTool],
504
- * hooks: [],
505
500
  * skills: [],
506
501
  * sessionManager: SessionManager.inMemory(),
507
502
  * });
@@ -510,17 +505,15 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa
510
505
  export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {
511
506
  const cwd = options.cwd ?? process.cwd();
512
507
  const agentDir = options.agentDir ?? getDefaultAgentDir();
508
+ const eventBus = options.eventBus ?? createEventBus();
513
509
 
514
510
  // Use provided or create AuthStorage and ModelRegistry
515
- const authStorage = options.authStorage ?? discoverAuthStorage(agentDir);
516
- const modelRegistry = options.modelRegistry ?? discoverModels(authStorage, agentDir);
511
+ const authStorage = options.authStorage ?? (await discoverAuthStorage(agentDir));
512
+ const modelRegistry = options.modelRegistry ?? (await discoverModels(authStorage, agentDir));
517
513
  time("discoverModels");
518
514
 
519
515
  const settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);
520
516
  time("settingsManager");
521
-
522
- // Initialize discovery system with settings for provider persistence
523
- const { initializeWithSettings } = await import("../discovery");
524
517
  initializeWithSettings(settingsManager);
525
518
  time("initializeWithSettings");
526
519
 
@@ -538,17 +531,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
538
531
  // If session has data, try to restore model from it
539
532
  const defaultModelStr = existingSession.models.default;
540
533
  if (!model && hasExistingSession && defaultModelStr) {
541
- const slashIdx = defaultModelStr.indexOf("/");
542
- if (slashIdx > 0) {
543
- const provider = defaultModelStr.slice(0, slashIdx);
544
- const modelId = defaultModelStr.slice(slashIdx + 1);
545
- const restoredModel = modelRegistry.find(provider, modelId);
534
+ const parsedModel = parseModelString(defaultModelStr);
535
+ if (parsedModel) {
536
+ const restoredModel = modelRegistry.find(parsedModel.provider, parsedModel.id);
546
537
  if (restoredModel && (await modelRegistry.getApiKey(restoredModel))) {
547
538
  model = restoredModel;
548
539
  }
549
- if (!model) {
550
- modelFallbackMessage = `Could not restore model ${defaultModelStr}`;
551
- }
540
+ }
541
+ if (!model) {
542
+ modelFallbackMessage = `Could not restore model ${defaultModelStr}`;
552
543
  }
553
544
  }
554
545
 
@@ -556,11 +547,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
556
547
  if (!model) {
557
548
  const settingsDefaultModel = settingsManager.getModelRole("default");
558
549
  if (settingsDefaultModel) {
559
- const slashIdx = settingsDefaultModel.indexOf("/");
560
- if (slashIdx > 0) {
561
- const provider = settingsDefaultModel.slice(0, slashIdx);
562
- const modelId = settingsDefaultModel.slice(slashIdx + 1);
563
- const settingsModel = modelRegistry.find(provider, modelId);
550
+ const parsedModel = parseModelString(settingsDefaultModel);
551
+ if (parsedModel) {
552
+ const settingsModel = modelRegistry.find(parsedModel.provider, parsedModel.id);
564
553
  if (settingsModel && (await modelRegistry.getApiKey(settingsModel))) {
565
554
  model = settingsModel;
566
555
  }
@@ -624,68 +613,35 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
624
613
  const contextFiles = options.contextFiles ?? discoverContextFiles(cwd, agentDir);
625
614
  time("discoverContextFiles");
626
615
 
627
- // Hook runner - always created (needed for custom command context even without hooks)
628
- let loadedHooks: LoadedHook[] = [];
629
- if (options.hooks !== undefined) {
630
- if (options.hooks.length > 0) {
631
- loadedHooks = createLoadedHooksFromDefinitions(options.hooks);
632
- }
633
- } else {
634
- // Discover hooks, merging with additional paths
635
- const configuredPaths = [...settingsManager.getHookPaths(), ...(options.additionalHookPaths ?? [])];
636
- const { hooks, errors } = await discoverAndLoadHooks(configuredPaths, cwd);
637
- time("discoverAndLoadHooks");
638
- for (const { path, error } of errors) {
639
- console.error(`Failed to load hook "${path}": ${error}`);
640
- }
641
- loadedHooks = hooks;
642
- }
643
- const hookRunner = new HookRunner(loadedHooks, cwd, sessionManager, modelRegistry);
644
-
645
616
  const sessionContext = {
646
617
  getSessionFile: () => sessionManager.getSessionFile() ?? null,
647
618
  };
648
- const builtInTools =
649
- options.tools ??
650
- createCodingTools(cwd, options.hasUI ?? false, sessionContext, {
651
- lspFormatOnWrite: settingsManager.getLspFormatOnWrite(),
652
- lspDiagnosticsOnWrite: settingsManager.getLspDiagnosticsOnWrite(),
653
- lspDiagnosticsOnEdit: settingsManager.getLspDiagnosticsOnEdit(),
654
- editFuzzyMatch: settingsManager.getEditFuzzyMatch(),
655
- });
656
- time("createCodingTools");
657
-
658
- let customToolsResult: CustomToolsLoadResult;
659
- if (options.customTools !== undefined) {
660
- // Use provided custom tools
661
- const loadedTools: LoadedCustomTool[] = options.customTools.map((ct) => ({
662
- path: ct.path ?? "<inline>",
663
- resolvedPath: ct.path ?? "<inline>",
664
- tool: ct.tool,
665
- }));
666
- customToolsResult = {
667
- tools: loadedTools,
668
- errors: [],
669
- setUIContext: () => {},
670
- };
671
- } else {
672
- // Discover custom tools, merging with additional paths
673
- const configuredPaths = [...settingsManager.getCustomToolPaths(), ...(options.additionalCustomToolPaths ?? [])];
674
- customToolsResult = await discoverAndLoadCustomTools(configuredPaths, cwd, Object.keys(allTools));
675
- time("discoverAndLoadCustomTools");
676
- for (const { path, error } of customToolsResult.errors) {
677
- console.error(`Failed to load custom tool "${path}": ${error}`);
678
- }
679
- }
619
+ const allBuiltInToolsMap = await createAllTools(cwd, sessionContext, {
620
+ lspFormatOnWrite: settingsManager.getLspFormatOnWrite(),
621
+ lspDiagnosticsOnWrite: settingsManager.getLspDiagnosticsOnWrite(),
622
+ lspDiagnosticsOnEdit: settingsManager.getLspDiagnosticsOnEdit(),
623
+ editFuzzyMatch: settingsManager.getEditFuzzyMatch(),
624
+ readAutoResizeImages: settingsManager.getImageAutoResize(),
625
+ });
626
+ time("createAllTools");
627
+
628
+ const initialActiveToolNames: ToolName[] = options.tools
629
+ ? options.tools.map((t) => t.name).filter((n): n is ToolName => n in allBuiltInToolsMap)
630
+ : baseCodingToolNames;
631
+ const initialActiveBuiltInTools = initialActiveToolNames.map((name) => allBuiltInToolsMap[name]);
680
632
 
681
633
  // Discover MCP tools from .mcp.json files
682
634
  let mcpManager: MCPManager | undefined;
683
635
  const enableMCP = options.enableMCP ?? true;
636
+ const customTools: CustomTool[] = [];
684
637
  if (enableMCP) {
685
638
  const mcpResult = await discoverAndLoadMCPTools(cwd, {
686
639
  onConnecting: (serverNames) => {
687
640
  if (options.hasUI && serverNames.length > 0) {
688
- process.stderr.write(chalk.gray(`Connecting to MCP servers: ${serverNames.join(", ")}...\n`));
641
+ process.stderr.write(
642
+ chalk.gray(`Connecting to MCP servers: ${serverNames.join(", ")}...
643
+ `),
644
+ );
689
645
  }
690
646
  },
691
647
  enableProjectConfig: settingsManager.getMCPProjectConfigEnabled(),
@@ -705,19 +661,22 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
705
661
  console.error(`MCP "${path}": ${error}`);
706
662
  }
707
663
 
708
- // Merge MCP tools into custom tools result
709
664
  if (mcpResult.tools.length > 0) {
710
- customToolsResult = {
711
- ...customToolsResult,
712
- tools: [...customToolsResult.tools, ...mcpResult.tools],
713
- };
665
+ // MCP tools are LoadedCustomTool, extract the tool property
666
+ customTools.push(...mcpResult.tools.map((loaded) => loaded.tool));
714
667
  }
715
668
  }
716
669
 
670
+ // Add Gemini image tools if GEMINI_API_KEY (or GOOGLE_API_KEY) is available
671
+ const geminiImageTools = await getGeminiImageTools();
672
+ if (geminiImageTools.length > 0) {
673
+ customTools.push(...(geminiImageTools as unknown as CustomTool[]));
674
+ }
675
+ time("getGeminiImageTools");
676
+
717
677
  // Add specialized Exa web search tools if EXA_API_KEY is available
718
678
  const exaSettings = settingsManager.getExaSettings();
719
679
  if (exaSettings.enabled && exaSettings.enableSearch) {
720
- const { getWebSearchTools } = await import("./tools/web-search/index.js");
721
680
  const exaWebSearchTools = await getWebSearchTools({
722
681
  enableLinkedin: exaSettings.enableLinkedin,
723
682
  enableCompany: exaSettings.enableCompany,
@@ -725,20 +684,85 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
725
684
  // Filter out the base web_search (already in built-in tools), add specialized Exa tools
726
685
  const specializedTools = exaWebSearchTools.filter((t) => t.name !== "web_search");
727
686
  if (specializedTools.length > 0) {
728
- const loadedExaTools: LoadedCustomTool[] = specializedTools.map((tool) => ({
729
- path: "<exa>",
730
- resolvedPath: "<exa>",
731
- tool,
732
- source: { provider: "builtin", providerName: "builtin", level: "user" },
733
- }));
734
- customToolsResult = {
735
- ...customToolsResult,
736
- tools: [...customToolsResult.tools, ...loadedExaTools],
737
- };
687
+ customTools.push(...specializedTools);
738
688
  }
739
689
  time("getWebSearchTools");
740
690
  }
741
691
 
692
+ const inlineExtensions: ExtensionFactory[] = options.extensions ? [...options.extensions] : [];
693
+ if (customTools.length > 0) {
694
+ inlineExtensions.push(createCustomToolsExtension(customTools));
695
+ }
696
+
697
+ // Load extensions (discovers from standard locations + configured paths)
698
+ let extensionsResult: LoadExtensionsResult;
699
+ if (options.preloadedExtensions !== undefined && options.preloadedExtensions.length > 0) {
700
+ extensionsResult = {
701
+ extensions: options.preloadedExtensions,
702
+ errors: [],
703
+ setUIContext: () => {},
704
+ };
705
+ } else {
706
+ // Merge CLI extension paths with settings extension paths
707
+ const configuredPaths = [...(options.additionalExtensionPaths ?? []), ...settingsManager.getExtensionPaths()];
708
+ extensionsResult = await discoverAndLoadExtensions(
709
+ configuredPaths,
710
+ cwd,
711
+ eventBus,
712
+ settingsManager.getDisabledExtensions(),
713
+ );
714
+ time("discoverAndLoadExtensions");
715
+ for (const { path, error } of extensionsResult.errors) {
716
+ console.error(`Failed to load extension "${path}": ${error}`);
717
+ }
718
+ }
719
+
720
+ // Load inline extensions from factories
721
+ if (inlineExtensions.length > 0) {
722
+ const uiHolder: { ui: any; hasUI: boolean } = {
723
+ ui: {
724
+ select: async () => undefined,
725
+ confirm: async () => false,
726
+ input: async () => undefined,
727
+ notify: () => {},
728
+ setStatus: () => {},
729
+ setWidget: () => {},
730
+ setTitle: () => {},
731
+ custom: async () => undefined as never,
732
+ setEditorText: () => {},
733
+ getEditorText: () => "",
734
+ editor: async () => undefined,
735
+ get theme() {
736
+ return {} as any;
737
+ },
738
+ },
739
+ hasUI: false,
740
+ };
741
+ for (let i = 0; i < inlineExtensions.length; i++) {
742
+ const factory = inlineExtensions[i];
743
+ const loaded = loadExtensionFromFactory(factory, cwd, eventBus, uiHolder, `<inline-${i}>`);
744
+ extensionsResult.extensions.push(loaded);
745
+ }
746
+ const originalSetUIContext = extensionsResult.setUIContext;
747
+ extensionsResult.setUIContext = (uiContext, hasUI) => {
748
+ originalSetUIContext(uiContext, hasUI);
749
+ uiHolder.ui = uiContext;
750
+ uiHolder.hasUI = hasUI;
751
+ };
752
+ }
753
+
754
+ // Discover custom commands (TypeScript slash commands)
755
+ const customCommandsResult = await loadCustomCommandsInternal({ cwd, agentDir });
756
+ time("discoverCustomCommands");
757
+ for (const { path, error } of customCommandsResult.errors) {
758
+ console.error(`Failed to load custom command "${path}": ${error}`);
759
+ }
760
+
761
+ let extensionRunner: ExtensionRunner | undefined;
762
+ if (extensionsResult.extensions.length > 0) {
763
+ extensionRunner = new ExtensionRunner(extensionsResult.extensions, cwd, sessionManager, modelRegistry);
764
+ }
765
+
742
766
  let agent: Agent;
743
767
  let session: AgentSession;
744
768
  const getSessionContext = () => ({
@@ -752,90 +776,148 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
752
776
  },
753
777
  });
754
778
  const toolContextStore = createToolContextStore(getSessionContext);
755
- const wrappedCustomTools = wrapCustomTools(customToolsResult.tools, getSessionContext);
756
- const baseSetUIContext = customToolsResult.setUIContext;
757
- customToolsResult = {
758
- ...customToolsResult,
759
- setUIContext: (uiContext, hasUI) => {
760
- toolContextStore.setUIContext(uiContext, hasUI);
761
- baseSetUIContext(uiContext, hasUI);
779
+
780
+ const registeredTools = extensionRunner?.getAllRegisteredTools() ?? [];
781
+ const allCustomTools = [
782
+ ...registeredTools,
783
+ ...(options.customTools?.map((tool) => {
784
+ const definition = isCustomTool(tool) ? customToolToDefinition(tool) : tool;
785
+ return { definition, extensionPath: "<sdk>" };
786
+ }) ?? []),
787
+ ];
788
+ const wrappedExtensionTools = wrapRegisteredTools(allCustomTools, () => ({
789
+ ui: extensionRunner?.getUIContext() ?? {
790
+ select: async () => undefined,
791
+ confirm: async () => false,
792
+ input: async () => undefined,
793
+ notify: () => {},
794
+ setStatus: () => {},
795
+ setWidget: () => {},
796
+ setTitle: () => {},
797
+ custom: async () => undefined as never,
798
+ setEditorText: () => {},
799
+ getEditorText: () => "",
800
+ editor: async () => undefined,
801
+ get theme() {
802
+ return {} as any;
803
+ },
762
804
  },
763
- };
805
+ hasUI: extensionRunner?.getHasUI() ?? false,
806
+ cwd,
807
+ sessionManager,
808
+ modelRegistry,
809
+ model: agent.state.model,
810
+ isIdle: () => !session.isStreaming,
811
+ abort: () => {
812
+ session.abort();
813
+ },
814
+ hasPendingMessages: () => session.queuedMessageCount > 0,
815
+ hasQueuedMessages: () => session.queuedMessageCount > 0,
816
+ }));
817
+
818
+ const toolRegistry = new Map<string, AgentTool>();
819
+ for (const [name, tool] of Object.entries(allBuiltInToolsMap)) {
820
+ toolRegistry.set(name, tool as AgentTool);
821
+ }
822
+ for (const tool of wrappedExtensionTools as AgentTool[]) {
823
+ toolRegistry.set(tool.name, tool);
824
+ }
764
825
 
765
- let allToolsArray: Tool[] = [...builtInTools, ...wrappedCustomTools];
826
+ let activeToolsArray: Tool[] = [...initialActiveBuiltInTools, ...wrappedExtensionTools];
766
827
 
767
- // Add rulebook tool if there are rules with descriptions (always enabled, regardless of --tools)
768
828
  if (rulebookRules.length > 0) {
769
- allToolsArray.push(createRulebookTool(rulebookRules));
829
+ activeToolsArray.push(createRulebookTool(rulebookRules));
770
830
  }
771
831
 
772
- // Filter out hidden tools unless explicitly requested
773
832
  if (options.explicitTools) {
774
833
  const explicitSet = new Set(options.explicitTools);
775
- allToolsArray = allToolsArray.filter((tool) => !tool.hidden || explicitSet.has(tool.name));
834
+ activeToolsArray = activeToolsArray.filter((tool) => !tool.hidden || explicitSet.has(tool.name));
776
835
  } else {
777
- allToolsArray = allToolsArray.filter((tool) => !tool.hidden);
836
+ activeToolsArray = activeToolsArray.filter((tool) => !tool.hidden);
778
837
  }
779
838
  time("combineTools");
780
839
 
781
- // Apply bash interception to redirect common shell patterns to proper tools (if enabled)
782
840
  if (settingsManager.getBashInterceptorEnabled()) {
783
- allToolsArray = applyBashInterception(allToolsArray);
841
+ activeToolsArray = applyBashInterception(activeToolsArray);
784
842
  }
785
843
  time("applyBashInterception");
786
844
 
787
- if (hookRunner) {
788
- allToolsArray = wrapToolsWithHooks(allToolsArray, hookRunner) as Tool[];
845
+ let wrappedToolRegistry: Map<string, AgentTool> | undefined;
846
+ if (extensionRunner) {
847
+ activeToolsArray = wrapToolsWithExtensions(activeToolsArray as AgentTool[], extensionRunner);
848
+ const allRegistryTools = Array.from(toolRegistry.values());
849
+ const wrappedAllTools = wrapToolsWithExtensions(allRegistryTools, extensionRunner);
850
+ wrappedToolRegistry = new Map<string, AgentTool>();
851
+ for (const tool of wrappedAllTools) {
852
+ wrappedToolRegistry.set(tool.name, tool);
853
+ }
789
854
  }
790
855
 
791
- let systemPrompt: string;
792
- const defaultPrompt = buildSystemPromptInternal({
793
- cwd,
794
- skills,
795
- contextFiles,
796
- rulebookRules,
797
- });
798
- time("buildSystemPrompt");
799
-
800
- if (options.systemPrompt === undefined) {
801
- systemPrompt = defaultPrompt;
802
- } else if (typeof options.systemPrompt === "string") {
803
- systemPrompt = buildSystemPromptInternal({
856
+ const rebuildSystemPrompt = (toolNames: string[]): string => {
857
+ const validToolNames = toolNames.filter((n): n is ToolName => n in allBuiltInToolsMap);
858
+ const extraToolDescriptions = toolNames
859
+ .filter((name) => !(name in allBuiltInToolsMap))
860
+ .map((name) => {
861
+ const tool = toolRegistry.get(name);
862
+ if (!tool) return null;
863
+ return { name, description: tool.description || tool.label || "Custom tool" };
864
+ })
865
+ .filter((tool): tool is { name: string; description: string } => tool !== null);
866
+ const defaultPrompt = buildSystemPromptInternal({
804
867
  cwd,
805
868
  skills,
806
869
  contextFiles,
807
870
  rulebookRules,
808
- customPrompt: options.systemPrompt,
871
+ selectedTools: validToolNames,
872
+ extraToolDescriptions,
873
+ skillsSettings: settingsManager.getSkillsSettings(),
809
874
  });
810
- } else {
811
- systemPrompt = options.systemPrompt(defaultPrompt);
812
- }
813
875
 
814
- const commandsSettings = settingsManager.getCommandsSettings();
815
- const slashCommands = options.slashCommands ?? discoverSlashCommands(cwd, agentDir, commandsSettings);
816
- time("discoverSlashCommands");
876
+ if (options.systemPrompt === undefined) {
877
+ return defaultPrompt;
878
+ }
879
+ if (typeof options.systemPrompt === "string") {
880
+ return buildSystemPromptInternal({
881
+ cwd,
882
+ skills,
883
+ contextFiles,
884
+ rulebookRules,
885
+ selectedTools: validToolNames,
886
+ extraToolDescriptions,
887
+ skillsSettings: settingsManager.getSkillsSettings(),
888
+ customPrompt: options.systemPrompt,
889
+ });
890
+ }
891
+ return options.systemPrompt(defaultPrompt);
892
+ };
817
893
 
818
- // Discover custom commands (TypeScript slash commands)
819
- const customCommandsResult = await loadCustomCommandsInternal({ cwd, agentDir });
820
- time("discoverCustomCommands");
821
- for (const { path, error } of customCommandsResult.errors) {
822
- console.error(`Failed to load custom command "${path}": ${error}`);
823
- }
894
+ const systemPrompt = rebuildSystemPrompt(initialActiveToolNames);
895
+ time("buildSystemPrompt");
896
+
897
+ const promptTemplates = options.promptTemplates ?? (await discoverPromptTemplates(cwd, agentDir));
898
+ time("discoverPromptTemplates");
899
+
900
+ const baseSetUIContext = extensionsResult.setUIContext;
901
+ extensionsResult.setUIContext = (uiContext, hasUI) => {
902
+ baseSetUIContext(uiContext, hasUI);
903
+ toolContextStore.setUIContext(uiContext, hasUI);
904
+ };
824
905
 
825
906
  agent = new Agent({
826
907
  initialState: {
827
908
  systemPrompt,
828
909
  model,
829
910
  thinkingLevel,
830
- tools: allToolsArray,
911
+ tools: activeToolsArray,
831
912
  },
832
913
  convertToLlm,
833
- transformContext: hookRunner
914
+ transformContext: extensionRunner
834
915
  ? async (messages) => {
835
- return hookRunner.emitContext(messages);
916
+ return extensionRunner.emitContext(messages);
836
917
  }
837
918
  : undefined,
838
- queueMode: settingsManager.getQueueMode(),
919
+ steeringMode: settingsManager.getSteeringMode(),
920
+ followUpMode: settingsManager.getFollowUpMode(),
839
921
  interruptMode: settingsManager.getInterruptMode(),
840
922
  getToolContext: toolContextStore.getContext,
841
923
  getApiKey: async () => {
@@ -868,12 +950,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
868
950
  sessionManager,
869
951
  settingsManager,
870
952
  scopedModels: options.scopedModels,
871
- fileCommands: slashCommands,
872
- hookRunner,
873
- customTools: customToolsResult.tools,
953
+ promptTemplates,
954
+ extensionRunner,
874
955
  customCommands: customCommandsResult.commands,
875
956
  skillsSettings: settingsManager.getSkillsSettings(),
876
957
  modelRegistry,
958
+ toolRegistry: wrappedToolRegistry ?? toolRegistry,
959
+ rebuildSystemPrompt,
877
960
  ttsrManager,
878
961
  });
879
962
  time("createAgentSession");
@@ -892,7 +975,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
892
975
 
893
976
  return {
894
977
  session,
895
- customToolsResult,
978
+ extensionsResult,
896
979
  mcpManager,
897
980
  modelFallbackMessage,
898
981
  lspServers,