@oh-my-pi/pi-coding-agent 0.1.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 (337) hide show
  1. package/CHANGELOG.md +1629 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/config-usage.md +113 -0
  5. package/docs/custom-tools.md +541 -0
  6. package/docs/extension-loading.md +1004 -0
  7. package/docs/hooks.md +867 -0
  8. package/docs/rpc.md +1040 -0
  9. package/docs/sdk.md +994 -0
  10. package/docs/session-tree-plan.md +441 -0
  11. package/docs/session.md +240 -0
  12. package/docs/skills.md +290 -0
  13. package/docs/theme.md +670 -0
  14. package/docs/tree.md +197 -0
  15. package/docs/tui.md +341 -0
  16. package/examples/README.md +21 -0
  17. package/examples/custom-tools/README.md +124 -0
  18. package/examples/custom-tools/hello/index.ts +20 -0
  19. package/examples/custom-tools/question/index.ts +84 -0
  20. package/examples/custom-tools/subagent/README.md +172 -0
  21. package/examples/custom-tools/subagent/agents/planner.md +37 -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 +89 -0
  57. package/src/bun-imports.d.ts +16 -0
  58. package/src/capability/context-file.ts +40 -0
  59. package/src/capability/extension.ts +48 -0
  60. package/src/capability/hook.ts +40 -0
  61. package/src/capability/index.ts +616 -0
  62. package/src/capability/instruction.ts +37 -0
  63. package/src/capability/mcp.ts +52 -0
  64. package/src/capability/prompt.ts +35 -0
  65. package/src/capability/rule.ts +56 -0
  66. package/src/capability/settings.ts +35 -0
  67. package/src/capability/skill.ts +49 -0
  68. package/src/capability/slash-command.ts +40 -0
  69. package/src/capability/system-prompt.ts +35 -0
  70. package/src/capability/tool.ts +38 -0
  71. package/src/capability/types.ts +166 -0
  72. package/src/cli/args.ts +259 -0
  73. package/src/cli/file-processor.ts +121 -0
  74. package/src/cli/list-models.ts +104 -0
  75. package/src/cli/plugin-cli.ts +661 -0
  76. package/src/cli/session-picker.ts +41 -0
  77. package/src/cli/update-cli.ts +274 -0
  78. package/src/cli.ts +10 -0
  79. package/src/config.ts +391 -0
  80. package/src/core/agent-session.ts +2178 -0
  81. package/src/core/auth-storage.ts +258 -0
  82. package/src/core/bash-executor.ts +197 -0
  83. package/src/core/compaction/branch-summarization.ts +315 -0
  84. package/src/core/compaction/compaction.ts +664 -0
  85. package/src/core/compaction/index.ts +7 -0
  86. package/src/core/compaction/utils.ts +153 -0
  87. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  88. package/src/core/custom-commands/index.ts +15 -0
  89. package/src/core/custom-commands/loader.ts +226 -0
  90. package/src/core/custom-commands/types.ts +112 -0
  91. package/src/core/custom-tools/index.ts +22 -0
  92. package/src/core/custom-tools/loader.ts +248 -0
  93. package/src/core/custom-tools/types.ts +185 -0
  94. package/src/core/custom-tools/wrapper.ts +29 -0
  95. package/src/core/exec.ts +139 -0
  96. package/src/core/export-html/index.ts +159 -0
  97. package/src/core/export-html/template.css +774 -0
  98. package/src/core/export-html/template.generated.ts +2 -0
  99. package/src/core/export-html/template.html +45 -0
  100. package/src/core/export-html/template.js +1185 -0
  101. package/src/core/export-html/template.macro.ts +24 -0
  102. package/src/core/file-mentions.ts +54 -0
  103. package/src/core/hooks/index.ts +16 -0
  104. package/src/core/hooks/loader.ts +288 -0
  105. package/src/core/hooks/runner.ts +434 -0
  106. package/src/core/hooks/tool-wrapper.ts +98 -0
  107. package/src/core/hooks/types.ts +770 -0
  108. package/src/core/index.ts +53 -0
  109. package/src/core/logger.ts +112 -0
  110. package/src/core/mcp/client.ts +185 -0
  111. package/src/core/mcp/config.ts +248 -0
  112. package/src/core/mcp/index.ts +45 -0
  113. package/src/core/mcp/loader.ts +99 -0
  114. package/src/core/mcp/manager.ts +235 -0
  115. package/src/core/mcp/tool-bridge.ts +156 -0
  116. package/src/core/mcp/transports/http.ts +316 -0
  117. package/src/core/mcp/transports/index.ts +6 -0
  118. package/src/core/mcp/transports/stdio.ts +252 -0
  119. package/src/core/mcp/types.ts +228 -0
  120. package/src/core/messages.ts +211 -0
  121. package/src/core/model-registry.ts +334 -0
  122. package/src/core/model-resolver.ts +494 -0
  123. package/src/core/plugins/doctor.ts +67 -0
  124. package/src/core/plugins/index.ts +38 -0
  125. package/src/core/plugins/installer.ts +189 -0
  126. package/src/core/plugins/loader.ts +339 -0
  127. package/src/core/plugins/manager.ts +672 -0
  128. package/src/core/plugins/parser.ts +105 -0
  129. package/src/core/plugins/paths.ts +37 -0
  130. package/src/core/plugins/types.ts +190 -0
  131. package/src/core/sdk.ts +900 -0
  132. package/src/core/session-manager.ts +1837 -0
  133. package/src/core/settings-manager.ts +860 -0
  134. package/src/core/skills.ts +352 -0
  135. package/src/core/slash-commands.ts +132 -0
  136. package/src/core/system-prompt.ts +442 -0
  137. package/src/core/timings.ts +25 -0
  138. package/src/core/title-generator.ts +110 -0
  139. package/src/core/tools/ask.ts +193 -0
  140. package/src/core/tools/bash-interceptor.ts +120 -0
  141. package/src/core/tools/bash.ts +91 -0
  142. package/src/core/tools/context.ts +32 -0
  143. package/src/core/tools/edit-diff.ts +487 -0
  144. package/src/core/tools/edit.ts +140 -0
  145. package/src/core/tools/exa/company.ts +59 -0
  146. package/src/core/tools/exa/index.ts +63 -0
  147. package/src/core/tools/exa/linkedin.ts +59 -0
  148. package/src/core/tools/exa/mcp-client.ts +368 -0
  149. package/src/core/tools/exa/render.ts +200 -0
  150. package/src/core/tools/exa/researcher.ts +90 -0
  151. package/src/core/tools/exa/search.ts +338 -0
  152. package/src/core/tools/exa/types.ts +167 -0
  153. package/src/core/tools/exa/websets.ts +248 -0
  154. package/src/core/tools/find.ts +244 -0
  155. package/src/core/tools/grep.ts +584 -0
  156. package/src/core/tools/index.ts +283 -0
  157. package/src/core/tools/ls.ts +142 -0
  158. package/src/core/tools/lsp/client.ts +767 -0
  159. package/src/core/tools/lsp/clients/biome-client.ts +207 -0
  160. package/src/core/tools/lsp/clients/index.ts +49 -0
  161. package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
  162. package/src/core/tools/lsp/config.ts +845 -0
  163. package/src/core/tools/lsp/edits.ts +110 -0
  164. package/src/core/tools/lsp/index.ts +1364 -0
  165. package/src/core/tools/lsp/render.ts +560 -0
  166. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  167. package/src/core/tools/lsp/types.ts +495 -0
  168. package/src/core/tools/lsp/utils.ts +526 -0
  169. package/src/core/tools/notebook.ts +182 -0
  170. package/src/core/tools/output.ts +198 -0
  171. package/src/core/tools/path-utils.ts +61 -0
  172. package/src/core/tools/read.ts +507 -0
  173. package/src/core/tools/renderers.ts +820 -0
  174. package/src/core/tools/review.ts +275 -0
  175. package/src/core/tools/rulebook.ts +124 -0
  176. package/src/core/tools/task/agents.ts +158 -0
  177. package/src/core/tools/task/artifacts.ts +114 -0
  178. package/src/core/tools/task/commands.ts +157 -0
  179. package/src/core/tools/task/discovery.ts +217 -0
  180. package/src/core/tools/task/executor.ts +531 -0
  181. package/src/core/tools/task/index.ts +548 -0
  182. package/src/core/tools/task/model-resolver.ts +176 -0
  183. package/src/core/tools/task/parallel.ts +38 -0
  184. package/src/core/tools/task/render.ts +502 -0
  185. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  186. package/src/core/tools/task/types.ts +142 -0
  187. package/src/core/tools/truncate.ts +265 -0
  188. package/src/core/tools/web-fetch.ts +2511 -0
  189. package/src/core/tools/web-search/auth.ts +199 -0
  190. package/src/core/tools/web-search/index.ts +583 -0
  191. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  192. package/src/core/tools/web-search/providers/exa.ts +196 -0
  193. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  194. package/src/core/tools/web-search/render.ts +372 -0
  195. package/src/core/tools/web-search/types.ts +180 -0
  196. package/src/core/tools/write.ts +63 -0
  197. package/src/core/ttsr.ts +211 -0
  198. package/src/core/utils.ts +187 -0
  199. package/src/discovery/agents-md.ts +75 -0
  200. package/src/discovery/builtin.ts +647 -0
  201. package/src/discovery/claude.ts +623 -0
  202. package/src/discovery/cline.ts +104 -0
  203. package/src/discovery/codex.ts +571 -0
  204. package/src/discovery/cursor.ts +266 -0
  205. package/src/discovery/gemini.ts +368 -0
  206. package/src/discovery/github.ts +120 -0
  207. package/src/discovery/helpers.test.ts +127 -0
  208. package/src/discovery/helpers.ts +249 -0
  209. package/src/discovery/index.ts +84 -0
  210. package/src/discovery/mcp-json.ts +127 -0
  211. package/src/discovery/vscode.ts +99 -0
  212. package/src/discovery/windsurf.ts +219 -0
  213. package/src/index.ts +192 -0
  214. package/src/main.ts +507 -0
  215. package/src/migrations.ts +156 -0
  216. package/src/modes/cleanup.ts +23 -0
  217. package/src/modes/index.ts +48 -0
  218. package/src/modes/interactive/components/armin.ts +382 -0
  219. package/src/modes/interactive/components/assistant-message.ts +86 -0
  220. package/src/modes/interactive/components/bash-execution.ts +199 -0
  221. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  222. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  223. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  224. package/src/modes/interactive/components/custom-editor.ts +122 -0
  225. package/src/modes/interactive/components/diff.ts +147 -0
  226. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  227. package/src/modes/interactive/components/extensions/extension-dashboard.ts +296 -0
  228. package/src/modes/interactive/components/extensions/extension-list.ts +479 -0
  229. package/src/modes/interactive/components/extensions/index.ts +9 -0
  230. package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
  231. package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
  232. package/src/modes/interactive/components/extensions/types.ts +191 -0
  233. package/src/modes/interactive/components/hook-editor.ts +117 -0
  234. package/src/modes/interactive/components/hook-input.ts +64 -0
  235. package/src/modes/interactive/components/hook-message.ts +96 -0
  236. package/src/modes/interactive/components/hook-selector.ts +91 -0
  237. package/src/modes/interactive/components/model-selector.ts +560 -0
  238. package/src/modes/interactive/components/oauth-selector.ts +136 -0
  239. package/src/modes/interactive/components/plugin-settings.ts +481 -0
  240. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  241. package/src/modes/interactive/components/session-selector.ts +220 -0
  242. package/src/modes/interactive/components/settings-defs.ts +597 -0
  243. package/src/modes/interactive/components/settings-selector.ts +545 -0
  244. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  245. package/src/modes/interactive/components/status-line/index.ts +4 -0
  246. package/src/modes/interactive/components/status-line/presets.ts +94 -0
  247. package/src/modes/interactive/components/status-line/segments.ts +350 -0
  248. package/src/modes/interactive/components/status-line/separators.ts +55 -0
  249. package/src/modes/interactive/components/status-line/types.ts +81 -0
  250. package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
  251. package/src/modes/interactive/components/status-line.ts +384 -0
  252. package/src/modes/interactive/components/theme-selector.ts +62 -0
  253. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  254. package/src/modes/interactive/components/tool-execution.ts +946 -0
  255. package/src/modes/interactive/components/tree-selector.ts +877 -0
  256. package/src/modes/interactive/components/ttsr-notification.ts +82 -0
  257. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  258. package/src/modes/interactive/components/user-message.ts +18 -0
  259. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  260. package/src/modes/interactive/components/welcome.ts +228 -0
  261. package/src/modes/interactive/interactive-mode.ts +2669 -0
  262. package/src/modes/interactive/theme/dark.json +102 -0
  263. package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
  264. package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
  265. package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
  266. package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
  267. package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
  268. package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
  269. package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
  270. package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
  271. package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
  272. package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
  273. package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
  274. package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
  275. package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
  276. package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
  277. package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
  278. package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
  279. package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
  280. package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
  281. package/src/modes/interactive/theme/defaults/index.ts +67 -0
  282. package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
  283. package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
  284. package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
  285. package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
  286. package/src/modes/interactive/theme/defaults/light-github.json +114 -0
  287. package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
  288. package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
  289. package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
  290. package/src/modes/interactive/theme/defaults/light-one.json +105 -0
  291. package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
  292. package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
  293. package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
  294. package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
  295. package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
  296. package/src/modes/interactive/theme/light.json +99 -0
  297. package/src/modes/interactive/theme/theme-schema.json +424 -0
  298. package/src/modes/interactive/theme/theme.ts +2211 -0
  299. package/src/modes/print-mode.ts +163 -0
  300. package/src/modes/rpc/rpc-client.ts +527 -0
  301. package/src/modes/rpc/rpc-mode.ts +494 -0
  302. package/src/modes/rpc/rpc-types.ts +203 -0
  303. package/src/prompts/architect-plan.md +10 -0
  304. package/src/prompts/branch-summary-preamble.md +3 -0
  305. package/src/prompts/branch-summary.md +28 -0
  306. package/src/prompts/browser.md +71 -0
  307. package/src/prompts/compaction-summary.md +34 -0
  308. package/src/prompts/compaction-turn-prefix.md +16 -0
  309. package/src/prompts/compaction-update-summary.md +41 -0
  310. package/src/prompts/explore.md +82 -0
  311. package/src/prompts/implement-with-critic.md +11 -0
  312. package/src/prompts/implement.md +11 -0
  313. package/src/prompts/init.md +30 -0
  314. package/src/prompts/plan.md +54 -0
  315. package/src/prompts/reviewer.md +81 -0
  316. package/src/prompts/summarization-system.md +3 -0
  317. package/src/prompts/system-prompt.md +27 -0
  318. package/src/prompts/task.md +56 -0
  319. package/src/prompts/title-system.md +8 -0
  320. package/src/prompts/tools/ask.md +24 -0
  321. package/src/prompts/tools/bash.md +23 -0
  322. package/src/prompts/tools/edit.md +9 -0
  323. package/src/prompts/tools/find.md +6 -0
  324. package/src/prompts/tools/grep.md +12 -0
  325. package/src/prompts/tools/lsp.md +14 -0
  326. package/src/prompts/tools/output.md +23 -0
  327. package/src/prompts/tools/read.md +25 -0
  328. package/src/prompts/tools/web-fetch.md +8 -0
  329. package/src/prompts/tools/web-search.md +10 -0
  330. package/src/prompts/tools/write.md +10 -0
  331. package/src/utils/changelog.ts +99 -0
  332. package/src/utils/clipboard.ts +265 -0
  333. package/src/utils/fuzzy.ts +108 -0
  334. package/src/utils/mime.ts +30 -0
  335. package/src/utils/shell-snapshot.ts +218 -0
  336. package/src/utils/shell.ts +364 -0
  337. package/src/utils/tools-manager.ts +265 -0
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Anthropic Web Search Provider
3
+ *
4
+ * Uses Claude's built-in web_search_20250305 tool to search the web.
5
+ * Returns synthesized answers with citations and source metadata.
6
+ */
7
+
8
+ import { buildAnthropicHeaders, buildAnthropicUrl, findAnthropicAuth, getEnv } from "../auth";
9
+ import type {
10
+ AnthropicApiResponse,
11
+ AnthropicAuthConfig,
12
+ AnthropicCitation,
13
+ WebSearchCitation,
14
+ WebSearchResponse,
15
+ WebSearchSource,
16
+ } from "../types";
17
+
18
+ const DEFAULT_MODEL = "claude-sonnet-4-5-20250514";
19
+ const DEFAULT_MAX_TOKENS = 4096;
20
+
21
+ export interface AnthropicSearchParams {
22
+ query: string;
23
+ system_prompt?: string;
24
+ max_tokens?: number;
25
+ num_results?: number;
26
+ }
27
+
28
+ /** Get model from env or use default */
29
+ async function getModel(): Promise<string> {
30
+ return (await getEnv("ANTHROPIC_SEARCH_MODEL")) ?? DEFAULT_MODEL;
31
+ }
32
+
33
+ /** Call Anthropic API with web search */
34
+ async function callWebSearch(
35
+ auth: AnthropicAuthConfig,
36
+ model: string,
37
+ query: string,
38
+ systemPrompt?: string,
39
+ maxTokens?: number,
40
+ ): Promise<AnthropicApiResponse> {
41
+ const url = buildAnthropicUrl(auth);
42
+ const headers = buildAnthropicHeaders(auth);
43
+
44
+ // Build system blocks
45
+ const systemBlocks: Array<{ type: string; text: string; cache_control?: { type: string } }> = [];
46
+
47
+ if (auth.isOAuth) {
48
+ // OAuth requires Claude Code identity with cache_control
49
+ systemBlocks.push({
50
+ type: "text",
51
+ text: "You are a helpful AI assistant with web search capabilities.",
52
+ cache_control: { type: "ephemeral" },
53
+ });
54
+ }
55
+
56
+ if (systemPrompt) {
57
+ systemBlocks.push({
58
+ type: "text",
59
+ text: systemPrompt,
60
+ ...(auth.isOAuth ? { cache_control: { type: "ephemeral" } } : {}),
61
+ });
62
+ }
63
+
64
+ const body: Record<string, unknown> = {
65
+ model,
66
+ max_tokens: maxTokens ?? DEFAULT_MAX_TOKENS,
67
+ messages: [{ role: "user", content: query }],
68
+ tools: [{ type: "web_search_20250305", name: "web_search" }],
69
+ };
70
+
71
+ if (systemBlocks.length > 0) {
72
+ body.system = systemBlocks;
73
+ }
74
+
75
+ const response = await fetch(url, {
76
+ method: "POST",
77
+ headers,
78
+ body: JSON.stringify(body),
79
+ });
80
+
81
+ if (!response.ok) {
82
+ const errorText = await response.text();
83
+ throw new Error(`Anthropic API error (${response.status}): ${errorText}`);
84
+ }
85
+
86
+ return response.json() as Promise<AnthropicApiResponse>;
87
+ }
88
+
89
+ /** Parse page_age string into seconds (e.g., "2 days ago", "3h ago", "1 week ago") */
90
+ function parsePageAge(pageAge: string | null | undefined): number | undefined {
91
+ if (!pageAge) return undefined;
92
+
93
+ const match = pageAge.match(/^(\d+)\s*(s|sec|second|m|min|minute|h|hour|d|day|w|week|mo|month|y|year)s?\s*(ago)?$/i);
94
+ if (!match) return undefined;
95
+
96
+ const value = parseInt(match[1], 10);
97
+ const unit = match[2].toLowerCase();
98
+
99
+ const multipliers: Record<string, number> = {
100
+ s: 1,
101
+ sec: 1,
102
+ second: 1,
103
+ m: 60,
104
+ min: 60,
105
+ minute: 60,
106
+ h: 3600,
107
+ hour: 3600,
108
+ d: 86400,
109
+ day: 86400,
110
+ w: 604800,
111
+ week: 604800,
112
+ mo: 2592000,
113
+ month: 2592000,
114
+ y: 31536000,
115
+ year: 31536000,
116
+ };
117
+
118
+ return value * (multipliers[unit] ?? 86400);
119
+ }
120
+
121
+ /** Parse API response into unified WebSearchResponse */
122
+ function parseResponse(response: AnthropicApiResponse): WebSearchResponse {
123
+ const answerParts: string[] = [];
124
+ const searchQueries: string[] = [];
125
+ const sources: WebSearchSource[] = [];
126
+ const citations: WebSearchCitation[] = [];
127
+
128
+ for (const block of response.content) {
129
+ if (block.type === "server_tool_use" && block.name === "web_search") {
130
+ // Intermediate search query
131
+ if (block.input?.query) {
132
+ searchQueries.push(block.input.query);
133
+ }
134
+ } else if (block.type === "web_search_tool_result" && block.content) {
135
+ // Search results
136
+ for (const result of block.content) {
137
+ if (result.type === "web_search_result") {
138
+ sources.push({
139
+ title: result.title,
140
+ url: result.url,
141
+ snippet: result.encrypted_content,
142
+ publishedDate: result.page_age ?? undefined,
143
+ ageSeconds: parsePageAge(result.page_age),
144
+ });
145
+ }
146
+ }
147
+ } else if (block.type === "text" && block.text) {
148
+ // Synthesized answer with citations
149
+ answerParts.push(block.text);
150
+ if (block.citations) {
151
+ for (const c of block.citations as AnthropicCitation[]) {
152
+ citations.push({
153
+ url: c.url,
154
+ title: c.title,
155
+ citedText: c.cited_text,
156
+ });
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ return {
163
+ provider: "anthropic",
164
+ answer: answerParts.join("\n\n") || undefined,
165
+ sources,
166
+ citations: citations.length > 0 ? citations : undefined,
167
+ searchQueries: searchQueries.length > 0 ? searchQueries : undefined,
168
+ usage: {
169
+ inputTokens: response.usage.input_tokens,
170
+ outputTokens: response.usage.output_tokens,
171
+ searchRequests: response.usage.server_tool_use?.web_search_requests,
172
+ },
173
+ model: response.model,
174
+ requestId: response.id,
175
+ };
176
+ }
177
+
178
+ /** Execute Anthropic web search */
179
+ export async function searchAnthropic(params: AnthropicSearchParams): Promise<WebSearchResponse> {
180
+ const auth = await findAnthropicAuth();
181
+ if (!auth) {
182
+ throw new Error(
183
+ "No Anthropic credentials found. Set ANTHROPIC_API_KEY or configure OAuth in ~/.omp/agent/auth.json",
184
+ );
185
+ }
186
+
187
+ const model = await getModel();
188
+ const response = await callWebSearch(auth, model, params.query, params.system_prompt, params.max_tokens);
189
+
190
+ const result = parseResponse(response);
191
+
192
+ // Apply num_results limit if specified
193
+ if (params.num_results && result.sources.length > params.num_results) {
194
+ result.sources = result.sources.slice(0, params.num_results);
195
+ }
196
+
197
+ return result;
198
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Exa Web Search Provider
3
+ *
4
+ * High-quality neural search via Exa Search API.
5
+ * Returns structured search results with optional content extraction.
6
+ */
7
+
8
+ import type { WebSearchResponse, WebSearchSource } from "../types";
9
+
10
+ const EXA_API_URL = "https://api.exa.ai/search";
11
+
12
+ type ExaSearchType = "neural" | "fast" | "auto" | "deep";
13
+
14
+ type ExaSearchParamType = ExaSearchType | "keyword";
15
+
16
+ export interface ExaSearchParams {
17
+ query: string;
18
+ num_results?: number;
19
+ type?: ExaSearchParamType;
20
+ include_domains?: string[];
21
+ exclude_domains?: string[];
22
+ start_published_date?: string;
23
+ end_published_date?: string;
24
+ }
25
+
26
+ /** Parse a .env file and return key-value pairs */
27
+ async function parseEnvFile(filePath: string): Promise<Record<string, string>> {
28
+ const result: Record<string, string> = {};
29
+ try {
30
+ const file = Bun.file(filePath);
31
+ if (!(await file.exists())) return result;
32
+
33
+ const content = await file.text();
34
+ for (const line of content.split("\n")) {
35
+ let trimmed = line.trim();
36
+ if (!trimmed || trimmed.startsWith("#")) continue;
37
+
38
+ if (trimmed.startsWith("export ")) {
39
+ trimmed = trimmed.slice("export ".length).trim();
40
+ }
41
+
42
+ const eqIndex = trimmed.indexOf("=");
43
+ if (eqIndex === -1) continue;
44
+
45
+ const key = trimmed.slice(0, eqIndex).trim();
46
+ let value = trimmed.slice(eqIndex + 1).trim();
47
+
48
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
49
+ value = value.slice(1, -1);
50
+ }
51
+
52
+ result[key] = value;
53
+ }
54
+ } catch {
55
+ // Ignore read errors
56
+ }
57
+ return result;
58
+ }
59
+
60
+ function getHomeDir(): string | null {
61
+ return process.env.HOME ?? process.env.USERPROFILE ?? null;
62
+ }
63
+
64
+ /** Find EXA_API_KEY from environment or .env files */
65
+ export async function findApiKey(): Promise<string | null> {
66
+ // 1. Check environment variable
67
+ if (process.env.EXA_API_KEY) {
68
+ return process.env.EXA_API_KEY;
69
+ }
70
+
71
+ // 2. Check .env in current directory
72
+ const localEnv = await parseEnvFile(`${process.cwd()}/.env`);
73
+ if (localEnv.EXA_API_KEY) {
74
+ return localEnv.EXA_API_KEY;
75
+ }
76
+
77
+ // 3. Check ~/.env
78
+ const homeDir = getHomeDir();
79
+ if (homeDir) {
80
+ const homeEnv = await parseEnvFile(`${homeDir}/.env`);
81
+ if (homeEnv.EXA_API_KEY) {
82
+ return homeEnv.EXA_API_KEY;
83
+ }
84
+ }
85
+
86
+ return null;
87
+ }
88
+
89
+ interface ExaSearchResult {
90
+ title?: string | null;
91
+ url?: string | null;
92
+ author?: string | null;
93
+ publishedDate?: string | null;
94
+ text?: string | null;
95
+ highlights?: string[] | null;
96
+ }
97
+
98
+ interface ExaSearchResponse {
99
+ requestId?: string;
100
+ resolvedSearchType?: string;
101
+ results?: ExaSearchResult[];
102
+ costDollars?: { total: number };
103
+ searchTime?: number;
104
+ }
105
+
106
+ function normalizeSearchType(type: ExaSearchParamType | undefined): ExaSearchType {
107
+ if (!type) return "auto";
108
+ if (type === "keyword") return "fast";
109
+ return type;
110
+ }
111
+
112
+ /** Call Exa Search API */
113
+ async function callExaSearch(apiKey: string, params: ExaSearchParams): Promise<ExaSearchResponse> {
114
+ const body: Record<string, unknown> = {
115
+ query: params.query,
116
+ numResults: params.num_results ?? 10,
117
+ type: normalizeSearchType(params.type),
118
+ };
119
+
120
+ if (params.include_domains?.length) {
121
+ body.includeDomains = params.include_domains;
122
+ }
123
+ if (params.exclude_domains?.length) {
124
+ body.excludeDomains = params.exclude_domains;
125
+ }
126
+ if (params.start_published_date) {
127
+ body.startPublishedDate = params.start_published_date;
128
+ }
129
+ if (params.end_published_date) {
130
+ body.endPublishedDate = params.end_published_date;
131
+ }
132
+
133
+ const response = await fetch(EXA_API_URL, {
134
+ method: "POST",
135
+ headers: {
136
+ "Content-Type": "application/json",
137
+ "x-api-key": apiKey,
138
+ },
139
+ body: JSON.stringify(body),
140
+ });
141
+
142
+ if (!response.ok) {
143
+ const errorText = await response.text();
144
+ throw new Error(`Exa API error (${response.status}): ${errorText}`);
145
+ }
146
+
147
+ return response.json() as Promise<ExaSearchResponse>;
148
+ }
149
+
150
+ /** Calculate age in seconds from ISO date string */
151
+ function dateToAgeSeconds(dateStr: string | null | undefined): number | undefined {
152
+ if (!dateStr) return undefined;
153
+ try {
154
+ const date = new Date(dateStr);
155
+ if (Number.isNaN(date.getTime())) return undefined;
156
+ return Math.floor((Date.now() - date.getTime()) / 1000);
157
+ } catch {
158
+ return undefined;
159
+ }
160
+ }
161
+
162
+ /** Execute Exa web search */
163
+ export async function searchExa(params: ExaSearchParams): Promise<WebSearchResponse> {
164
+ const apiKey = await findApiKey();
165
+ if (!apiKey) {
166
+ throw new Error("EXA_API_KEY not found. Set it in environment or .env file.");
167
+ }
168
+
169
+ const response = await callExaSearch(apiKey, params);
170
+
171
+ // Convert to unified WebSearchResponse
172
+ const sources: WebSearchSource[] = [];
173
+
174
+ if (response.results) {
175
+ for (const result of response.results) {
176
+ if (!result.url) continue;
177
+ sources.push({
178
+ title: result.title ?? result.url,
179
+ url: result.url,
180
+ snippet: result.text ?? result.highlights?.join(" ") ?? undefined,
181
+ publishedDate: result.publishedDate ?? undefined,
182
+ ageSeconds: dateToAgeSeconds(result.publishedDate ?? undefined),
183
+ author: result.author ?? undefined,
184
+ });
185
+ }
186
+ }
187
+
188
+ // Apply num_results limit if specified
189
+ const limitedSources = params.num_results ? sources.slice(0, params.num_results) : sources;
190
+
191
+ return {
192
+ provider: "exa",
193
+ sources: limitedSources,
194
+ requestId: response.requestId,
195
+ };
196
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Perplexity Web Search Provider
3
+ *
4
+ * Supports both sonar (fast) and sonar-pro (comprehensive) models.
5
+ * Returns synthesized answers with citations and related questions.
6
+ */
7
+
8
+ import * as os from "node:os";
9
+ import type {
10
+ PerplexityRequest,
11
+ PerplexityResponse,
12
+ WebSearchCitation,
13
+ WebSearchResponse,
14
+ WebSearchSource,
15
+ } from "../types";
16
+
17
+ const PERPLEXITY_API_URL = "https://api.perplexity.ai/chat/completions";
18
+
19
+ export interface PerplexitySearchParams {
20
+ query: string;
21
+ model?: "sonar" | "sonar-pro";
22
+ system_prompt?: string;
23
+ search_recency_filter?: "day" | "week" | "month" | "year";
24
+ search_domain_filter?: string[];
25
+ search_context_size?: "low" | "medium" | "high";
26
+ return_related_questions?: boolean;
27
+ num_results?: number;
28
+ }
29
+
30
+ /** Parse a .env file and return key-value pairs */
31
+ async function parseEnvFile(filePath: string): Promise<Record<string, string>> {
32
+ const result: Record<string, string> = {};
33
+ try {
34
+ const file = Bun.file(filePath);
35
+ if (!(await file.exists())) return result;
36
+
37
+ const content = await file.text();
38
+ for (const line of content.split("\n")) {
39
+ const trimmed = line.trim();
40
+ if (!trimmed || trimmed.startsWith("#")) continue;
41
+
42
+ const eqIndex = trimmed.indexOf("=");
43
+ if (eqIndex === -1) continue;
44
+
45
+ const key = trimmed.slice(0, eqIndex).trim();
46
+ let value = trimmed.slice(eqIndex + 1).trim();
47
+
48
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
49
+ value = value.slice(1, -1);
50
+ }
51
+
52
+ result[key] = value;
53
+ }
54
+ } catch {
55
+ // Ignore read errors
56
+ }
57
+ return result;
58
+ }
59
+
60
+ /** Find PERPLEXITY_API_KEY from environment or .env files */
61
+ export async function findApiKey(): Promise<string | null> {
62
+ // 1. Check environment variable
63
+ if (process.env.PERPLEXITY_API_KEY) {
64
+ return process.env.PERPLEXITY_API_KEY;
65
+ }
66
+
67
+ // 2. Check .env in current directory
68
+ const localEnv = await parseEnvFile(`${process.cwd()}/.env`);
69
+ if (localEnv.PERPLEXITY_API_KEY) {
70
+ return localEnv.PERPLEXITY_API_KEY;
71
+ }
72
+
73
+ // 3. Check ~/.env
74
+ const homeEnv = await parseEnvFile(`${os.homedir()}/.env`);
75
+ if (homeEnv.PERPLEXITY_API_KEY) {
76
+ return homeEnv.PERPLEXITY_API_KEY;
77
+ }
78
+
79
+ return null;
80
+ }
81
+
82
+ /** Call Perplexity API */
83
+ async function callPerplexity(apiKey: string, request: PerplexityRequest): Promise<PerplexityResponse> {
84
+ const response = await fetch(PERPLEXITY_API_URL, {
85
+ method: "POST",
86
+ headers: {
87
+ Authorization: `Bearer ${apiKey}`,
88
+ "Content-Type": "application/json",
89
+ },
90
+ body: JSON.stringify(request),
91
+ });
92
+
93
+ if (!response.ok) {
94
+ const errorText = await response.text();
95
+ throw new Error(`Perplexity API error (${response.status}): ${errorText}`);
96
+ }
97
+
98
+ return response.json() as Promise<PerplexityResponse>;
99
+ }
100
+
101
+ /** Calculate age in seconds from ISO date string */
102
+ function dateToAgeSeconds(dateStr: string | undefined): number | undefined {
103
+ if (!dateStr) return undefined;
104
+ try {
105
+ const date = new Date(dateStr);
106
+ if (Number.isNaN(date.getTime())) return undefined;
107
+ return Math.floor((Date.now() - date.getTime()) / 1000);
108
+ } catch {
109
+ return undefined;
110
+ }
111
+ }
112
+
113
+ /** Parse API response into unified WebSearchResponse */
114
+ function parseResponse(response: PerplexityResponse): WebSearchResponse {
115
+ const answer = response.choices[0]?.message?.content ?? "";
116
+
117
+ // Build sources by matching citations to search_results
118
+ const sources: WebSearchSource[] = [];
119
+ const citations: WebSearchCitation[] = [];
120
+
121
+ const citationUrls = response.citations ?? [];
122
+ const searchResults = response.search_results ?? [];
123
+
124
+ for (const url of citationUrls) {
125
+ const searchResult = searchResults.find((r) => r.url === url);
126
+ sources.push({
127
+ title: searchResult?.title ?? url,
128
+ url,
129
+ snippet: searchResult?.snippet,
130
+ publishedDate: searchResult?.date,
131
+ ageSeconds: dateToAgeSeconds(searchResult?.date),
132
+ });
133
+ citations.push({
134
+ url,
135
+ title: searchResult?.title ?? url,
136
+ });
137
+ }
138
+
139
+ return {
140
+ provider: "perplexity",
141
+ answer: answer || undefined,
142
+ sources,
143
+ citations: citations.length > 0 ? citations : undefined,
144
+ relatedQuestions: response.related_questions,
145
+ usage: {
146
+ inputTokens: response.usage.prompt_tokens,
147
+ outputTokens: response.usage.completion_tokens,
148
+ totalTokens: response.usage.total_tokens,
149
+ },
150
+ model: response.model,
151
+ requestId: response.id,
152
+ };
153
+ }
154
+
155
+ /** Execute Perplexity web search */
156
+ export async function searchPerplexity(params: PerplexitySearchParams): Promise<WebSearchResponse> {
157
+ const apiKey = await findApiKey();
158
+ if (!apiKey) {
159
+ throw new Error("PERPLEXITY_API_KEY not found. Set it in environment or .env file.");
160
+ }
161
+
162
+ const messages: PerplexityRequest["messages"] = [];
163
+ if (params.system_prompt) {
164
+ messages.push({ role: "system", content: params.system_prompt });
165
+ }
166
+ messages.push({ role: "user", content: params.query });
167
+
168
+ const request: PerplexityRequest = {
169
+ model: params.model ?? "sonar",
170
+ messages,
171
+ // Default to true for related questions (unlike original which hardcoded false)
172
+ return_related_questions: params.return_related_questions ?? true,
173
+ };
174
+
175
+ // Add optional parameters
176
+ if (params.search_recency_filter) {
177
+ request.search_recency_filter = params.search_recency_filter;
178
+ }
179
+ if (params.search_domain_filter && params.search_domain_filter.length > 0) {
180
+ request.search_domain_filter = params.search_domain_filter;
181
+ }
182
+ if (params.search_context_size) {
183
+ request.search_context_size = params.search_context_size;
184
+ }
185
+
186
+ const response = await callPerplexity(apiKey, request);
187
+ const result = parseResponse(response);
188
+
189
+ // Apply num_results limit if specified
190
+ if (params.num_results && result.sources.length > params.num_results) {
191
+ result.sources = result.sources.slice(0, params.num_results);
192
+ }
193
+
194
+ return result;
195
+ }