@oh-my-pi/pi-coding-agent 1.337.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 (224) hide show
  1. package/CHANGELOG.md +1228 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/custom-tools.md +541 -0
  5. package/docs/extension-loading.md +1004 -0
  6. package/docs/hooks.md +867 -0
  7. package/docs/rpc.md +1040 -0
  8. package/docs/sdk.md +994 -0
  9. package/docs/session-tree-plan.md +441 -0
  10. package/docs/session.md +240 -0
  11. package/docs/skills.md +290 -0
  12. package/docs/theme.md +637 -0
  13. package/docs/tree.md +197 -0
  14. package/docs/tui.md +341 -0
  15. package/examples/README.md +21 -0
  16. package/examples/custom-tools/README.md +124 -0
  17. package/examples/custom-tools/hello/index.ts +20 -0
  18. package/examples/custom-tools/question/index.ts +84 -0
  19. package/examples/custom-tools/subagent/README.md +172 -0
  20. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  21. package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +81 -0
  57. package/src/cli/args.ts +246 -0
  58. package/src/cli/file-processor.ts +72 -0
  59. package/src/cli/list-models.ts +104 -0
  60. package/src/cli/plugin-cli.ts +650 -0
  61. package/src/cli/session-picker.ts +41 -0
  62. package/src/cli.ts +10 -0
  63. package/src/commands/init.md +20 -0
  64. package/src/config.ts +159 -0
  65. package/src/core/agent-session.ts +1900 -0
  66. package/src/core/auth-storage.ts +236 -0
  67. package/src/core/bash-executor.ts +196 -0
  68. package/src/core/compaction/branch-summarization.ts +343 -0
  69. package/src/core/compaction/compaction.ts +742 -0
  70. package/src/core/compaction/index.ts +7 -0
  71. package/src/core/compaction/utils.ts +154 -0
  72. package/src/core/custom-tools/index.ts +21 -0
  73. package/src/core/custom-tools/loader.ts +248 -0
  74. package/src/core/custom-tools/types.ts +169 -0
  75. package/src/core/custom-tools/wrapper.ts +28 -0
  76. package/src/core/exec.ts +129 -0
  77. package/src/core/export-html/index.ts +211 -0
  78. package/src/core/export-html/template.css +781 -0
  79. package/src/core/export-html/template.html +54 -0
  80. package/src/core/export-html/template.js +1185 -0
  81. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  82. package/src/core/export-html/vendor/marked.min.js +6 -0
  83. package/src/core/hooks/index.ts +16 -0
  84. package/src/core/hooks/loader.ts +312 -0
  85. package/src/core/hooks/runner.ts +434 -0
  86. package/src/core/hooks/tool-wrapper.ts +99 -0
  87. package/src/core/hooks/types.ts +773 -0
  88. package/src/core/index.ts +52 -0
  89. package/src/core/mcp/client.ts +158 -0
  90. package/src/core/mcp/config.ts +154 -0
  91. package/src/core/mcp/index.ts +45 -0
  92. package/src/core/mcp/loader.ts +68 -0
  93. package/src/core/mcp/manager.ts +181 -0
  94. package/src/core/mcp/tool-bridge.ts +148 -0
  95. package/src/core/mcp/transports/http.ts +316 -0
  96. package/src/core/mcp/transports/index.ts +6 -0
  97. package/src/core/mcp/transports/stdio.ts +252 -0
  98. package/src/core/mcp/types.ts +220 -0
  99. package/src/core/messages.ts +189 -0
  100. package/src/core/model-registry.ts +317 -0
  101. package/src/core/model-resolver.ts +393 -0
  102. package/src/core/plugins/doctor.ts +59 -0
  103. package/src/core/plugins/index.ts +38 -0
  104. package/src/core/plugins/installer.ts +189 -0
  105. package/src/core/plugins/loader.ts +338 -0
  106. package/src/core/plugins/manager.ts +672 -0
  107. package/src/core/plugins/parser.ts +105 -0
  108. package/src/core/plugins/paths.ts +32 -0
  109. package/src/core/plugins/types.ts +190 -0
  110. package/src/core/sdk.ts +760 -0
  111. package/src/core/session-manager.ts +1128 -0
  112. package/src/core/settings-manager.ts +443 -0
  113. package/src/core/skills.ts +437 -0
  114. package/src/core/slash-commands.ts +248 -0
  115. package/src/core/system-prompt.ts +439 -0
  116. package/src/core/timings.ts +25 -0
  117. package/src/core/tools/ask.ts +211 -0
  118. package/src/core/tools/bash-interceptor.ts +120 -0
  119. package/src/core/tools/bash.ts +250 -0
  120. package/src/core/tools/context.ts +32 -0
  121. package/src/core/tools/edit-diff.ts +475 -0
  122. package/src/core/tools/edit.ts +208 -0
  123. package/src/core/tools/exa/company.ts +59 -0
  124. package/src/core/tools/exa/index.ts +64 -0
  125. package/src/core/tools/exa/linkedin.ts +59 -0
  126. package/src/core/tools/exa/logger.ts +56 -0
  127. package/src/core/tools/exa/mcp-client.ts +368 -0
  128. package/src/core/tools/exa/render.ts +196 -0
  129. package/src/core/tools/exa/researcher.ts +90 -0
  130. package/src/core/tools/exa/search.ts +337 -0
  131. package/src/core/tools/exa/types.ts +168 -0
  132. package/src/core/tools/exa/websets.ts +248 -0
  133. package/src/core/tools/find.ts +261 -0
  134. package/src/core/tools/grep.ts +555 -0
  135. package/src/core/tools/index.ts +202 -0
  136. package/src/core/tools/ls.ts +140 -0
  137. package/src/core/tools/lsp/client.ts +605 -0
  138. package/src/core/tools/lsp/config.ts +147 -0
  139. package/src/core/tools/lsp/edits.ts +101 -0
  140. package/src/core/tools/lsp/index.ts +804 -0
  141. package/src/core/tools/lsp/render.ts +447 -0
  142. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  143. package/src/core/tools/lsp/types.ts +463 -0
  144. package/src/core/tools/lsp/utils.ts +486 -0
  145. package/src/core/tools/notebook.ts +229 -0
  146. package/src/core/tools/path-utils.ts +61 -0
  147. package/src/core/tools/read.ts +240 -0
  148. package/src/core/tools/renderers.ts +540 -0
  149. package/src/core/tools/task/agents.ts +153 -0
  150. package/src/core/tools/task/artifacts.ts +114 -0
  151. package/src/core/tools/task/bundled-agents/browser.md +71 -0
  152. package/src/core/tools/task/bundled-agents/explore.md +82 -0
  153. package/src/core/tools/task/bundled-agents/plan.md +54 -0
  154. package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
  155. package/src/core/tools/task/bundled-agents/task.md +53 -0
  156. package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
  157. package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
  158. package/src/core/tools/task/bundled-commands/implement.md +11 -0
  159. package/src/core/tools/task/commands.ts +213 -0
  160. package/src/core/tools/task/discovery.ts +208 -0
  161. package/src/core/tools/task/executor.ts +367 -0
  162. package/src/core/tools/task/index.ts +388 -0
  163. package/src/core/tools/task/model-resolver.ts +115 -0
  164. package/src/core/tools/task/parallel.ts +38 -0
  165. package/src/core/tools/task/render.ts +232 -0
  166. package/src/core/tools/task/types.ts +99 -0
  167. package/src/core/tools/truncate.ts +265 -0
  168. package/src/core/tools/web-fetch.ts +2370 -0
  169. package/src/core/tools/web-search/auth.ts +193 -0
  170. package/src/core/tools/web-search/index.ts +537 -0
  171. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  172. package/src/core/tools/web-search/providers/exa.ts +302 -0
  173. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  174. package/src/core/tools/web-search/render.ts +182 -0
  175. package/src/core/tools/web-search/types.ts +180 -0
  176. package/src/core/tools/write.ts +99 -0
  177. package/src/index.ts +176 -0
  178. package/src/main.ts +464 -0
  179. package/src/migrations.ts +135 -0
  180. package/src/modes/index.ts +43 -0
  181. package/src/modes/interactive/components/armin.ts +382 -0
  182. package/src/modes/interactive/components/assistant-message.ts +86 -0
  183. package/src/modes/interactive/components/bash-execution.ts +196 -0
  184. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  185. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  186. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  187. package/src/modes/interactive/components/custom-editor.ts +122 -0
  188. package/src/modes/interactive/components/diff.ts +147 -0
  189. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  190. package/src/modes/interactive/components/footer.ts +381 -0
  191. package/src/modes/interactive/components/hook-editor.ts +117 -0
  192. package/src/modes/interactive/components/hook-input.ts +64 -0
  193. package/src/modes/interactive/components/hook-message.ts +96 -0
  194. package/src/modes/interactive/components/hook-selector.ts +91 -0
  195. package/src/modes/interactive/components/model-selector.ts +247 -0
  196. package/src/modes/interactive/components/oauth-selector.ts +120 -0
  197. package/src/modes/interactive/components/plugin-settings.ts +479 -0
  198. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  199. package/src/modes/interactive/components/session-selector.ts +204 -0
  200. package/src/modes/interactive/components/settings-selector.ts +453 -0
  201. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  202. package/src/modes/interactive/components/theme-selector.ts +62 -0
  203. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  204. package/src/modes/interactive/components/tool-execution.ts +675 -0
  205. package/src/modes/interactive/components/tree-selector.ts +866 -0
  206. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  207. package/src/modes/interactive/components/user-message.ts +18 -0
  208. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  209. package/src/modes/interactive/components/welcome.ts +183 -0
  210. package/src/modes/interactive/interactive-mode.ts +2516 -0
  211. package/src/modes/interactive/theme/dark.json +101 -0
  212. package/src/modes/interactive/theme/light.json +98 -0
  213. package/src/modes/interactive/theme/theme-schema.json +308 -0
  214. package/src/modes/interactive/theme/theme.ts +998 -0
  215. package/src/modes/print-mode.ts +128 -0
  216. package/src/modes/rpc/rpc-client.ts +527 -0
  217. package/src/modes/rpc/rpc-mode.ts +483 -0
  218. package/src/modes/rpc/rpc-types.ts +203 -0
  219. package/src/utils/changelog.ts +99 -0
  220. package/src/utils/clipboard.ts +265 -0
  221. package/src/utils/fuzzy.ts +108 -0
  222. package/src/utils/mime.ts +30 -0
  223. package/src/utils/shell.ts +276 -0
  224. package/src/utils/tools-manager.ts +274 -0
@@ -0,0 +1,760 @@
1
+ /**
2
+ * SDK for programmatic usage of AgentSession.
3
+ *
4
+ * Provides a factory function and discovery helpers that allow full control
5
+ * over agent configuration, or sensible defaults that match CLI behavior.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // Minimal - everything auto-discovered
10
+ * const session = await createAgentSession();
11
+ *
12
+ * // With custom hooks
13
+ * const session = await createAgentSession({
14
+ * hooks: [
15
+ * ...await discoverHooks(),
16
+ * { factory: myHookFactory },
17
+ * ],
18
+ * });
19
+ *
20
+ * // Full control
21
+ * const session = await createAgentSession({
22
+ * model: myModel,
23
+ * getApiKey: async () => process.env.MY_KEY,
24
+ * tools: [readTool, bashTool],
25
+ * hooks: [],
26
+ * skills: [],
27
+ * sessionFile: false,
28
+ * });
29
+ * ```
30
+ */
31
+
32
+ import { Agent, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
33
+ import type { Model } from "@oh-my-pi/pi-ai";
34
+ import { join } from "path";
35
+ import { getAgentDir } from "../config.js";
36
+ import { AgentSession } from "./agent-session.js";
37
+ import { AuthStorage } from "./auth-storage.js";
38
+ import {
39
+ type CustomToolsLoadResult,
40
+ discoverAndLoadCustomTools,
41
+ type LoadedCustomTool,
42
+ wrapCustomTools,
43
+ } from "./custom-tools/index.js";
44
+ import type { CustomTool } from "./custom-tools/types.js";
45
+ import { discoverAndLoadHooks, HookRunner, type LoadedHook, wrapToolsWithHooks } from "./hooks/index.js";
46
+ import type { HookFactory } from "./hooks/types.js";
47
+ import { discoverAndLoadMCPTools, type MCPManager, type MCPToolsLoadResult } from "./mcp/index.js";
48
+ import { convertToLlm } from "./messages.js";
49
+ import { ModelRegistry } from "./model-registry.js";
50
+ import { SessionManager } from "./session-manager.js";
51
+ import { type Settings, SettingsManager, type SkillsSettings } from "./settings-manager.js";
52
+ import { loadSkills as loadSkillsInternal, type Skill } from "./skills.js";
53
+ import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal } from "./slash-commands.js";
54
+ import {
55
+ buildSystemPrompt as buildSystemPromptInternal,
56
+ loadProjectContextFiles as loadContextFilesInternal,
57
+ } from "./system-prompt.js";
58
+ import { time } from "./timings.js";
59
+ import { createToolContextStore } from "./tools/context.js";
60
+ import {
61
+ allTools,
62
+ applyBashInterception,
63
+ bashTool,
64
+ codingTools,
65
+ createBashTool,
66
+ createCodingTools,
67
+ createEditTool,
68
+ createFindTool,
69
+ createGrepTool,
70
+ createLsTool,
71
+ createReadOnlyTools,
72
+ createReadTool,
73
+ createWriteTool,
74
+ editTool,
75
+ findTool,
76
+ grepTool,
77
+ lsTool,
78
+ readOnlyTools,
79
+ readTool,
80
+ type Tool,
81
+ writeTool,
82
+ } from "./tools/index.js";
83
+
84
+ // Types
85
+
86
+ export interface CreateAgentSessionOptions {
87
+ /** Working directory for project-local discovery. Default: process.cwd() */
88
+ cwd?: string;
89
+ /** Global config directory. Default: ~/.pi/agent */
90
+ agentDir?: string;
91
+
92
+ /** Auth storage for credentials. Default: discoverAuthStorage(agentDir) */
93
+ authStorage?: AuthStorage;
94
+ /** Model registry. Default: discoverModels(authStorage, agentDir) */
95
+ modelRegistry?: ModelRegistry;
96
+
97
+ /** Model to use. Default: from settings, else first available */
98
+ model?: Model<any>;
99
+ /** Thinking level. Default: from settings, else 'off' (clamped to model capabilities) */
100
+ thinkingLevel?: ThinkingLevel;
101
+ /** Models available for cycling (Ctrl+P in interactive mode) */
102
+ scopedModels?: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }>;
103
+
104
+ /** System prompt. String replaces default, function receives default and returns final. */
105
+ systemPrompt?: string | ((defaultPrompt: string) => string);
106
+
107
+ /** Built-in tools to use. Default: codingTools [read, bash, edit, write] */
108
+ tools?: Tool[];
109
+ /** Custom tools (replaces discovery). */
110
+ customTools?: Array<{ path?: string; tool: CustomTool }>;
111
+ /** Additional custom tool paths to load (merged with discovery). */
112
+ additionalCustomToolPaths?: string[];
113
+
114
+ /** Hooks (replaces discovery). */
115
+ hooks?: Array<{ path?: string; factory: HookFactory }>;
116
+ /** Additional hook paths to load (merged with discovery). */
117
+ additionalHookPaths?: string[];
118
+
119
+ /** Skills. Default: discovered from multiple locations */
120
+ skills?: Skill[];
121
+ /** Context files (AGENTS.md content). Default: discovered walking up from cwd */
122
+ contextFiles?: Array<{ path: string; content: string }>;
123
+ /** Slash commands. Default: discovered from cwd/.pi/commands/ + agentDir/commands/ */
124
+ slashCommands?: FileSlashCommand[];
125
+
126
+ /** Enable MCP server discovery from .mcp.json files. Default: true */
127
+ enableMCP?: boolean;
128
+
129
+ /** Session manager. Default: SessionManager.create(cwd) */
130
+ sessionManager?: SessionManager;
131
+
132
+ /** Settings manager. Default: SettingsManager.create(cwd, agentDir) */
133
+ settingsManager?: SettingsManager;
134
+
135
+ /** Whether UI is available (enables interactive tools like ask). Default: false */
136
+ hasUI?: boolean;
137
+ }
138
+
139
+ /** Result from createAgentSession */
140
+ export interface CreateAgentSessionResult {
141
+ /** The created session */
142
+ session: AgentSession;
143
+ /** Custom tools result (for UI context setup in interactive mode) */
144
+ customToolsResult: CustomToolsLoadResult;
145
+ /** MCP manager for server lifecycle management (undefined if MCP disabled) */
146
+ mcpManager?: MCPManager;
147
+ /** Warning if session was restored with a different model than saved */
148
+ modelFallbackMessage?: string;
149
+ }
150
+
151
+ // Re-exports
152
+
153
+ export type { CustomTool } from "./custom-tools/types.js";
154
+ export type { HookAPI, HookCommandContext, HookContext, HookFactory } from "./hooks/types.js";
155
+ export type { MCPManager, MCPServerConfig, MCPServerConnection, MCPToolsLoadResult } from "./mcp/index.js";
156
+ export type { Settings, SkillsSettings } from "./settings-manager.js";
157
+ export type { Skill } from "./skills.js";
158
+ export type { FileSlashCommand } from "./slash-commands.js";
159
+ export type { Tool } from "./tools/index.js";
160
+
161
+ export {
162
+ // Pre-built tools (use process.cwd())
163
+ readTool,
164
+ bashTool,
165
+ editTool,
166
+ writeTool,
167
+ grepTool,
168
+ findTool,
169
+ lsTool,
170
+ codingTools,
171
+ readOnlyTools,
172
+ allTools as allBuiltInTools,
173
+ // Tool factories (for custom cwd)
174
+ createCodingTools,
175
+ createReadOnlyTools,
176
+ createReadTool,
177
+ createBashTool,
178
+ createEditTool,
179
+ createWriteTool,
180
+ createGrepTool,
181
+ createFindTool,
182
+ createLsTool,
183
+ };
184
+
185
+ // Helper Functions
186
+
187
+ function getDefaultAgentDir(): string {
188
+ return getAgentDir();
189
+ }
190
+
191
+ // Discovery Functions
192
+
193
+ /**
194
+ * Create an AuthStorage instance for the given agent directory.
195
+ */
196
+ export function discoverAuthStorage(agentDir: string = getDefaultAgentDir()): AuthStorage {
197
+ return new AuthStorage(join(agentDir, "auth.json"));
198
+ }
199
+
200
+ /**
201
+ * Create a ModelRegistry for the given agent directory.
202
+ */
203
+ export function discoverModels(authStorage: AuthStorage, agentDir: string = getDefaultAgentDir()): ModelRegistry {
204
+ return new ModelRegistry(authStorage, join(agentDir, "models.json"));
205
+ }
206
+
207
+ /**
208
+ * Discover hooks from cwd and agentDir.
209
+ */
210
+ export async function discoverHooks(
211
+ cwd?: string,
212
+ agentDir?: string,
213
+ ): Promise<Array<{ path: string; factory: HookFactory }>> {
214
+ const resolvedCwd = cwd ?? process.cwd();
215
+ const resolvedAgentDir = agentDir ?? getDefaultAgentDir();
216
+
217
+ const { hooks, errors } = await discoverAndLoadHooks([], resolvedCwd, resolvedAgentDir);
218
+
219
+ // Log errors but don't fail
220
+ for (const { path, error } of errors) {
221
+ console.error(`Failed to load hook "${path}": ${error}`);
222
+ }
223
+
224
+ return hooks.map((h) => ({
225
+ path: h.path,
226
+ factory: createFactoryFromLoadedHook(h),
227
+ }));
228
+ }
229
+
230
+ /**
231
+ * Discover custom tools from cwd and agentDir.
232
+ */
233
+ export async function discoverCustomTools(
234
+ cwd?: string,
235
+ agentDir?: string,
236
+ ): Promise<Array<{ path: string; tool: CustomTool }>> {
237
+ const resolvedCwd = cwd ?? process.cwd();
238
+ const resolvedAgentDir = agentDir ?? getDefaultAgentDir();
239
+
240
+ const { tools, errors } = await discoverAndLoadCustomTools([], resolvedCwd, Object.keys(allTools), resolvedAgentDir);
241
+
242
+ // Log errors but don't fail
243
+ for (const { path, error } of errors) {
244
+ console.error(`Failed to load custom tool "${path}": ${error}`);
245
+ }
246
+
247
+ return tools.map((t) => ({
248
+ path: t.path,
249
+ tool: t.tool,
250
+ }));
251
+ }
252
+
253
+ /**
254
+ * Discover skills from cwd and agentDir.
255
+ */
256
+ export function discoverSkills(cwd?: string, agentDir?: string, settings?: SkillsSettings): Skill[] {
257
+ const { skills } = loadSkillsInternal({
258
+ ...settings,
259
+ cwd: cwd ?? process.cwd(),
260
+ agentDir: agentDir ?? getDefaultAgentDir(),
261
+ });
262
+ return skills;
263
+ }
264
+
265
+ /**
266
+ * Discover context files (AGENTS.md) walking up from cwd.
267
+ */
268
+ export function discoverContextFiles(cwd?: string, agentDir?: string): Array<{ path: string; content: string }> {
269
+ return loadContextFilesInternal({
270
+ cwd: cwd ?? process.cwd(),
271
+ agentDir: agentDir ?? getDefaultAgentDir(),
272
+ });
273
+ }
274
+
275
+ /**
276
+ * Discover slash commands from cwd and agentDir.
277
+ */
278
+ export function discoverSlashCommands(cwd?: string, agentDir?: string): FileSlashCommand[] {
279
+ return loadSlashCommandsInternal({
280
+ cwd: cwd ?? process.cwd(),
281
+ agentDir: agentDir ?? getDefaultAgentDir(),
282
+ });
283
+ }
284
+
285
+ /**
286
+ * Discover MCP servers from .mcp.json files.
287
+ * Returns the manager and loaded tools.
288
+ */
289
+ export async function discoverMCPServers(cwd?: string): Promise<MCPToolsLoadResult> {
290
+ const resolvedCwd = cwd ?? process.cwd();
291
+ return discoverAndLoadMCPTools(resolvedCwd);
292
+ }
293
+
294
+ // API Key Helpers
295
+
296
+ // System Prompt
297
+
298
+ export interface BuildSystemPromptOptions {
299
+ tools?: Tool[];
300
+ skills?: Skill[];
301
+ contextFiles?: Array<{ path: string; content: string }>;
302
+ cwd?: string;
303
+ appendPrompt?: string;
304
+ }
305
+
306
+ /**
307
+ * Build the default system prompt.
308
+ */
309
+ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {
310
+ return buildSystemPromptInternal({
311
+ cwd: options.cwd,
312
+ skills: options.skills,
313
+ contextFiles: options.contextFiles,
314
+ appendSystemPrompt: options.appendPrompt,
315
+ });
316
+ }
317
+
318
+ // Settings
319
+
320
+ /**
321
+ * Load settings from agentDir/settings.json merged with cwd/.pi/settings.json.
322
+ */
323
+ export function loadSettings(cwd?: string, agentDir?: string): Settings {
324
+ const manager = SettingsManager.create(cwd ?? process.cwd(), agentDir ?? getDefaultAgentDir());
325
+ return {
326
+ defaultProvider: manager.getDefaultProvider(),
327
+ defaultModel: manager.getDefaultModel(),
328
+ defaultThinkingLevel: manager.getDefaultThinkingLevel(),
329
+ queueMode: manager.getQueueMode(),
330
+ theme: manager.getTheme(),
331
+ compaction: manager.getCompactionSettings(),
332
+ retry: manager.getRetrySettings(),
333
+ hideThinkingBlock: manager.getHideThinkingBlock(),
334
+ shellPath: manager.getShellPath(),
335
+ collapseChangelog: manager.getCollapseChangelog(),
336
+ hooks: manager.getHookPaths(),
337
+ customTools: manager.getCustomToolPaths(),
338
+ skills: manager.getSkillsSettings(),
339
+ terminal: { showImages: manager.getShowImages() },
340
+ };
341
+ }
342
+
343
+ // Internal Helpers
344
+
345
+ /**
346
+ * Create a HookFactory from a LoadedHook.
347
+ * This allows mixing discovered hooks with inline hooks.
348
+ */
349
+ function createFactoryFromLoadedHook(loaded: LoadedHook): HookFactory {
350
+ return (api) => {
351
+ for (const [eventType, handlers] of loaded.handlers) {
352
+ for (const handler of handlers) {
353
+ api.on(eventType as any, handler as any);
354
+ }
355
+ }
356
+ };
357
+ }
358
+
359
+ /**
360
+ * Convert hook definitions to LoadedHooks for the HookRunner.
361
+ */
362
+ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; factory: HookFactory }>): LoadedHook[] {
363
+ return definitions.map((def) => {
364
+ const handlers = new Map<string, Array<(...args: unknown[]) => Promise<unknown>>>();
365
+ const messageRenderers = new Map<string, any>();
366
+ const commands = new Map<string, any>();
367
+ let sendMessageHandler: (message: any, triggerTurn?: boolean) => void = () => {};
368
+ let appendEntryHandler: (customType: string, data?: any) => void = () => {};
369
+ let newSessionHandler: (options?: any) => Promise<{ cancelled: boolean }> = async () => ({ cancelled: false });
370
+ let branchHandler: (entryId: string) => Promise<{ cancelled: boolean }> = async () => ({ cancelled: false });
371
+ let navigateTreeHandler: (targetId: string, options?: any) => Promise<{ cancelled: boolean }> = async () => ({
372
+ cancelled: false,
373
+ });
374
+
375
+ const api = {
376
+ on: (event: string, handler: (...args: unknown[]) => Promise<unknown>) => {
377
+ const list = handlers.get(event) ?? [];
378
+ list.push(handler);
379
+ handlers.set(event, list);
380
+ },
381
+ sendMessage: (message: any, triggerTurn?: boolean) => {
382
+ sendMessageHandler(message, triggerTurn);
383
+ },
384
+ appendEntry: (customType: string, data?: any) => {
385
+ appendEntryHandler(customType, data);
386
+ },
387
+ registerMessageRenderer: (customType: string, renderer: any) => {
388
+ messageRenderers.set(customType, renderer);
389
+ },
390
+ registerCommand: (name: string, options: any) => {
391
+ commands.set(name, { name, ...options });
392
+ },
393
+ newSession: (options?: any) => newSessionHandler(options),
394
+ branch: (entryId: string) => branchHandler(entryId),
395
+ navigateTree: (targetId: string, options?: any) => navigateTreeHandler(targetId, options),
396
+ };
397
+
398
+ def.factory(api as any);
399
+
400
+ return {
401
+ path: def.path ?? "<inline>",
402
+ resolvedPath: def.path ?? "<inline>",
403
+ handlers,
404
+ messageRenderers,
405
+ commands,
406
+ setSendMessageHandler: (handler: (message: any, triggerTurn?: boolean) => void) => {
407
+ sendMessageHandler = handler;
408
+ },
409
+ setAppendEntryHandler: (handler: (customType: string, data?: any) => void) => {
410
+ appendEntryHandler = handler;
411
+ },
412
+ setNewSessionHandler: (handler: (options?: any) => Promise<{ cancelled: boolean }>) => {
413
+ newSessionHandler = handler;
414
+ },
415
+ setBranchHandler: (handler: (entryId: string) => Promise<{ cancelled: boolean }>) => {
416
+ branchHandler = handler;
417
+ },
418
+ setNavigateTreeHandler: (handler: (targetId: string, options?: any) => Promise<{ cancelled: boolean }>) => {
419
+ navigateTreeHandler = handler;
420
+ },
421
+ };
422
+ });
423
+ }
424
+
425
+ // Factory
426
+
427
+ /**
428
+ * Create an AgentSession with the specified options.
429
+ *
430
+ * @example
431
+ * ```typescript
432
+ * // Minimal - uses defaults
433
+ * const { session } = await createAgentSession();
434
+ *
435
+ * // With explicit model
436
+ * import { getModel } from '@oh-my-pi/pi-ai';
437
+ * const { session } = await createAgentSession({
438
+ * model: getModel('anthropic', 'claude-opus-4-5'),
439
+ * thinkingLevel: 'high',
440
+ * });
441
+ *
442
+ * // Continue previous session
443
+ * const { session, modelFallbackMessage } = await createAgentSession({
444
+ * continueSession: true,
445
+ * });
446
+ *
447
+ * // Full control
448
+ * const { session } = await createAgentSession({
449
+ * model: myModel,
450
+ * getApiKey: async () => process.env.MY_KEY,
451
+ * systemPrompt: 'You are helpful.',
452
+ * tools: [readTool, bashTool],
453
+ * hooks: [],
454
+ * skills: [],
455
+ * sessionManager: SessionManager.inMemory(),
456
+ * });
457
+ * ```
458
+ */
459
+ export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {
460
+ const cwd = options.cwd ?? process.cwd();
461
+ const agentDir = options.agentDir ?? getDefaultAgentDir();
462
+
463
+ // Use provided or create AuthStorage and ModelRegistry
464
+ const authStorage = options.authStorage ?? discoverAuthStorage(agentDir);
465
+ const modelRegistry = options.modelRegistry ?? discoverModels(authStorage, agentDir);
466
+ time("discoverModels");
467
+
468
+ const settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);
469
+ time("settingsManager");
470
+ const sessionManager = options.sessionManager ?? SessionManager.create(cwd);
471
+ time("sessionManager");
472
+
473
+ // Check if session has existing data to restore
474
+ const existingSession = sessionManager.buildSessionContext();
475
+ time("loadSession");
476
+ const hasExistingSession = existingSession.messages.length > 0;
477
+
478
+ let model = options.model;
479
+ let modelFallbackMessage: string | undefined;
480
+
481
+ // If session has data, try to restore model from it
482
+ if (!model && hasExistingSession && existingSession.model) {
483
+ const restoredModel = modelRegistry.find(existingSession.model.provider, existingSession.model.modelId);
484
+ if (restoredModel && (await modelRegistry.getApiKey(restoredModel))) {
485
+ model = restoredModel;
486
+ }
487
+ if (!model) {
488
+ modelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`;
489
+ }
490
+ }
491
+
492
+ // If still no model, try settings default
493
+ if (!model) {
494
+ const defaultProvider = settingsManager.getDefaultProvider();
495
+ const defaultModelId = settingsManager.getDefaultModel();
496
+ if (defaultProvider && defaultModelId) {
497
+ const settingsModel = modelRegistry.find(defaultProvider, defaultModelId);
498
+ if (settingsModel && (await modelRegistry.getApiKey(settingsModel))) {
499
+ model = settingsModel;
500
+ }
501
+ }
502
+ }
503
+
504
+ // Fall back to first available model with a valid API key
505
+ if (!model) {
506
+ for (const m of modelRegistry.getAll()) {
507
+ if (await modelRegistry.getApiKey(m)) {
508
+ model = m;
509
+ break;
510
+ }
511
+ }
512
+ time("findAvailableModel");
513
+ if (model) {
514
+ if (modelFallbackMessage) {
515
+ modelFallbackMessage += `. Using ${model.provider}/${model.id}`;
516
+ }
517
+ } else {
518
+ // No models available - set message so user knows to /login or configure keys
519
+ modelFallbackMessage = "No models available. Use /login or set an API key environment variable.";
520
+ }
521
+ }
522
+
523
+ let thinkingLevel = options.thinkingLevel;
524
+
525
+ // If session has data, restore thinking level from it
526
+ if (thinkingLevel === undefined && hasExistingSession) {
527
+ thinkingLevel = existingSession.thinkingLevel as ThinkingLevel;
528
+ }
529
+
530
+ // Fall back to settings default
531
+ if (thinkingLevel === undefined) {
532
+ thinkingLevel = settingsManager.getDefaultThinkingLevel() ?? "off";
533
+ }
534
+
535
+ // Clamp to model capabilities
536
+ if (!model || !model.reasoning) {
537
+ thinkingLevel = "off";
538
+ }
539
+
540
+ const skills = options.skills ?? discoverSkills(cwd, agentDir, settingsManager.getSkillsSettings());
541
+ time("discoverSkills");
542
+
543
+ const contextFiles = options.contextFiles ?? discoverContextFiles(cwd, agentDir);
544
+ time("discoverContextFiles");
545
+
546
+ // Hook runner - created early for hooks
547
+ let hookRunner: HookRunner | undefined;
548
+ if (options.hooks !== undefined) {
549
+ if (options.hooks.length > 0) {
550
+ const loadedHooks = createLoadedHooksFromDefinitions(options.hooks);
551
+ hookRunner = new HookRunner(loadedHooks, cwd, sessionManager, modelRegistry);
552
+ }
553
+ } else {
554
+ // Discover hooks, merging with additional paths
555
+ const configuredPaths = [...settingsManager.getHookPaths(), ...(options.additionalHookPaths ?? [])];
556
+ const { hooks, errors } = await discoverAndLoadHooks(configuredPaths, cwd, agentDir);
557
+ time("discoverAndLoadHooks");
558
+ for (const { path, error } of errors) {
559
+ console.error(`Failed to load hook "${path}": ${error}`);
560
+ }
561
+ if (hooks.length > 0) {
562
+ hookRunner = new HookRunner(hooks, cwd, sessionManager, modelRegistry);
563
+ }
564
+ }
565
+
566
+ const sessionContext = {
567
+ getSessionFile: () => sessionManager.getSessionFile() ?? null,
568
+ };
569
+ const builtInTools = options.tools ?? createCodingTools(cwd, options.hasUI ?? false, sessionContext);
570
+ time("createCodingTools");
571
+
572
+ let customToolsResult: CustomToolsLoadResult;
573
+ if (options.customTools !== undefined) {
574
+ // Use provided custom tools
575
+ const loadedTools: LoadedCustomTool[] = options.customTools.map((ct) => ({
576
+ path: ct.path ?? "<inline>",
577
+ resolvedPath: ct.path ?? "<inline>",
578
+ tool: ct.tool,
579
+ }));
580
+ customToolsResult = {
581
+ tools: loadedTools,
582
+ errors: [],
583
+ setUIContext: () => {},
584
+ };
585
+ } else {
586
+ // Discover custom tools, merging with additional paths
587
+ const configuredPaths = [...settingsManager.getCustomToolPaths(), ...(options.additionalCustomToolPaths ?? [])];
588
+ customToolsResult = await discoverAndLoadCustomTools(configuredPaths, cwd, Object.keys(allTools), agentDir);
589
+ time("discoverAndLoadCustomTools");
590
+ for (const { path, error } of customToolsResult.errors) {
591
+ console.error(`Failed to load custom tool "${path}": ${error}`);
592
+ }
593
+ }
594
+
595
+ // Discover MCP tools from .mcp.json files
596
+ let mcpManager: MCPManager | undefined;
597
+ const enableMCP = options.enableMCP ?? true;
598
+ if (enableMCP) {
599
+ const mcpResult = await discoverAndLoadMCPTools(cwd);
600
+ time("discoverAndLoadMCPTools");
601
+ mcpManager = mcpResult.manager;
602
+
603
+ // Log MCP errors
604
+ for (const { path, error } of mcpResult.errors) {
605
+ console.error(`MCP "${path}": ${error}`);
606
+ }
607
+
608
+ // Merge MCP tools into custom tools result
609
+ if (mcpResult.tools.length > 0) {
610
+ customToolsResult = {
611
+ ...customToolsResult,
612
+ tools: [...customToolsResult.tools, ...mcpResult.tools],
613
+ };
614
+ }
615
+ }
616
+
617
+ // Add specialized Exa web search tools if EXA_API_KEY is available
618
+ const exaSettings = settingsManager.getExaSettings();
619
+ if (exaSettings.enabled && exaSettings.enableSearch) {
620
+ const { getWebSearchTools } = await import("./tools/web-search/index.js");
621
+ const exaWebSearchTools = await getWebSearchTools({
622
+ enableLinkedin: exaSettings.enableLinkedin,
623
+ enableCompany: exaSettings.enableCompany,
624
+ });
625
+ // Filter out the base web_search (already in built-in tools), add specialized Exa tools
626
+ const specializedTools = exaWebSearchTools.filter((t) => t.name !== "web_search");
627
+ if (specializedTools.length > 0) {
628
+ const loadedExaTools: LoadedCustomTool[] = specializedTools.map((tool) => ({
629
+ path: "<exa>",
630
+ resolvedPath: "<exa>",
631
+ tool,
632
+ }));
633
+ customToolsResult = {
634
+ ...customToolsResult,
635
+ tools: [...customToolsResult.tools, ...loadedExaTools],
636
+ };
637
+ }
638
+ time("getWebSearchTools");
639
+ }
640
+
641
+ let agent: Agent;
642
+ let session: AgentSession;
643
+ const getSessionContext = () => ({
644
+ sessionManager,
645
+ modelRegistry,
646
+ model: agent.state.model,
647
+ isIdle: () => !session.isStreaming,
648
+ hasQueuedMessages: () => session.queuedMessageCount > 0,
649
+ abort: () => {
650
+ session.abort();
651
+ },
652
+ });
653
+ const toolContextStore = createToolContextStore(getSessionContext);
654
+ const wrappedCustomTools = wrapCustomTools(customToolsResult.tools, getSessionContext);
655
+ const baseSetUIContext = customToolsResult.setUIContext;
656
+ customToolsResult = {
657
+ ...customToolsResult,
658
+ setUIContext: (uiContext, hasUI) => {
659
+ toolContextStore.setUIContext(uiContext, hasUI);
660
+ baseSetUIContext(uiContext, hasUI);
661
+ },
662
+ };
663
+
664
+ let allToolsArray: Tool[] = [...builtInTools, ...wrappedCustomTools];
665
+ time("combineTools");
666
+
667
+ // Apply bash interception to redirect common shell patterns to proper tools
668
+ allToolsArray = applyBashInterception(allToolsArray);
669
+ time("applyBashInterception");
670
+
671
+ if (hookRunner) {
672
+ allToolsArray = wrapToolsWithHooks(allToolsArray, hookRunner) as Tool[];
673
+ }
674
+
675
+ let systemPrompt: string;
676
+ const defaultPrompt = buildSystemPromptInternal({
677
+ cwd,
678
+ agentDir,
679
+ skills,
680
+ contextFiles,
681
+ });
682
+ time("buildSystemPrompt");
683
+
684
+ if (options.systemPrompt === undefined) {
685
+ systemPrompt = defaultPrompt;
686
+ } else if (typeof options.systemPrompt === "string") {
687
+ systemPrompt = buildSystemPromptInternal({
688
+ cwd,
689
+ agentDir,
690
+ skills,
691
+ contextFiles,
692
+ customPrompt: options.systemPrompt,
693
+ });
694
+ } else {
695
+ systemPrompt = options.systemPrompt(defaultPrompt);
696
+ }
697
+
698
+ const slashCommands = options.slashCommands ?? discoverSlashCommands(cwd, agentDir);
699
+ time("discoverSlashCommands");
700
+
701
+ agent = new Agent({
702
+ initialState: {
703
+ systemPrompt,
704
+ model,
705
+ thinkingLevel,
706
+ tools: allToolsArray,
707
+ },
708
+ convertToLlm,
709
+ transformContext: hookRunner
710
+ ? async (messages) => {
711
+ return hookRunner.emitContext(messages);
712
+ }
713
+ : undefined,
714
+ queueMode: settingsManager.getQueueMode(),
715
+ getToolContext: toolContextStore.getContext,
716
+ getApiKey: async () => {
717
+ const currentModel = agent.state.model;
718
+ if (!currentModel) {
719
+ throw new Error("No model selected");
720
+ }
721
+ const key = await modelRegistry.getApiKey(currentModel);
722
+ if (!key) {
723
+ throw new Error(`No API key found for provider "${currentModel.provider}"`);
724
+ }
725
+ return key;
726
+ },
727
+ });
728
+ time("createAgent");
729
+
730
+ // Restore messages if session has existing data
731
+ if (hasExistingSession) {
732
+ agent.replaceMessages(existingSession.messages);
733
+ } else {
734
+ // Save initial model and thinking level for new sessions so they can be restored on resume
735
+ if (model) {
736
+ sessionManager.appendModelChange(model.provider, model.id);
737
+ }
738
+ sessionManager.appendThinkingLevelChange(thinkingLevel);
739
+ }
740
+
741
+ session = new AgentSession({
742
+ agent,
743
+ sessionManager,
744
+ settingsManager,
745
+ scopedModels: options.scopedModels,
746
+ fileCommands: slashCommands,
747
+ hookRunner,
748
+ customTools: customToolsResult.tools,
749
+ skillsSettings: settingsManager.getSkillsSettings(),
750
+ modelRegistry,
751
+ });
752
+ time("createAgentSession");
753
+
754
+ return {
755
+ session,
756
+ customToolsResult,
757
+ mcpManager,
758
+ modelFallbackMessage,
759
+ };
760
+ }