@oh-my-pi/pi-coding-agent 16.1.23 → 16.2.1

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 (260) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/cli.js +3785 -3695
  3. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +1 -0
  4. package/dist/types/advisor/emission-guard.d.ts +73 -0
  5. package/dist/types/advisor/index.d.ts +1 -0
  6. package/dist/types/advisor/runtime.d.ts +7 -0
  7. package/dist/types/advisor/watchdog.d.ts +13 -0
  8. package/dist/types/cli/flag-tables.d.ts +1 -0
  9. package/dist/types/cli/gallery-fixtures/search.d.ts +1 -1
  10. package/dist/types/collab/display-name.d.ts +3 -0
  11. package/dist/types/collab/host.d.ts +0 -2
  12. package/dist/types/commands/worktree.d.ts +0 -3
  13. package/dist/types/config/inline-tool-descriptors-mode.d.ts +13 -0
  14. package/dist/types/config/model-discovery.d.ts +6 -1
  15. package/dist/types/config/model-registry.d.ts +6 -2
  16. package/dist/types/config/model-resolver.d.ts +12 -0
  17. package/dist/types/config/models-config-schema.d.ts +29 -2
  18. package/dist/types/config/models-config.d.ts +22 -1
  19. package/dist/types/config/settings-schema.d.ts +91 -21
  20. package/dist/types/config/settings.d.ts +2 -0
  21. package/dist/types/dap/config.d.ts +1 -1
  22. package/dist/types/edit/hashline/filesystem.d.ts +4 -2
  23. package/dist/types/edit/renderer.d.ts +4 -0
  24. package/dist/types/extensibility/extensions/types.d.ts +17 -17
  25. package/dist/types/extensibility/hooks/types.d.ts +10 -10
  26. package/dist/types/extensibility/plugins/marketplace/types.d.ts +1 -0
  27. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +1 -0
  28. package/dist/types/internal-urls/index.d.ts +1 -0
  29. package/dist/types/internal-urls/registry-helpers.d.ts +2 -0
  30. package/dist/types/internal-urls/router.d.ts +1 -1
  31. package/dist/types/internal-urls/ssh-protocol.d.ts +10 -0
  32. package/dist/types/internal-urls/types.d.ts +19 -2
  33. package/dist/types/irc/bus.d.ts +6 -0
  34. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -0
  35. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +1 -0
  36. package/dist/types/modes/components/custom-editor.d.ts +7 -0
  37. package/dist/types/modes/components/move-overlay.d.ts +23 -0
  38. package/dist/types/modes/components/plugin-selector.d.ts +2 -1
  39. package/dist/types/modes/components/queue-mode-selector.d.ts +2 -1
  40. package/dist/types/modes/components/select-list-mouse-routing.d.ts +2 -0
  41. package/dist/types/modes/components/settings-defs.d.ts +4 -1
  42. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  43. package/dist/types/modes/components/show-images-selector.d.ts +2 -1
  44. package/dist/types/modes/components/status-line/component.d.ts +2 -0
  45. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  46. package/dist/types/modes/components/theme-selector.d.ts +2 -1
  47. package/dist/types/modes/components/thinking-selector.d.ts +2 -1
  48. package/dist/types/modes/controllers/command-controller.d.ts +10 -1
  49. package/dist/types/modes/controllers/streaming-reveal.d.ts +6 -0
  50. package/dist/types/modes/interactive-mode.d.ts +10 -1
  51. package/dist/types/modes/internal-url-autocomplete.d.ts +1 -1
  52. package/dist/types/modes/running-subagent-badge.d.ts +6 -0
  53. package/dist/types/modes/theme/theme.d.ts +1 -1
  54. package/dist/types/modes/types.d.ts +9 -1
  55. package/dist/types/sdk.d.ts +2 -2
  56. package/dist/types/session/agent-session.d.ts +8 -1
  57. package/dist/types/session/blob-store.d.ts +4 -0
  58. package/dist/types/session/session-manager.d.ts +13 -0
  59. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  60. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +1 -0
  61. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +1 -0
  62. package/dist/types/ssh/connection-manager.d.ts +2 -0
  63. package/dist/types/ssh/file-transfer.d.ts +79 -0
  64. package/dist/types/ssh/utils.d.ts +6 -0
  65. package/dist/types/system-prompt.d.ts +3 -0
  66. package/dist/types/task/executor.d.ts +16 -0
  67. package/dist/types/tiny/text.d.ts +1 -1
  68. package/dist/types/tools/builtin-names.d.ts +5 -1
  69. package/dist/types/tools/{find.d.ts → glob.d.ts} +15 -15
  70. package/dist/types/tools/{search.d.ts → grep.d.ts} +14 -14
  71. package/dist/types/tools/index.d.ts +3 -3
  72. package/dist/types/tools/path-utils.d.ts +29 -0
  73. package/dist/types/tools/read.d.ts +2 -2
  74. package/dist/types/tools/render-utils.d.ts +8 -0
  75. package/dist/types/tools/renderers.d.ts +11 -0
  76. package/dist/types/tools/ssh.d.ts +2 -0
  77. package/dist/types/tools/todo.d.ts +0 -16
  78. package/dist/types/tools/write.d.ts +2 -2
  79. package/dist/types/utils/active-repo-context.d.ts +8 -0
  80. package/dist/types/utils/image-resize.d.ts +1 -0
  81. package/dist/types/utils/markit-cache.d.ts +23 -0
  82. package/dist/types/utils/markit.d.ts +5 -1
  83. package/dist/types/utils/prompt-path.d.ts +1 -0
  84. package/dist/types/web/search/providers/duckduckgo.d.ts +14 -0
  85. package/dist/types/web/search/providers/firecrawl.d.ts +28 -0
  86. package/dist/types/web/search/providers/tinyfish.d.ts +29 -0
  87. package/dist/types/web/search/providers/xai.d.ts +13 -0
  88. package/dist/types/web/search/types.d.ts +18 -2
  89. package/package.json +30 -15
  90. package/scripts/build-binary.ts +9 -9
  91. package/scripts/bundle-dist.ts +2 -2
  92. package/src/advisor/__tests__/advisor.test.ts +61 -4
  93. package/src/advisor/__tests__/emission-guard.test.ts +147 -0
  94. package/src/advisor/advise-tool.ts +1 -1
  95. package/src/advisor/emission-guard.ts +172 -0
  96. package/src/advisor/index.ts +1 -0
  97. package/src/advisor/runtime.ts +11 -0
  98. package/src/advisor/watchdog.ts +27 -1
  99. package/src/cli/args.ts +5 -2
  100. package/src/cli/auth-broker-cli.ts +17 -0
  101. package/src/cli/config-cli.ts +4 -0
  102. package/src/cli/flag-tables.ts +7 -4
  103. package/src/cli/gallery-cli.ts +14 -2
  104. package/src/cli/gallery-fixtures/edit.ts +60 -0
  105. package/src/cli/gallery-fixtures/fs.ts +17 -17
  106. package/src/cli/gallery-fixtures/search.ts +4 -4
  107. package/src/cli/usage-cli.ts +20 -1
  108. package/src/cli/web-search-cli.ts +1 -1
  109. package/src/collab/display-name.ts +13 -0
  110. package/src/collab/guest.ts +5 -1
  111. package/src/collab/host.ts +2 -13
  112. package/src/commands/worktree.ts +6 -0
  113. package/src/config/inline-tool-descriptors-mode.ts +27 -0
  114. package/src/config/model-discovery.ts +77 -8
  115. package/src/config/model-registry.ts +89 -11
  116. package/src/config/model-resolver.ts +39 -1
  117. package/src/config/models-config-schema.ts +41 -5
  118. package/src/config/models-config.ts +4 -1
  119. package/src/config/settings-schema.ts +85 -27
  120. package/src/config/settings.ts +172 -1
  121. package/src/cursor.ts +1 -1
  122. package/src/dap/config.ts +152 -8
  123. package/src/dap/session.ts +1 -1
  124. package/src/discovery/claude-plugins.ts +3 -2
  125. package/src/discovery/helpers.ts +3 -1
  126. package/src/edit/hashline/diff.ts +14 -3
  127. package/src/edit/hashline/execute.ts +42 -6
  128. package/src/edit/hashline/filesystem.ts +38 -2
  129. package/src/edit/index.ts +1 -0
  130. package/src/edit/modes/patch.ts +11 -1
  131. package/src/edit/renderer.ts +140 -13
  132. package/src/eval/__tests__/agent-bridge.test.ts +101 -0
  133. package/src/eval/agent-bridge.ts +26 -3
  134. package/src/eval/js/tool-bridge.ts +2 -2
  135. package/src/export/html/index.ts +1 -1
  136. package/src/export/html/template.js +3 -1
  137. package/src/export/html/tool-views.generated.js +20 -20
  138. package/src/extensibility/extensions/types.ts +22 -22
  139. package/src/extensibility/hooks/types.ts +11 -11
  140. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +8 -3
  141. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +25 -10
  142. package/src/extensibility/plugins/marketplace/manager.ts +22 -0
  143. package/src/extensibility/plugins/marketplace/types.ts +1 -0
  144. package/src/internal-urls/__tests__/ssh-protocol.test.ts +331 -0
  145. package/src/internal-urls/docs-index.generated.txt +2 -2
  146. package/src/internal-urls/docs-index.ts +2 -3
  147. package/src/internal-urls/index.ts +1 -0
  148. package/src/internal-urls/registry-helpers.ts +19 -4
  149. package/src/internal-urls/router.ts +5 -3
  150. package/src/internal-urls/ssh-protocol.ts +367 -0
  151. package/src/internal-urls/types.ts +19 -2
  152. package/src/irc/bus.ts +11 -3
  153. package/src/mcp/tool-bridge.ts +32 -2
  154. package/src/memories/index.ts +1 -1
  155. package/src/modes/acp/acp-agent.ts +14 -1
  156. package/src/modes/acp/acp-event-mapper.ts +91 -27
  157. package/src/modes/components/__tests__/move-overlay.test.ts +166 -0
  158. package/src/modes/components/agent-hub.ts +3 -0
  159. package/src/modes/components/agent-transcript-viewer.ts +9 -7
  160. package/src/modes/components/custom-editor.ts +90 -5
  161. package/src/modes/components/move-overlay.ts +282 -0
  162. package/src/modes/components/plan-review-overlay.ts +35 -35
  163. package/src/modes/components/plugin-selector.ts +6 -1
  164. package/src/modes/components/queue-mode-selector.ts +6 -1
  165. package/src/modes/components/select-list-mouse-routing.ts +35 -0
  166. package/src/modes/components/session-selector.ts +11 -10
  167. package/src/modes/components/settings-defs.ts +14 -1
  168. package/src/modes/components/settings-selector.ts +196 -29
  169. package/src/modes/components/show-images-selector.ts +6 -1
  170. package/src/modes/components/status-line/component.ts +108 -28
  171. package/src/modes/components/status-line/segments.ts +5 -1
  172. package/src/modes/components/status-line/types.ts +2 -0
  173. package/src/modes/components/theme-selector.ts +6 -1
  174. package/src/modes/components/thinking-selector.ts +6 -1
  175. package/src/modes/components/tool-execution.ts +25 -9
  176. package/src/modes/components/tree-selector.ts +5 -5
  177. package/src/modes/controllers/command-controller.ts +140 -47
  178. package/src/modes/controllers/event-controller.ts +59 -3
  179. package/src/modes/controllers/input-controller.ts +68 -2
  180. package/src/modes/controllers/selector-controller.ts +5 -2
  181. package/src/modes/controllers/streaming-reveal.ts +17 -0
  182. package/src/modes/controllers/tool-args-reveal.ts +1 -1
  183. package/src/modes/interactive-mode.ts +116 -50
  184. package/src/modes/internal-url-autocomplete.ts +17 -2
  185. package/src/modes/prompt-action-autocomplete.ts +3 -1
  186. package/src/modes/running-subagent-badge.ts +13 -0
  187. package/src/modes/setup-wizard/scenes/glyph.ts +2 -13
  188. package/src/modes/setup-wizard/scenes/providers.ts +2 -1
  189. package/src/modes/setup-wizard/scenes/theme.ts +6 -12
  190. package/src/modes/setup-wizard/scenes/web-search.ts +8 -13
  191. package/src/modes/setup-wizard/wizard-overlay.ts +6 -5
  192. package/src/modes/theme/theme.ts +9 -1
  193. package/src/modes/types.ts +9 -1
  194. package/src/modes/utils/interactive-context-helpers.ts +1 -1
  195. package/src/priority.json +15 -0
  196. package/src/prompts/advisor/active-repo-watchdog.md +6 -0
  197. package/src/prompts/advisor/context-files.md +8 -0
  198. package/src/prompts/advisor/system.md +21 -7
  199. package/src/prompts/agents/designer.md +1 -1
  200. package/src/prompts/agents/explore.md +1 -1
  201. package/src/prompts/agents/librarian.md +2 -2
  202. package/src/prompts/agents/plan.md +2 -2
  203. package/src/prompts/agents/reviewer.md +1 -1
  204. package/src/prompts/agents/task.md +2 -2
  205. package/src/prompts/system/active-repo-context.md +4 -0
  206. package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
  207. package/src/prompts/system/plan-mode-active.md +3 -3
  208. package/src/prompts/system/project-prompt.md +2 -2
  209. package/src/prompts/system/system-prompt.md +6 -6
  210. package/src/prompts/tools/bash.md +2 -2
  211. package/src/prompts/tools/checkpoint.md +1 -1
  212. package/src/prompts/tools/{find.md → glob.md} +1 -1
  213. package/src/prompts/tools/{search.md → grep.md} +3 -3
  214. package/src/prompts/tools/read.md +4 -2
  215. package/src/sdk.ts +65 -25
  216. package/src/session/agent-session.ts +519 -132
  217. package/src/session/blob-store.ts +24 -0
  218. package/src/session/session-history-format.ts +2 -2
  219. package/src/session/session-manager.ts +46 -0
  220. package/src/slash-commands/builtin-registry.ts +138 -20
  221. package/src/slash-commands/helpers/usage-report.ts +23 -2
  222. package/src/ssh/__tests__/connection-manager-args.test.ts +69 -0
  223. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +68 -0
  224. package/src/ssh/connection-manager.ts +12 -15
  225. package/src/ssh/file-transfer.ts +209 -0
  226. package/src/ssh/utils.ts +24 -0
  227. package/src/system-prompt.ts +56 -28
  228. package/src/task/executor.ts +96 -36
  229. package/src/task/index.ts +3 -3
  230. package/src/task/render.ts +14 -13
  231. package/src/task/worktree.ts +38 -1
  232. package/src/tiny/text.ts +49 -4
  233. package/src/tiny/worker.ts +3 -3
  234. package/src/tools/bash.ts +2 -2
  235. package/src/tools/builtin-names.ts +26 -2
  236. package/src/tools/{find.ts → glob.ts} +48 -42
  237. package/src/tools/{search.ts → grep.ts} +311 -129
  238. package/src/tools/index.ts +13 -14
  239. package/src/tools/irc.ts +6 -2
  240. package/src/tools/path-utils.ts +64 -1
  241. package/src/tools/read.ts +35 -6
  242. package/src/tools/render-utils.ts +14 -0
  243. package/src/tools/renderers.ts +15 -4
  244. package/src/tools/ssh.ts +17 -2
  245. package/src/tools/todo.ts +4 -26
  246. package/src/tools/write.ts +25 -9
  247. package/src/utils/active-repo-context.ts +143 -0
  248. package/src/utils/image-resize.ts +88 -7
  249. package/src/utils/lang-from-path.ts +3 -3
  250. package/src/utils/markit-cache.ts +166 -0
  251. package/src/utils/markit.ts +86 -18
  252. package/src/utils/prompt-path.ts +3 -0
  253. package/src/utils/title-generator.ts +1 -1
  254. package/src/web/search/provider.ts +54 -34
  255. package/src/web/search/providers/duckduckgo.ts +140 -0
  256. package/src/web/search/providers/firecrawl.ts +144 -0
  257. package/src/web/search/providers/tinyfish.ts +159 -0
  258. package/src/web/search/providers/xai.ts +292 -0
  259. package/src/web/search/providers/zai.ts +142 -56
  260. package/src/web/search/types.ts +6 -2
@@ -24,66 +24,81 @@ interface ProviderMeta {
24
24
 
25
25
  /** Lazy factories. Each `load()` dynamic-imports its provider module on first call. */
26
26
  const PROVIDER_META: Record<SearchProviderId, ProviderMeta> = {
27
- exa: {
28
- id: "exa",
29
- label: SEARCH_PROVIDER_LABELS.exa,
30
- load: async () => new (await import("./providers/exa")).ExaProvider(),
31
- },
32
- brave: {
33
- id: "brave",
34
- label: SEARCH_PROVIDER_LABELS.brave,
35
- load: async () => new (await import("./providers/brave")).BraveProvider(),
36
- },
37
- jina: {
38
- id: "jina",
39
- label: SEARCH_PROVIDER_LABELS.jina,
40
- load: async () => new (await import("./providers/jina")).JinaProvider(),
41
- },
42
27
  perplexity: {
43
28
  id: "perplexity",
44
29
  label: SEARCH_PROVIDER_LABELS.perplexity,
45
30
  load: async () => new (await import("./providers/perplexity")).PerplexityProvider(),
46
31
  },
47
- kimi: {
48
- id: "kimi",
49
- label: SEARCH_PROVIDER_LABELS.kimi,
50
- load: async () => new (await import("./providers/kimi")).KimiProvider(),
51
- },
52
- zai: {
53
- id: "zai",
54
- label: SEARCH_PROVIDER_LABELS.zai,
55
- load: async () => new (await import("./providers/zai")).ZaiProvider(),
32
+ gemini: {
33
+ id: "gemini",
34
+ label: SEARCH_PROVIDER_LABELS.gemini,
35
+ load: async () => new (await import("./providers/gemini")).GeminiProvider(),
56
36
  },
57
37
  anthropic: {
58
38
  id: "anthropic",
59
39
  label: SEARCH_PROVIDER_LABELS.anthropic,
60
40
  load: async () => new (await import("./providers/anthropic")).AnthropicProvider(),
61
41
  },
62
- gemini: {
63
- id: "gemini",
64
- label: SEARCH_PROVIDER_LABELS.gemini,
65
- load: async () => new (await import("./providers/gemini")).GeminiProvider(),
66
- },
67
42
  codex: {
68
43
  id: "codex",
69
44
  label: SEARCH_PROVIDER_LABELS.codex,
70
45
  load: async () => new (await import("./providers/codex")).CodexProvider(),
71
46
  },
47
+ xai: {
48
+ id: "xai",
49
+ label: SEARCH_PROVIDER_LABELS.xai,
50
+ load: async () => new (await import("./providers/xai")).XAIProvider(),
51
+ },
52
+ zai: {
53
+ id: "zai",
54
+ label: SEARCH_PROVIDER_LABELS.zai,
55
+ load: async () => new (await import("./providers/zai")).ZaiProvider(),
56
+ },
57
+ exa: {
58
+ id: "exa",
59
+ label: SEARCH_PROVIDER_LABELS.exa,
60
+ load: async () => new (await import("./providers/exa")).ExaProvider(),
61
+ },
62
+ tinyfish: {
63
+ id: "tinyfish",
64
+ label: SEARCH_PROVIDER_LABELS.tinyfish,
65
+ load: async () => new (await import("./providers/tinyfish")).TinyFishProvider(),
66
+ },
67
+ jina: {
68
+ id: "jina",
69
+ label: SEARCH_PROVIDER_LABELS.jina,
70
+ load: async () => new (await import("./providers/jina")).JinaProvider(),
71
+ },
72
+ kagi: {
73
+ id: "kagi",
74
+ label: SEARCH_PROVIDER_LABELS.kagi,
75
+ load: async () => new (await import("./providers/kagi")).KagiProvider(),
76
+ },
72
77
  tavily: {
73
78
  id: "tavily",
74
79
  label: SEARCH_PROVIDER_LABELS.tavily,
75
80
  load: async () => new (await import("./providers/tavily")).TavilyProvider(),
76
81
  },
82
+ firecrawl: {
83
+ id: "firecrawl",
84
+ label: SEARCH_PROVIDER_LABELS.firecrawl,
85
+ load: async () => new (await import("./providers/firecrawl")).FirecrawlProvider(),
86
+ },
87
+ brave: {
88
+ id: "brave",
89
+ label: SEARCH_PROVIDER_LABELS.brave,
90
+ load: async () => new (await import("./providers/brave")).BraveProvider(),
91
+ },
92
+ kimi: {
93
+ id: "kimi",
94
+ label: SEARCH_PROVIDER_LABELS.kimi,
95
+ load: async () => new (await import("./providers/kimi")).KimiProvider(),
96
+ },
77
97
  parallel: {
78
98
  id: "parallel",
79
99
  label: SEARCH_PROVIDER_LABELS.parallel,
80
100
  load: async () => new (await import("./providers/parallel")).ParallelProvider(),
81
101
  },
82
- kagi: {
83
- id: "kagi",
84
- label: SEARCH_PROVIDER_LABELS.kagi,
85
- load: async () => new (await import("./providers/kagi")).KagiProvider(),
86
- },
87
102
  synthetic: {
88
103
  id: "synthetic",
89
104
  label: SEARCH_PROVIDER_LABELS.synthetic,
@@ -94,6 +109,11 @@ const PROVIDER_META: Record<SearchProviderId, ProviderMeta> = {
94
109
  label: SEARCH_PROVIDER_LABELS.searxng,
95
110
  load: async () => new (await import("./providers/searxng")).SearXNGProvider(),
96
111
  },
112
+ duckduckgo: {
113
+ id: "duckduckgo",
114
+ label: SEARCH_PROVIDER_LABELS.duckduckgo,
115
+ load: async () => new (await import("./providers/duckduckgo")).DuckDuckGoProvider(),
116
+ },
97
117
  };
98
118
 
99
119
  const instanceCache = new Map<SearchProviderId, SearchProvider>();
@@ -0,0 +1,140 @@
1
+ import type { AuthStorage } from "@oh-my-pi/pi-ai";
2
+ import type { SearchResponse, SearchSource } from "../../../web/search/types";
3
+ import { SearchProviderError } from "../../../web/search/types";
4
+ import { clampNumResults } from "../utils";
5
+ import type { SearchParams } from "./base";
6
+ import { SearchProvider } from "./base";
7
+ import { classifyProviderHttpError, withHardTimeout } from "./utils";
8
+
9
+ const DUCKDUCKGO_SEARCH_URL = "https://api.duckduckgo.com/";
10
+ const DEFAULT_NUM_RESULTS = 10;
11
+ const MAX_NUM_RESULTS = 20;
12
+
13
+ interface DuckDuckGoTopic {
14
+ FirstURL?: string | null;
15
+ Text?: string | null;
16
+ Topics?: DuckDuckGoTopic[] | null;
17
+ }
18
+
19
+ interface DuckDuckGoResponse {
20
+ AbstractText?: string | null;
21
+ AbstractURL?: string | null;
22
+ AbstractSource?: string | null;
23
+ Answer?: string | null;
24
+ Definition?: string | null;
25
+ Heading?: string | null;
26
+ Results?: DuckDuckGoTopic[] | null;
27
+ RelatedTopics?: DuckDuckGoTopic[] | null;
28
+ }
29
+
30
+ function cleanText(value: string | null | undefined): string | undefined {
31
+ const cleaned = value
32
+ ?.replace(/<[^>]*>/g, " ")
33
+ .replace(/&nbsp;/gi, " ")
34
+ .replace(/&amp;/gi, "&")
35
+ .replace(/&lt;/gi, "<")
36
+ .replace(/&gt;/gi, ">")
37
+ .replace(/&quot;/gi, '"')
38
+ .replace(/&#39;/gi, "'")
39
+ .replace(/\s+/g, " ")
40
+ .trim();
41
+ return cleaned ? cleaned : undefined;
42
+ }
43
+
44
+ function addSource(sources: SearchSource[], source: SearchSource): void {
45
+ if (!source.url || sources.some(existing => existing.url === source.url)) return;
46
+ sources.push(source);
47
+ }
48
+
49
+ function addTopicSource(sources: SearchSource[], topic: DuckDuckGoTopic): void {
50
+ const url = topic.FirstURL?.trim();
51
+ if (!url) return;
52
+ const text = cleanText(topic.Text);
53
+ addSource(sources, {
54
+ title: text ?? url,
55
+ url,
56
+ snippet: text,
57
+ });
58
+ }
59
+
60
+ function collectTopicSources(sources: SearchSource[], topics: readonly DuckDuckGoTopic[] | null | undefined): void {
61
+ if (!topics) return;
62
+ for (const topic of topics) {
63
+ addTopicSource(sources, topic);
64
+ collectTopicSources(sources, topic.Topics);
65
+ }
66
+ }
67
+
68
+ async function callDuckDuckGoSearch(params: SearchParams): Promise<DuckDuckGoResponse> {
69
+ const queryString = [
70
+ ["q", params.query],
71
+ ["format", "json"],
72
+ ["no_redirect", "1"],
73
+ ["no_html", "1"],
74
+ ["skip_disambig", "1"],
75
+ ["t", "oh-my-pi"],
76
+ ]
77
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
78
+ .join("&");
79
+ const response = await (params.fetch ?? fetch)(`${DUCKDUCKGO_SEARCH_URL}?${queryString}`, {
80
+ method: "GET",
81
+ signal: withHardTimeout(params.signal),
82
+ });
83
+
84
+ if (!response.ok) {
85
+ const errorText = await response.text();
86
+ const classified = classifyProviderHttpError("duckduckgo", response.status, errorText);
87
+ if (classified) throw classified;
88
+ throw new SearchProviderError(
89
+ "duckduckgo",
90
+ `DuckDuckGo API error (${response.status}): ${errorText}`,
91
+ response.status,
92
+ );
93
+ }
94
+
95
+ return (await response.json()) as DuckDuckGoResponse;
96
+ }
97
+
98
+ /** Execute DuckDuckGo Instant Answer API search. */
99
+ export async function searchDuckDuckGo(params: SearchParams): Promise<SearchResponse> {
100
+ const numResults = clampNumResults(params.numSearchResults ?? params.limit, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
101
+ const data = await callDuckDuckGoSearch(params);
102
+ const answer = cleanText(data.AbstractText) ?? cleanText(data.Answer) ?? cleanText(data.Definition);
103
+ const sources: SearchSource[] = [];
104
+
105
+ const abstractUrl = data.AbstractURL?.trim();
106
+ if (abstractUrl) {
107
+ addSource(sources, {
108
+ title: cleanText(data.AbstractSource) ?? cleanText(data.Heading) ?? abstractUrl,
109
+ url: abstractUrl,
110
+ snippet: cleanText(data.AbstractText),
111
+ });
112
+ }
113
+
114
+ collectTopicSources(sources, data.Results);
115
+ collectTopicSources(sources, data.RelatedTopics);
116
+
117
+ return {
118
+ provider: "duckduckgo",
119
+ answer,
120
+ sources: sources.slice(0, numResults),
121
+ };
122
+ }
123
+
124
+ /** Search provider for DuckDuckGo Instant Answer API. */
125
+ export class DuckDuckGoProvider extends SearchProvider {
126
+ readonly id = "duckduckgo";
127
+ readonly label = "DuckDuckGo";
128
+
129
+ isAvailable(_authStorage: AuthStorage): boolean {
130
+ return true;
131
+ }
132
+
133
+ isExplicitlyAvailable(_authStorage: AuthStorage): boolean {
134
+ return true;
135
+ }
136
+
137
+ search(params: SearchParams): Promise<SearchResponse> {
138
+ return searchDuckDuckGo(params);
139
+ }
140
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Firecrawl Web Search Provider
3
+ *
4
+ * Calls Firecrawl's search API and maps web results into the unified
5
+ * SearchResponse shape used by the web search tool.
6
+ */
7
+ import { type ApiKey, type AuthStorage, type FetchImpl, getEnvApiKey, withAuth } from "@oh-my-pi/pi-ai";
8
+ import type { SearchResponse, SearchSource } from "../../../web/search/types";
9
+ import { SearchProviderError } from "../../../web/search/types";
10
+ import { clampNumResults } from "../utils";
11
+ import type { SearchParams } from "./base";
12
+ import { SearchProvider } from "./base";
13
+ import { classifyProviderHttpError, withHardTimeout } from "./utils";
14
+
15
+ const FIRECRAWL_SEARCH_URL = "https://api.firecrawl.dev/v2/search";
16
+ const DEFAULT_NUM_RESULTS = 10;
17
+ const MAX_NUM_RESULTS = 100;
18
+
19
+ const RECENCY_TBS: Record<NonNullable<SearchParams["recency"]>, string> = {
20
+ day: "qdr:d",
21
+ week: "qdr:w",
22
+ month: "qdr:m",
23
+ year: "qdr:y",
24
+ };
25
+
26
+ export interface FirecrawlSearchParams {
27
+ query: string;
28
+ num_results?: number;
29
+ recency?: SearchParams["recency"];
30
+ signal?: AbortSignal;
31
+ fetch?: FetchImpl;
32
+ }
33
+
34
+ interface FirecrawlWebResult {
35
+ title?: string | null;
36
+ url?: string | null;
37
+ description?: string | null;
38
+ markdown?: string | null;
39
+ }
40
+
41
+ interface FirecrawlSearchResponse {
42
+ id?: string | null;
43
+ data?: {
44
+ web?: FirecrawlWebResult[] | null;
45
+ } | null;
46
+ }
47
+
48
+ /** Resolve Firecrawl API key through the shared auth storage pipeline. */
49
+ export function findApiKey(
50
+ authStorage: AuthStorage,
51
+ sessionId?: string,
52
+ signal?: AbortSignal,
53
+ ): Promise<string | undefined> {
54
+ return authStorage.getApiKey("firecrawl", sessionId, { signal });
55
+ }
56
+
57
+ function buildRequestBody(params: FirecrawlSearchParams): Record<string, unknown> {
58
+ const body: Record<string, unknown> = {
59
+ query: params.query,
60
+ limit: clampNumResults(params.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS),
61
+ sources: [{ type: "web" }],
62
+ };
63
+ if (params.recency) {
64
+ body.tbs = RECENCY_TBS[params.recency];
65
+ }
66
+ return body;
67
+ }
68
+
69
+ async function callFirecrawlSearch(apiKey: string, params: FirecrawlSearchParams): Promise<FirecrawlSearchResponse> {
70
+ const response = await (params.fetch ?? fetch)(FIRECRAWL_SEARCH_URL, {
71
+ method: "POST",
72
+ headers: {
73
+ "Content-Type": "application/json",
74
+ Authorization: `Bearer ${apiKey}`,
75
+ },
76
+ body: JSON.stringify(buildRequestBody(params)),
77
+ signal: withHardTimeout(params.signal),
78
+ });
79
+
80
+ if (!response.ok) {
81
+ const errorText = await response.text();
82
+ const classified = classifyProviderHttpError("firecrawl", response.status, errorText);
83
+ if (classified) throw classified;
84
+ throw new SearchProviderError(
85
+ "firecrawl",
86
+ `Firecrawl API error (${response.status}): ${errorText}`,
87
+ response.status,
88
+ );
89
+ }
90
+
91
+ return (await response.json()) as FirecrawlSearchResponse;
92
+ }
93
+
94
+ /** Execute Firecrawl web search. */
95
+ export async function searchFirecrawl(params: SearchParams): Promise<SearchResponse> {
96
+ const firecrawlParams: FirecrawlSearchParams = {
97
+ query: params.query,
98
+ num_results: params.numSearchResults ?? params.limit,
99
+ recency: params.recency,
100
+ signal: params.signal,
101
+ fetch: params.fetch,
102
+ };
103
+ const keyOrResolver: ApiKey = params.authStorage.resolver("firecrawl", {
104
+ sessionId: params.sessionId,
105
+ });
106
+ const numResults = clampNumResults(firecrawlParams.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
107
+
108
+ const data = await withAuth(keyOrResolver, key => callFirecrawlSearch(key, firecrawlParams), {
109
+ signal: params.signal,
110
+ missingKeyMessage:
111
+ 'Firecrawl credentials not found. Set FIRECRAWL_API_KEY or configure an API key for provider "firecrawl".',
112
+ });
113
+ const sources: SearchSource[] = [];
114
+
115
+ for (const result of data.data?.web ?? []) {
116
+ if (!result.url) continue;
117
+ sources.push({
118
+ title: result.title ?? result.url,
119
+ url: result.url,
120
+ snippet: result.description ?? result.markdown ?? undefined,
121
+ });
122
+ }
123
+
124
+ return {
125
+ provider: "firecrawl",
126
+ sources: sources.slice(0, numResults),
127
+ requestId: data.id ?? undefined,
128
+ authMode: "api_key",
129
+ };
130
+ }
131
+
132
+ /** Search provider for Firecrawl web search. */
133
+ export class FirecrawlProvider extends SearchProvider {
134
+ readonly id = "firecrawl";
135
+ readonly label = "Firecrawl";
136
+
137
+ isAvailable(authStorage: AuthStorage): boolean {
138
+ return authStorage.hasAuth("firecrawl") || !!getEnvApiKey("firecrawl");
139
+ }
140
+
141
+ search(params: SearchParams): Promise<SearchResponse> {
142
+ return searchFirecrawl(params);
143
+ }
144
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * TinyFish Web Search Provider
3
+ *
4
+ * Calls TinyFish's search API and maps results into the unified
5
+ * SearchResponse shape used by the web search tool.
6
+ */
7
+ import { type ApiKey, type AuthStorage, type FetchImpl, getEnvApiKey, withAuth } from "@oh-my-pi/pi-ai";
8
+ import type { SearchResponse, SearchSource } from "../../../web/search/types";
9
+ import { SearchProviderError } from "../../../web/search/types";
10
+ import { clampNumResults } from "../utils";
11
+ import type { SearchParams } from "./base";
12
+ import { SearchProvider } from "./base";
13
+ import { classifyProviderHttpError, withHardTimeout } from "./utils";
14
+
15
+ const TINYFISH_SEARCH_URL = "https://api.search.tinyfish.ai";
16
+ const DEFAULT_NUM_RESULTS = 10;
17
+ const MAX_NUM_RESULTS = 20;
18
+ const MAX_PAGE = 10;
19
+
20
+ const RECENCY_MINUTES: Record<NonNullable<SearchParams["recency"]>, number> = {
21
+ day: 1440,
22
+ week: 10080,
23
+ month: 43200,
24
+ year: 525600,
25
+ };
26
+
27
+ export interface TinyFishSearchParams {
28
+ query: string;
29
+ num_results?: number;
30
+ recency?: SearchParams["recency"];
31
+ page?: number;
32
+ signal?: AbortSignal;
33
+ fetch?: FetchImpl;
34
+ }
35
+
36
+ interface TinyFishSearchResult {
37
+ title?: string | null;
38
+ url?: string | null;
39
+ snippet?: string | null;
40
+ site_name?: string | null;
41
+ }
42
+
43
+ interface TinyFishSearchResponse {
44
+ total_results?: number | null;
45
+ page?: number | null;
46
+ results?: TinyFishSearchResult[] | null;
47
+ }
48
+
49
+ /** Resolve TinyFish API key through the shared auth storage pipeline. */
50
+ export function findApiKey(
51
+ authStorage: AuthStorage,
52
+ sessionId?: string,
53
+ signal?: AbortSignal,
54
+ ): Promise<string | undefined> {
55
+ return authStorage.getApiKey("tinyfish", sessionId, { signal });
56
+ }
57
+
58
+ async function callTinyFishSearch(apiKey: string, params: TinyFishSearchParams): Promise<TinyFishSearchResponse> {
59
+ const url = new URL(TINYFISH_SEARCH_URL);
60
+ url.searchParams.set("query", params.query);
61
+ if (params.recency) {
62
+ url.searchParams.set("recency_minutes", String(RECENCY_MINUTES[params.recency]));
63
+ }
64
+ if (params.num_results !== undefined) {
65
+ url.searchParams.set("num_results", String(params.num_results));
66
+ }
67
+ if (params.page !== undefined) {
68
+ url.searchParams.set("page", String(params.page));
69
+ }
70
+
71
+ const response = await (params.fetch ?? fetch)(url, {
72
+ method: "GET",
73
+ headers: {
74
+ Accept: "application/json",
75
+ "X-API-Key": apiKey,
76
+ },
77
+ signal: withHardTimeout(params.signal),
78
+ });
79
+
80
+ if (!response.ok) {
81
+ const errorText = await response.text();
82
+ const classified = classifyProviderHttpError("tinyfish", response.status, errorText);
83
+ if (classified) throw classified;
84
+ throw new SearchProviderError(
85
+ "tinyfish",
86
+ `TinyFish API error (${response.status}): ${errorText}`,
87
+ response.status,
88
+ );
89
+ }
90
+
91
+ return (await response.json()) as TinyFishSearchResponse;
92
+ }
93
+
94
+ function appendTinyFishSources(sources: SearchSource[], results: readonly TinyFishSearchResult[]): void {
95
+ for (const result of results) {
96
+ if (!result.url) continue;
97
+ sources.push({
98
+ title: result.title ?? result.site_name ?? result.url,
99
+ url: result.url,
100
+ snippet: result.snippet ?? undefined,
101
+ author: result.site_name ?? undefined,
102
+ });
103
+ }
104
+ }
105
+
106
+ /** Execute TinyFish web search. */
107
+ export async function searchTinyFish(params: SearchParams): Promise<SearchResponse> {
108
+ const numResults = clampNumResults(params.numSearchResults ?? params.limit, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
109
+ const pageSize = Math.min(numResults, DEFAULT_NUM_RESULTS);
110
+ const tinyFishParams: TinyFishSearchParams = {
111
+ query: params.query,
112
+ num_results: pageSize,
113
+ recency: params.recency,
114
+ signal: params.signal,
115
+ fetch: params.fetch,
116
+ };
117
+ const keyOrResolver: ApiKey = params.authStorage.resolver("tinyfish", {
118
+ sessionId: params.sessionId,
119
+ });
120
+ const sources = await withAuth(
121
+ keyOrResolver,
122
+ async key => {
123
+ const collected: SearchSource[] = [];
124
+ for (let page = 0; page <= MAX_PAGE && collected.length < numResults; page += 1) {
125
+ const searchPage = await callTinyFishSearch(key, { ...tinyFishParams, page });
126
+ const results = searchPage.results ?? [];
127
+ appendTinyFishSources(collected, results);
128
+ if (results.length < pageSize) break;
129
+ }
130
+
131
+ return collected.slice(0, numResults);
132
+ },
133
+ {
134
+ signal: params.signal,
135
+ missingKeyMessage:
136
+ 'TinyFish credentials not found. Set TINYFISH_API_KEY or configure an API key for provider "tinyfish".',
137
+ },
138
+ );
139
+
140
+ return {
141
+ provider: "tinyfish",
142
+ sources,
143
+ authMode: "api_key",
144
+ };
145
+ }
146
+
147
+ /** Search provider for TinyFish web search. */
148
+ export class TinyFishProvider extends SearchProvider {
149
+ readonly id = "tinyfish";
150
+ readonly label = "TinyFish";
151
+
152
+ isAvailable(authStorage: AuthStorage): boolean {
153
+ return authStorage.hasAuth("tinyfish") || !!getEnvApiKey("tinyfish");
154
+ }
155
+
156
+ search(params: SearchParams): Promise<SearchResponse> {
157
+ return searchTinyFish(params);
158
+ }
159
+ }