@oh-my-pi/pi-coding-agent 12.18.3 → 12.19.2

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 (233) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/package.json +35 -27
  3. package/src/async/index.ts +1 -0
  4. package/src/async/job-manager.ts +341 -0
  5. package/src/cli/file-processor.ts +3 -3
  6. package/src/cli/list-models.ts +3 -17
  7. package/src/cli/stats-cli.ts +3 -22
  8. package/src/cli/web-search-cli.ts +8 -16
  9. package/src/commit/agentic/agent.ts +6 -9
  10. package/src/commit/agentic/index.ts +44 -50
  11. package/src/commit/agentic/state.ts +0 -9
  12. package/src/commit/agentic/tools/propose-commit.ts +1 -30
  13. package/src/commit/agentic/tools/schemas.ts +31 -0
  14. package/src/commit/agentic/tools/split-commit.ts +1 -30
  15. package/src/commit/agentic/validation.ts +1 -18
  16. package/src/commit/analysis/conventional.ts +3 -50
  17. package/src/commit/analysis/summary.ts +2 -13
  18. package/src/commit/changelog/detect.ts +4 -1
  19. package/src/commit/changelog/generate.ts +2 -25
  20. package/src/commit/changelog/index.ts +1 -2
  21. package/src/commit/cli.ts +4 -12
  22. package/src/commit/map-reduce/reduce-phase.ts +2 -43
  23. package/src/commit/pipeline.ts +7 -15
  24. package/src/commit/utils.ts +44 -0
  25. package/src/config/prompt-templates.ts +1 -81
  26. package/src/config/settings-schema.ts +20 -1
  27. package/src/config.ts +2 -3
  28. package/src/debug/index.ts +1 -6
  29. package/src/debug/system-info.ts +2 -6
  30. package/src/discovery/builtin.ts +5 -9
  31. package/src/discovery/helpers.ts +0 -26
  32. package/src/discovery/ssh.ts +1 -8
  33. package/src/exa/company.ts +8 -39
  34. package/src/exa/factory.ts +64 -0
  35. package/src/exa/index.ts +0 -16
  36. package/src/exa/linkedin.ts +8 -39
  37. package/src/exa/mcp-client.ts +0 -64
  38. package/src/exa/researcher.ts +17 -59
  39. package/src/exa/search.ts +30 -154
  40. package/src/extensibility/custom-tools/loader.ts +3 -41
  41. package/src/extensibility/extensions/loader.ts +2 -9
  42. package/src/extensibility/hooks/loader.ts +3 -20
  43. package/src/extensibility/hooks/runner.ts +3 -19
  44. package/src/extensibility/plugins/installer.ts +2 -1
  45. package/src/extensibility/plugins/loader.ts +29 -117
  46. package/src/extensibility/skills.ts +2 -89
  47. package/src/extensibility/slash-commands.ts +1 -63
  48. package/src/extensibility/utils.ts +38 -0
  49. package/src/index.ts +9 -25
  50. package/src/internal-urls/index.ts +1 -0
  51. package/src/internal-urls/jobs-protocol.ts +118 -0
  52. package/src/ipy/kernel.ts +2 -0
  53. package/src/lsp/config.ts +1 -5
  54. package/src/lsp/lspmux.ts +0 -17
  55. package/src/lsp/utils.ts +2 -24
  56. package/src/main.ts +16 -24
  57. package/src/mcp/client.ts +1 -46
  58. package/src/mcp/render.ts +8 -1
  59. package/src/mcp/tool-cache.ts +1 -5
  60. package/src/mcp/transports/http.ts +2 -7
  61. package/src/mcp/transports/stdio.ts +2 -7
  62. package/src/modes/components/bash-execution.ts +2 -16
  63. package/src/modes/components/extensions/inspector-panel.ts +8 -18
  64. package/src/modes/components/footer.ts +10 -50
  65. package/src/modes/components/model-selector.ts +2 -21
  66. package/src/modes/components/python-execution.ts +2 -16
  67. package/src/modes/components/settings-selector.ts +1 -10
  68. package/src/modes/components/status-line/segments.ts +8 -25
  69. package/src/modes/components/status-line.ts +14 -31
  70. package/src/modes/components/tool-execution.ts +8 -2
  71. package/src/modes/controllers/command-controller.ts +71 -30
  72. package/src/modes/controllers/event-controller.ts +34 -4
  73. package/src/modes/controllers/mcp-command-controller.ts +3 -34
  74. package/src/modes/controllers/selector-controller.ts +2 -2
  75. package/src/modes/controllers/ssh-command-controller.ts +3 -34
  76. package/src/modes/interactive-mode.ts +6 -2
  77. package/src/modes/rpc/rpc-client.ts +1 -5
  78. package/src/modes/shared.ts +73 -0
  79. package/src/modes/types.ts +1 -0
  80. package/src/modes/utils/ui-helpers.ts +26 -2
  81. package/src/patch/hashline.ts +6 -286
  82. package/src/patch/index.ts +6 -57
  83. package/src/patch/normalize.ts +22 -65
  84. package/src/patch/shared.ts +16 -16
  85. package/src/prompts/system/custom-system-prompt.md +0 -10
  86. package/src/prompts/system/system-prompt.md +69 -89
  87. package/src/prompts/tools/async-result.md +5 -0
  88. package/src/prompts/tools/bash.md +5 -0
  89. package/src/prompts/tools/cancel-job.md +7 -0
  90. package/src/prompts/tools/hashline.md +0 -16
  91. package/src/prompts/tools/poll-jobs.md +7 -0
  92. package/src/prompts/tools/task.md +4 -0
  93. package/src/sdk.ts +70 -6
  94. package/src/session/agent-session.ts +43 -6
  95. package/src/session/agent-storage.ts +69 -278
  96. package/src/session/auth-storage.ts +14 -1430
  97. package/src/session/session-manager.ts +69 -5
  98. package/src/session/session-storage.ts +1 -5
  99. package/src/session/streaming-output.ts +637 -76
  100. package/src/slash-commands/builtin-registry.ts +8 -0
  101. package/src/ssh/connection-manager.ts +4 -12
  102. package/src/ssh/sshfs-mount.ts +3 -7
  103. package/src/ssh/utils.ts +8 -0
  104. package/src/system-prompt.ts +24 -90
  105. package/src/task/executor.ts +11 -1
  106. package/src/task/index.ts +258 -13
  107. package/src/task/parallel.ts +32 -0
  108. package/src/task/render.ts +15 -7
  109. package/src/task/types.ts +5 -0
  110. package/src/tools/ask.ts +4 -7
  111. package/src/tools/bash-interactive.ts +4 -5
  112. package/src/tools/bash.ts +125 -41
  113. package/src/tools/cancel-job.ts +93 -0
  114. package/src/tools/fetch.ts +7 -27
  115. package/src/tools/find.ts +3 -3
  116. package/src/tools/gemini-image.ts +15 -14
  117. package/src/tools/grep.ts +3 -3
  118. package/src/tools/index.ts +13 -29
  119. package/src/tools/json-tree.ts +12 -1
  120. package/src/tools/jtd-to-json-schema.ts +10 -74
  121. package/src/tools/jtd-to-typescript.ts +10 -72
  122. package/src/tools/jtd-utils.ts +102 -0
  123. package/src/tools/notebook.ts +4 -9
  124. package/src/tools/output-meta.ts +52 -26
  125. package/src/tools/path-utils.ts +13 -7
  126. package/src/tools/poll-jobs.ts +178 -0
  127. package/src/tools/python.ts +32 -35
  128. package/src/tools/read.ts +61 -82
  129. package/src/tools/render-utils.ts +8 -159
  130. package/src/tools/ssh.ts +7 -20
  131. package/src/tools/submit-result.ts +1 -1
  132. package/src/tools/tool-errors.ts +0 -30
  133. package/src/tools/tool-result.ts +1 -2
  134. package/src/tools/write.ts +8 -10
  135. package/src/tui/code-cell.ts +8 -3
  136. package/src/tui/status-line.ts +4 -4
  137. package/src/tui/types.ts +0 -1
  138. package/src/tui/utils.ts +1 -14
  139. package/src/utils/command-args.ts +76 -0
  140. package/src/utils/file-mentions.ts +15 -19
  141. package/src/utils/frontmatter.ts +5 -10
  142. package/src/utils/shell-snapshot.ts +0 -11
  143. package/src/utils/title-generator.ts +0 -12
  144. package/src/web/scrapers/artifacthub.ts +7 -16
  145. package/src/web/scrapers/arxiv.ts +3 -8
  146. package/src/web/scrapers/aur.ts +8 -22
  147. package/src/web/scrapers/biorxiv.ts +5 -14
  148. package/src/web/scrapers/bluesky.ts +13 -36
  149. package/src/web/scrapers/brew.ts +5 -10
  150. package/src/web/scrapers/cheatsh.ts +2 -12
  151. package/src/web/scrapers/chocolatey.ts +63 -26
  152. package/src/web/scrapers/choosealicense.ts +3 -18
  153. package/src/web/scrapers/cisa-kev.ts +4 -18
  154. package/src/web/scrapers/clojars.ts +6 -33
  155. package/src/web/scrapers/coingecko.ts +25 -33
  156. package/src/web/scrapers/crates-io.ts +7 -26
  157. package/src/web/scrapers/crossref.ts +4 -18
  158. package/src/web/scrapers/devto.ts +11 -41
  159. package/src/web/scrapers/discogs.ts +7 -10
  160. package/src/web/scrapers/discourse.ts +6 -31
  161. package/src/web/scrapers/dockerhub.ts +12 -35
  162. package/src/web/scrapers/fdroid.ts +8 -33
  163. package/src/web/scrapers/firefox-addons.ts +10 -34
  164. package/src/web/scrapers/flathub.ts +7 -24
  165. package/src/web/scrapers/github-gist.ts +2 -12
  166. package/src/web/scrapers/github.ts +9 -47
  167. package/src/web/scrapers/gitlab.ts +130 -185
  168. package/src/web/scrapers/go-pkg.ts +12 -22
  169. package/src/web/scrapers/hackage.ts +88 -43
  170. package/src/web/scrapers/hackernews.ts +25 -45
  171. package/src/web/scrapers/hex.ts +19 -36
  172. package/src/web/scrapers/huggingface.ts +26 -91
  173. package/src/web/scrapers/iacr.ts +3 -8
  174. package/src/web/scrapers/jetbrains-marketplace.ts +9 -20
  175. package/src/web/scrapers/lemmy.ts +5 -23
  176. package/src/web/scrapers/lobsters.ts +16 -28
  177. package/src/web/scrapers/mastodon.ts +24 -43
  178. package/src/web/scrapers/maven.ts +6 -21
  179. package/src/web/scrapers/mdn.ts +7 -11
  180. package/src/web/scrapers/metacpan.ts +9 -41
  181. package/src/web/scrapers/musicbrainz.ts +4 -28
  182. package/src/web/scrapers/npm.ts +8 -25
  183. package/src/web/scrapers/nuget.ts +14 -37
  184. package/src/web/scrapers/nvd.ts +6 -28
  185. package/src/web/scrapers/ollama.ts +7 -34
  186. package/src/web/scrapers/open-vsx.ts +5 -19
  187. package/src/web/scrapers/opencorporates.ts +30 -14
  188. package/src/web/scrapers/openlibrary.ts +49 -33
  189. package/src/web/scrapers/orcid.ts +4 -18
  190. package/src/web/scrapers/osv.ts +7 -24
  191. package/src/web/scrapers/packagist.ts +9 -24
  192. package/src/web/scrapers/pub-dev.ts +7 -50
  193. package/src/web/scrapers/pubmed.ts +54 -21
  194. package/src/web/scrapers/pypi.ts +8 -26
  195. package/src/web/scrapers/rawg.ts +11 -19
  196. package/src/web/scrapers/readthedocs.ts +4 -9
  197. package/src/web/scrapers/reddit.ts +5 -15
  198. package/src/web/scrapers/repology.ts +8 -20
  199. package/src/web/scrapers/rfc.ts +5 -14
  200. package/src/web/scrapers/rubygems.ts +6 -21
  201. package/src/web/scrapers/searchcode.ts +8 -36
  202. package/src/web/scrapers/sec-edgar.ts +4 -18
  203. package/src/web/scrapers/semantic-scholar.ts +15 -35
  204. package/src/web/scrapers/snapcraft.ts +5 -19
  205. package/src/web/scrapers/sourcegraph.ts +5 -43
  206. package/src/web/scrapers/spdx.ts +4 -18
  207. package/src/web/scrapers/spotify.ts +4 -23
  208. package/src/web/scrapers/stackoverflow.ts +8 -13
  209. package/src/web/scrapers/terraform.ts +9 -37
  210. package/src/web/scrapers/tldr.ts +3 -7
  211. package/src/web/scrapers/twitter.ts +3 -7
  212. package/src/web/scrapers/types.ts +105 -27
  213. package/src/web/scrapers/utils.ts +97 -103
  214. package/src/web/scrapers/vimeo.ts +7 -27
  215. package/src/web/scrapers/vscode-marketplace.ts +8 -17
  216. package/src/web/scrapers/w3c.ts +6 -14
  217. package/src/web/scrapers/wikidata.ts +5 -19
  218. package/src/web/scrapers/wikipedia.ts +2 -12
  219. package/src/web/scrapers/youtube.ts +5 -34
  220. package/src/web/search/index.ts +0 -9
  221. package/src/web/search/providers/anthropic.ts +3 -2
  222. package/src/web/search/providers/brave.ts +3 -18
  223. package/src/web/search/providers/exa.ts +1 -12
  224. package/src/web/search/providers/kimi.ts +5 -44
  225. package/src/web/search/providers/perplexity.ts +1 -12
  226. package/src/web/search/providers/synthetic.ts +3 -26
  227. package/src/web/search/providers/utils.ts +36 -0
  228. package/src/web/search/providers/zai.ts +9 -50
  229. package/src/web/search/types.ts +0 -28
  230. package/src/web/search/utils.ts +17 -0
  231. package/src/tools/output-utils.ts +0 -63
  232. package/src/tools/truncate.ts +0 -385
  233. package/src/web/search/auth.ts +0 -178
@@ -5,53 +5,22 @@
5
5
  */
6
6
  import { Type } from "@sinclair/typebox";
7
7
  import type { CustomTool } from "../extensibility/custom-tools/types";
8
- import { callExaTool, findApiKey, formatSearchResults, isSearchResponse } from "./mcp-client";
8
+ import { createExaTool } from "./factory";
9
9
  import type { ExaRenderDetails } from "./types";
10
10
 
11
11
  /** exa_linkedin - LinkedIn search */
12
- export const linkedinTool: CustomTool<any, ExaRenderDetails> = {
13
- name: "exa_linkedin",
14
- label: "Exa LinkedIn",
15
- description: `Search LinkedIn for people, companies, and professional content using Exa.
12
+ export const linkedinTool: CustomTool<any, ExaRenderDetails> = createExaTool(
13
+ "exa_linkedin",
14
+ "Exa LinkedIn",
15
+ `Search LinkedIn for people, companies, and professional content using Exa.
16
16
 
17
17
  Returns LinkedIn search results with profiles, posts, and company information.
18
18
 
19
19
  Parameters:
20
20
  - query: LinkedIn search query (e.g., "Software Engineer at OpenAI", "Y Combinator companies")`,
21
21
 
22
- parameters: Type.Object({
22
+ Type.Object({
23
23
  query: Type.String({ description: "LinkedIn search query" }),
24
24
  }),
25
-
26
- async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
27
- try {
28
- const apiKey = await findApiKey();
29
- if (!apiKey) {
30
- return {
31
- content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
32
- details: { error: "EXA_API_KEY not found", toolName: "exa_linkedin" },
33
- };
34
- }
35
- const response = await callExaTool("linkedin_search", params, apiKey);
36
-
37
- if (isSearchResponse(response)) {
38
- const formatted = formatSearchResults(response);
39
- return {
40
- content: [{ type: "text" as const, text: formatted }],
41
- details: { response, toolName: "exa_linkedin" },
42
- };
43
- }
44
-
45
- return {
46
- content: [{ type: "text" as const, text: JSON.stringify(response, null, 2) }],
47
- details: { raw: response, toolName: "exa_linkedin" },
48
- };
49
- } catch (error) {
50
- const message = error instanceof Error ? error.message : String(error);
51
- return {
52
- content: [{ type: "text" as const, text: `Error: ${message}` }],
53
- details: { error: message, toolName: "exa_linkedin" },
54
- };
55
- }
56
- },
57
- };
25
+ "linkedin_search",
26
+ );
@@ -5,7 +5,6 @@ import { callMCP } from "../mcp/json-rpc";
5
5
  import type {
6
6
  ExaRenderDetails,
7
7
  ExaSearchResponse,
8
- ExaSearchResult,
9
8
  MCPCallResponse,
10
9
  MCPTool,
11
10
  MCPToolsResponse,
@@ -79,69 +78,6 @@ export async function callWebsetsTool(
79
78
  return response.result;
80
79
  }
81
80
 
82
- /** Parse Exa markdown format into SearchResponse */
83
- export function parseExaMarkdown(text: string): ExaSearchResponse | null {
84
- const results: ExaSearchResult[] = [];
85
- const lines = text.split("\n");
86
- let currentResult: Partial<ExaSearchResult> | null = null;
87
-
88
- for (let i = 0; i < lines.length; i++) {
89
- const line = lines[i].trim();
90
-
91
- // Match result header: ## Title
92
- if (line.startsWith("## ")) {
93
- if (currentResult?.title) {
94
- results.push(currentResult as ExaSearchResult);
95
- }
96
- currentResult = { title: line.slice(3).trim() };
97
- continue;
98
- }
99
-
100
- if (!currentResult) continue;
101
-
102
- // Match URL: **URL:** ...
103
- if (line.startsWith("**URL:**")) {
104
- currentResult.url = line.slice(8).trim();
105
- continue;
106
- }
107
-
108
- // Match Author: **Author:** ...
109
- if (line.startsWith("**Author:**")) {
110
- currentResult.author = line.slice(11).trim();
111
- continue;
112
- }
113
-
114
- // Match Published Date: **Published Date:** ...
115
- if (line.startsWith("**Published Date:**")) {
116
- currentResult.publishedDate = line.slice(19).trim();
117
- continue;
118
- }
119
-
120
- // Match Text: **Text:** ...
121
- if (line.startsWith("**Text:**")) {
122
- currentResult.text = line.slice(9).trim();
123
- continue;
124
- }
125
-
126
- // Accumulate text content
127
- if (currentResult.text && line && !line.startsWith("**")) {
128
- currentResult.text += ` ${line}`;
129
- }
130
- }
131
-
132
- // Add last result
133
- if (currentResult?.title) {
134
- results.push(currentResult as ExaSearchResult);
135
- }
136
-
137
- if (results.length === 0) return null;
138
-
139
- return {
140
- results,
141
- statuses: results.map((r, i) => ({ id: r.id ?? `result-${i}`, status: "success" })),
142
- };
143
- }
144
-
145
81
  /** Format search results for LLM */
146
82
  export function formatSearchResults(data: ExaSearchResponse): string {
147
83
  const results = data.results ?? [];
@@ -5,15 +5,14 @@
5
5
  */
6
6
  import { Type } from "@sinclair/typebox";
7
7
  import type { CustomTool } from "../extensibility/custom-tools/types";
8
- import { callExaTool, findApiKey } from "./mcp-client";
8
+ import { createExaTool } from "./factory";
9
9
  import type { ExaRenderDetails } from "./types";
10
10
 
11
- const researcherStartTool: CustomTool<any, ExaRenderDetails> = {
12
- name: "exa_researcher_start",
13
- label: "Start Deep Research",
14
- description:
15
- "Start an asynchronous deep research task using Exa's researcher. Returns a task_id for polling completion.",
16
- parameters: Type.Object({
11
+ const researcherStartTool = createExaTool(
12
+ "exa_researcher_start",
13
+ "Start Deep Research",
14
+ "Start an asynchronous deep research task using Exa's researcher. Returns a task_id for polling completion.",
15
+ Type.Object({
17
16
  query: Type.String({ description: "Research query to investigate" }),
18
17
  depth: Type.Optional(
19
18
  Type.Number({
@@ -30,60 +29,19 @@ const researcherStartTool: CustomTool<any, ExaRenderDetails> = {
30
29
  }),
31
30
  ),
32
31
  }),
33
- async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
34
- try {
35
- const apiKey = await findApiKey();
36
- if (!apiKey) {
37
- return {
38
- content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
39
- details: { error: "EXA_API_KEY not found", toolName: "exa_researcher_start" },
40
- };
41
- }
42
- const result = await callExaTool("deep_researcher_start", params as Record<string, unknown>, apiKey);
43
- return {
44
- content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
45
- details: { raw: result, toolName: "exa_researcher_start" },
46
- };
47
- } catch (error) {
48
- const message = error instanceof Error ? error.message : String(error);
49
- return {
50
- content: [{ type: "text" as const, text: `Error: ${message}` }],
51
- details: { error: message, toolName: "exa_researcher_start" },
52
- };
53
- }
54
- },
55
- };
32
+ "deep_researcher_start",
33
+ { formatResponse: false },
34
+ );
56
35
 
57
- const researcherPollTool: CustomTool<any, ExaRenderDetails> = {
58
- name: "exa_researcher_poll",
59
- label: "Poll Research Status",
60
- description:
61
- "Poll the status of an asynchronous research task. Returns status (pending|running|completed|failed) and result if completed.",
62
- parameters: Type.Object({
36
+ const researcherPollTool = createExaTool(
37
+ "exa_researcher_poll",
38
+ "Poll Research Status",
39
+ "Poll the status of an asynchronous research task. Returns status (pending|running|completed|failed) and result if completed.",
40
+ Type.Object({
63
41
  task_id: Type.String({ description: "Task ID returned from exa_researcher_start" }),
64
42
  }),
65
- async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
66
- try {
67
- const apiKey = await findApiKey();
68
- if (!apiKey) {
69
- return {
70
- content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
71
- details: { error: "EXA_API_KEY not found", toolName: "exa_researcher_poll" },
72
- };
73
- }
74
- const result = await callExaTool("deep_researcher_check", params as Record<string, unknown>, apiKey);
75
- return {
76
- content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
77
- details: { raw: result, toolName: "exa_researcher_poll" },
78
- };
79
- } catch (error) {
80
- const message = error instanceof Error ? error.message : String(error);
81
- return {
82
- content: [{ type: "text" as const, text: `Error: ${message}` }],
83
- details: { error: message, toolName: "exa_researcher_poll" },
84
- };
85
- }
86
- },
87
- };
43
+ "deep_researcher_check",
44
+ { formatResponse: false },
45
+ );
88
46
 
89
47
  export const researcherTools: CustomTool<any, ExaRenderDetails>[] = [researcherStartTool, researcherPollTool];
package/src/exa/search.ts CHANGED
@@ -6,14 +6,14 @@
6
6
  import { StringEnum } from "@oh-my-pi/pi-ai";
7
7
  import { Type } from "@sinclair/typebox";
8
8
  import type { CustomTool } from "../extensibility/custom-tools/types";
9
- import { callExaTool, findApiKey, formatSearchResults, isSearchResponse } from "./mcp-client";
9
+ import { createExaTool } from "./factory";
10
10
  import type { ExaRenderDetails } from "./types";
11
11
 
12
12
  /** exa_search - Basic neural/keyword search */
13
- const exaSearchTool: CustomTool<any, ExaRenderDetails> = {
14
- name: "exa_search",
15
- label: "Exa Search",
16
- description: `Search the web using Exa's neural or keyword search.
13
+ const exaSearchTool = createExaTool(
14
+ "exa_search",
15
+ "Exa Search",
16
+ `Search the web using Exa's neural or keyword search.
17
17
 
18
18
  Returns structured search results with optional text content and highlights.
19
19
 
@@ -29,7 +29,7 @@ Parameters:
29
29
  - highlights: Include highlighted relevant snippets (default: false)
30
30
  - num_results: Maximum number of results to return (default: 10, max: 100)`,
31
31
 
32
- parameters: Type.Object({
32
+ Type.Object({
33
33
  query: Type.String({ description: "Search query" }),
34
34
  type: Type.Optional(
35
35
  StringEnum(["keyword", "neural", "auto"], {
@@ -79,51 +79,20 @@ Parameters:
79
79
  }),
80
80
  ),
81
81
  }),
82
-
83
- async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
84
- try {
85
- const apiKey = await findApiKey();
86
- if (!apiKey) {
87
- return {
88
- content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
89
- details: { error: "EXA_API_KEY not found", toolName: "exa_search" },
90
- };
91
- }
92
- const response = await callExaTool("web_search_exa", params, apiKey);
93
-
94
- if (isSearchResponse(response)) {
95
- const formatted = formatSearchResults(response);
96
- return {
97
- content: [{ type: "text" as const, text: formatted }],
98
- details: { response, toolName: "exa_search" },
99
- };
100
- }
101
-
102
- return {
103
- content: [{ type: "text" as const, text: JSON.stringify(response, null, 2) }],
104
- details: { raw: response, toolName: "exa_search" },
105
- };
106
- } catch (error) {
107
- const message = error instanceof Error ? error.message : String(error);
108
- return {
109
- content: [{ type: "text" as const, text: `Error: ${message}` }],
110
- details: { error: message, toolName: "exa_search" },
111
- };
112
- }
113
- },
114
- };
82
+ "web_search_exa",
83
+ );
115
84
 
116
85
  /** exa_search_deep - AI-synthesized deep research */
117
- const exaSearchDeepTool: CustomTool<any, ExaRenderDetails> = {
118
- name: "exa_search_deep",
119
- label: "Exa Deep Search",
120
- description: `Perform AI-synthesized deep research using Exa.
86
+ const exaSearchDeepTool = createExaTool(
87
+ "exa_search_deep",
88
+ "Exa Deep Search",
89
+ `Perform AI-synthesized deep research using Exa.
121
90
 
122
91
  Returns comprehensive research with synthesized answers and multiple sources.
123
92
 
124
93
  Similar parameters to exa_search, optimized for research depth.`,
125
94
 
126
- parameters: Type.Object({
95
+ Type.Object({
127
96
  query: Type.String({ description: "Research query" }),
128
97
  type: Type.Optional(
129
98
  StringEnum(["keyword", "neural", "auto"], {
@@ -173,46 +142,15 @@ Similar parameters to exa_search, optimized for research depth.`,
173
142
  }),
174
143
  ),
175
144
  }),
176
-
177
- async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
178
- try {
179
- const apiKey = await findApiKey();
180
- if (!apiKey) {
181
- return {
182
- content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
183
- details: { error: "EXA_API_KEY not found", toolName: "exa_search_deep" },
184
- };
185
- }
186
- const args = { ...params, type: "deep" };
187
- const response = await callExaTool("web_search_exa", args, apiKey);
188
-
189
- if (isSearchResponse(response)) {
190
- const formatted = formatSearchResults(response);
191
- return {
192
- content: [{ type: "text" as const, text: formatted }],
193
- details: { response, toolName: "exa_search_deep" },
194
- };
195
- }
196
-
197
- return {
198
- content: [{ type: "text" as const, text: JSON.stringify(response, null, 2) }],
199
- details: { raw: response, toolName: "exa_search_deep" },
200
- };
201
- } catch (error) {
202
- const message = error instanceof Error ? error.message : String(error);
203
- return {
204
- content: [{ type: "text" as const, text: `Error: ${message}` }],
205
- details: { error: message, toolName: "exa_search_deep" },
206
- };
207
- }
208
- },
209
- };
145
+ "web_search_exa",
146
+ { transformParams: params => ({ ...params, type: "deep" }) },
147
+ );
210
148
 
211
149
  /** exa_search_code - Code-focused search */
212
- const exaSearchCodeTool: CustomTool<any, ExaRenderDetails> = {
213
- name: "exa_search_code",
214
- label: "Exa Code Search",
215
- description: `Search for code examples and technical documentation using Exa.
150
+ const exaSearchCodeTool = createExaTool(
151
+ "exa_search_code",
152
+ "Exa Code Search",
153
+ `Search for code examples and technical documentation using Exa.
216
154
 
217
155
  Optimized for finding code snippets, API documentation, and technical content.
218
156
 
@@ -220,7 +158,7 @@ Parameters:
220
158
  - query: Code or technical search query (required)
221
159
  - code_context: Additional context about what you're looking for`,
222
160
 
223
- parameters: Type.Object({
161
+ Type.Object({
224
162
  query: Type.String({ description: "Code or technical search query" }),
225
163
  code_context: Type.Optional(
226
164
  Type.String({
@@ -228,45 +166,14 @@ Parameters:
228
166
  }),
229
167
  ),
230
168
  }),
231
-
232
- async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
233
- try {
234
- const apiKey = await findApiKey();
235
- if (!apiKey) {
236
- return {
237
- content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
238
- details: { error: "EXA_API_KEY not found", toolName: "exa_search_code" },
239
- };
240
- }
241
- const response = await callExaTool("get_code_context_exa", params, apiKey);
242
-
243
- if (isSearchResponse(response)) {
244
- const formatted = formatSearchResults(response);
245
- return {
246
- content: [{ type: "text" as const, text: formatted }],
247
- details: { response, toolName: "exa_search_code" },
248
- };
249
- }
250
-
251
- return {
252
- content: [{ type: "text" as const, text: JSON.stringify(response, null, 2) }],
253
- details: { raw: response, toolName: "exa_search_code" },
254
- };
255
- } catch (error) {
256
- const message = error instanceof Error ? error.message : String(error);
257
- return {
258
- content: [{ type: "text" as const, text: `Error: ${message}` }],
259
- details: { error: message, toolName: "exa_search_code" },
260
- };
261
- }
262
- },
263
- };
169
+ "get_code_context_exa",
170
+ );
264
171
 
265
172
  /** exa_crawl - URL content extraction */
266
- const exaCrawlTool: CustomTool<any, ExaRenderDetails> = {
267
- name: "exa_crawl",
268
- label: "Exa Crawl",
269
- description: `Extract content from a specific URL using Exa.
173
+ const exaCrawlTool = createExaTool(
174
+ "exa_crawl",
175
+ "Exa Crawl",
176
+ `Extract content from a specific URL using Exa.
270
177
 
271
178
  Returns the page content with optional text and highlights.
272
179
 
@@ -275,7 +182,7 @@ Parameters:
275
182
  - text: Include full page text content (default: false)
276
183
  - highlights: Include highlighted relevant snippets (default: false)`,
277
184
 
278
- parameters: Type.Object({
185
+ Type.Object({
279
186
  url: Type.String({ description: "URL to crawl and extract content from" }),
280
187
  text: Type.Optional(
281
188
  Type.Boolean({
@@ -288,39 +195,8 @@ Parameters:
288
195
  }),
289
196
  ),
290
197
  }),
291
-
292
- async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
293
- try {
294
- const apiKey = await findApiKey();
295
- if (!apiKey) {
296
- return {
297
- content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
298
- details: { error: "EXA_API_KEY not found", toolName: "exa_crawl" },
299
- };
300
- }
301
- const response = await callExaTool("crawling", params, apiKey);
302
-
303
- if (isSearchResponse(response)) {
304
- const formatted = formatSearchResults(response);
305
- return {
306
- content: [{ type: "text" as const, text: formatted }],
307
- details: { response, toolName: "exa_crawl" },
308
- };
309
- }
310
-
311
- return {
312
- content: [{ type: "text" as const, text: JSON.stringify(response, null, 2) }],
313
- details: { raw: response, toolName: "exa_crawl" },
314
- };
315
- } catch (error) {
316
- const message = error instanceof Error ? error.message : String(error);
317
- return {
318
- content: [{ type: "text" as const, text: `Error: ${message}` }],
319
- details: { error: message, toolName: "exa_crawl" },
320
- };
321
- }
322
- },
323
- };
198
+ "crawling",
199
+ );
324
200
 
325
201
  export const searchTools: CustomTool<any, ExaRenderDetails>[] = [
326
202
  exaSearchTool,
@@ -10,51 +10,13 @@ import { logger } from "@oh-my-pi/pi-utils";
10
10
  import * as typebox from "@sinclair/typebox";
11
11
  import { toolCapability } from "../../capability/tool";
12
12
  import { type CustomTool, loadCapability } from "../../discovery";
13
- import { expandPath } from "../../discovery/helpers";
14
13
  import type { ExecOptions } from "../../exec/exec";
15
14
  import { execCommand } from "../../exec/exec";
16
15
  import type { HookUIContext } from "../../extensibility/hooks/types";
17
16
  import { getAllPluginToolPaths } from "../../extensibility/plugins/loader";
18
- import { theme } from "../../modes/theme/theme";
17
+ import { createNoOpUIContext, resolvePath } from "../utils";
19
18
  import type { CustomToolAPI, CustomToolFactory, LoadedCustomTool, ToolLoadError } from "./types";
20
19
 
21
- /**
22
- * Resolve tool path.
23
- * - Absolute paths used as-is
24
- * - Paths starting with ~ expanded to home directory
25
- * - Relative paths resolved from cwd
26
- */
27
- function resolveToolPath(toolPath: string, cwd: string): string {
28
- const expanded = expandPath(toolPath);
29
-
30
- if (path.isAbsolute(expanded)) {
31
- return expanded;
32
- }
33
-
34
- // Relative paths resolved from cwd
35
- return path.resolve(cwd, expanded);
36
- }
37
-
38
- /**
39
- * Create a no-op UI context for headless modes.
40
- */
41
- function createNoOpUIContext(): HookUIContext {
42
- return {
43
- select: async () => undefined,
44
- confirm: async () => false,
45
- input: async () => undefined,
46
- notify: () => {},
47
- setStatus: () => {},
48
- custom: async () => undefined as never,
49
- setEditorText: () => {},
50
- getEditorText: () => "",
51
- editor: async () => undefined,
52
- get theme() {
53
- return theme;
54
- },
55
- };
56
- }
57
-
58
20
  /**
59
21
  * Load a single tool module using native Bun import.
60
22
  */
@@ -64,7 +26,7 @@ async function loadTool(
64
26
  sharedApi: CustomToolAPI,
65
27
  source?: { provider: string; providerName: string; level: "user" | "project" },
66
28
  ): Promise<{ tools: LoadedCustomTool[] | null; error: ToolLoadError | null }> {
67
- const resolvedPath = resolveToolPath(toolPath, cwd);
29
+ const resolvedPath = resolvePath(toolPath, cwd);
68
30
 
69
31
  // Skip declarative tool files (.md, .json) - these are metadata only, not executable modules
70
32
  if (resolvedPath.endsWith(".md") || resolvedPath.endsWith(".json")) {
@@ -228,7 +190,7 @@ export async function discoverAndLoadCustomTools(configuredPaths: string[], cwd:
228
190
 
229
191
  // 3. Explicitly configured paths (can override/add)
230
192
  for (const configPath of configuredPaths) {
231
- addPath(resolveToolPath(configPath, cwd), { provider: "config", providerName: "Config", level: "project" });
193
+ addPath(resolvePath(configPath, cwd), { provider: "config", providerName: "Config", level: "project" });
232
194
  }
233
195
 
234
196
  return loadCustomTools(allPathsWithSources, cwd, builtInToolNames);
@@ -13,11 +13,12 @@ import type { TSchema } from "@sinclair/typebox";
13
13
  import * as TypeBox from "@sinclair/typebox";
14
14
  import { type ExtensionModule, extensionModuleCapability } from "../../capability/extension-module";
15
15
  import { loadCapability } from "../../discovery";
16
- import { expandPath, getExtensionNameFromPath } from "../../discovery/helpers";
16
+ import { getExtensionNameFromPath } from "../../discovery/helpers";
17
17
  import type { ExecOptions } from "../../exec/exec";
18
18
  import { execCommand } from "../../exec/exec";
19
19
  import type { CustomMessage } from "../../session/messages";
20
20
  import { EventBus } from "../../utils/event-bus";
21
+ import { resolvePath } from "../utils";
21
22
  import type {
22
23
  Extension,
23
24
  ExtensionAPI,
@@ -30,14 +31,6 @@ import type {
30
31
  ToolDefinition,
31
32
  } from "./types";
32
33
 
33
- function resolvePath(extPath: string, cwd: string): string {
34
- const expanded = expandPath(extPath);
35
- if (path.isAbsolute(expanded)) {
36
- return expanded;
37
- }
38
- return path.resolve(cwd, expanded);
39
- }
40
-
41
34
  type HandlerFn = (...args: unknown[]) => Promise<unknown>;
42
35
 
43
36
  export class ExtensionRuntimeNotInitializedError extends Error {
@@ -8,9 +8,9 @@ import * as typebox from "@sinclair/typebox";
8
8
  import { hookCapability } from "../../capability/hook";
9
9
  import type { Hook } from "../../discovery";
10
10
  import { loadCapability } from "../../discovery";
11
- import { expandPath } from "../../discovery/helpers";
12
11
  import type { HookMessage } from "../../session/messages";
13
12
  import type { SessionManager } from "../../session/session-manager";
13
+ import { resolvePath } from "../utils";
14
14
  import { execCommand } from "./runner";
15
15
  import type { ExecOptions, HookAPI, HookFactory, HookMessageRenderer, RegisteredCommand } from "./types";
16
16
 
@@ -83,23 +83,6 @@ export interface LoadHooksResult {
83
83
  errors: Array<{ path: string; error: string }>;
84
84
  }
85
85
 
86
- /**
87
- * Resolve hook path.
88
- * - Absolute paths used as-is
89
- * - Paths starting with ~ expanded to home directory
90
- * - Relative paths resolved from cwd
91
- */
92
- function resolveHookPath(hookPath: string, cwd: string): string {
93
- const expanded = expandPath(hookPath);
94
-
95
- if (path.isAbsolute(expanded)) {
96
- return expanded;
97
- }
98
-
99
- // Relative paths resolved from cwd
100
- return path.resolve(cwd, expanded);
101
- }
102
-
103
86
  /**
104
87
  * Create a HookAPI instance that collects handlers, renderers, and commands.
105
88
  * Returns the API, maps, and functions to set handlers later.
@@ -174,7 +157,7 @@ function createHookAPI(
174
157
  * Load a single hook module using native Bun import.
175
158
  */
176
159
  async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHook | null; error: string | null }> {
177
- const resolvedPath = resolveHookPath(hookPath, cwd);
160
+ const resolvedPath = resolvePath(hookPath, cwd);
178
161
 
179
162
  try {
180
163
  // Import the module using native Bun import
@@ -267,7 +250,7 @@ export async function discoverAndLoadHooks(configuredPaths: string[], cwd: strin
267
250
  addPaths(discovered.items.map(hook => hook.path));
268
251
 
269
252
  // 2. Explicitly configured paths (can override/add)
270
- addPaths(configuredPaths.map(p => resolveHookPath(p, cwd)));
253
+ addPaths(configuredPaths.map(p => resolvePath(p, cwd)));
271
254
 
272
255
  return loadHooks(allPaths, cwd);
273
256
  }
@@ -4,8 +4,8 @@
4
4
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
5
5
  import type { Model } from "@oh-my-pi/pi-ai";
6
6
  import type { ModelRegistry } from "../../config/model-registry";
7
- import { theme } from "../../modes/theme/theme";
8
7
  import type { SessionManager } from "../../session/session-manager";
8
+ import { createNoOpUIContext } from "../utils";
9
9
  import type {
10
10
  AppendEntryHandler,
11
11
  BranchHandler,
@@ -42,22 +42,6 @@ export type HookErrorListener = (error: HookError) => void;
42
42
  // Re-export execCommand for backward compatibility
43
43
  export { execCommand } from "../../exec/exec";
44
44
 
45
- /** No-op UI context used when no UI is available */
46
- const noOpUIContext: HookUIContext = {
47
- select: async () => undefined,
48
- confirm: async () => false,
49
- input: async () => undefined,
50
- notify: () => {},
51
- setStatus: () => {},
52
- custom: async () => undefined as never,
53
- setEditorText: () => {},
54
- getEditorText: () => "",
55
- editor: async () => undefined,
56
- get theme() {
57
- return theme;
58
- },
59
- };
60
-
61
45
  /**
62
46
  * HookRunner executes hooks and manages event emission.
63
47
  */
@@ -80,7 +64,7 @@ export class HookRunner {
80
64
  private readonly sessionManager: SessionManager,
81
65
  private readonly modelRegistry: ModelRegistry,
82
66
  ) {
83
- this.#uiContext = noOpUIContext;
67
+ this.#uiContext = createNoOpUIContext();
84
68
  this.#hasUI = false;
85
69
  }
86
70
 
@@ -134,7 +118,7 @@ export class HookRunner {
134
118
  hook.setSendMessageHandler(options.sendMessageHandler);
135
119
  hook.setAppendEntryHandler(options.appendEntryHandler);
136
120
  }
137
- this.#uiContext = options.uiContext ?? noOpUIContext;
121
+ this.#uiContext = options.uiContext ?? createNoOpUIContext();
138
122
  this.#hasUI = options.hasUI ?? false;
139
123
  }
140
124