@oh-my-pi/pi-coding-agent 16.1.23 → 16.2.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 (254) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/cli.js +3817 -3735
  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 +2 -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/components/__tests__/move-overlay.test.d.ts +1 -0
  35. package/dist/types/modes/components/custom-editor.d.ts +7 -0
  36. package/dist/types/modes/components/move-overlay.d.ts +23 -0
  37. package/dist/types/modes/components/plugin-selector.d.ts +2 -1
  38. package/dist/types/modes/components/queue-mode-selector.d.ts +2 -1
  39. package/dist/types/modes/components/select-list-mouse-routing.d.ts +2 -0
  40. package/dist/types/modes/components/settings-defs.d.ts +4 -1
  41. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  42. package/dist/types/modes/components/show-images-selector.d.ts +2 -1
  43. package/dist/types/modes/components/status-line/component.d.ts +2 -0
  44. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  45. package/dist/types/modes/components/theme-selector.d.ts +2 -1
  46. package/dist/types/modes/components/thinking-selector.d.ts +2 -1
  47. package/dist/types/modes/controllers/command-controller.d.ts +10 -1
  48. package/dist/types/modes/controllers/streaming-reveal.d.ts +6 -0
  49. package/dist/types/modes/interactive-mode.d.ts +10 -1
  50. package/dist/types/modes/internal-url-autocomplete.d.ts +1 -1
  51. package/dist/types/modes/running-subagent-badge.d.ts +6 -0
  52. package/dist/types/modes/theme/theme.d.ts +1 -1
  53. package/dist/types/modes/types.d.ts +9 -1
  54. package/dist/types/sdk.d.ts +2 -2
  55. package/dist/types/session/agent-session.d.ts +2 -1
  56. package/dist/types/session/session-manager.d.ts +13 -0
  57. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  58. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +1 -0
  59. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +1 -0
  60. package/dist/types/ssh/connection-manager.d.ts +2 -0
  61. package/dist/types/ssh/file-transfer.d.ts +79 -0
  62. package/dist/types/ssh/utils.d.ts +6 -0
  63. package/dist/types/system-prompt.d.ts +3 -0
  64. package/dist/types/task/executor.d.ts +16 -0
  65. package/dist/types/tiny/text.d.ts +1 -1
  66. package/dist/types/tools/builtin-names.d.ts +5 -1
  67. package/dist/types/tools/{find.d.ts → glob.d.ts} +15 -15
  68. package/dist/types/tools/{search.d.ts → grep.d.ts} +14 -14
  69. package/dist/types/tools/index.d.ts +3 -3
  70. package/dist/types/tools/path-utils.d.ts +29 -0
  71. package/dist/types/tools/read.d.ts +2 -2
  72. package/dist/types/tools/render-utils.d.ts +8 -0
  73. package/dist/types/tools/renderers.d.ts +11 -0
  74. package/dist/types/tools/ssh.d.ts +2 -0
  75. package/dist/types/tools/todo.d.ts +0 -16
  76. package/dist/types/tools/write.d.ts +2 -2
  77. package/dist/types/utils/active-repo-context.d.ts +8 -0
  78. package/dist/types/utils/image-resize.d.ts +1 -0
  79. package/dist/types/utils/markit-cache.d.ts +23 -0
  80. package/dist/types/utils/markit.d.ts +5 -1
  81. package/dist/types/utils/prompt-path.d.ts +1 -0
  82. package/dist/types/web/search/providers/duckduckgo.d.ts +14 -0
  83. package/dist/types/web/search/providers/firecrawl.d.ts +28 -0
  84. package/dist/types/web/search/providers/tinyfish.d.ts +29 -0
  85. package/dist/types/web/search/providers/xai.d.ts +13 -0
  86. package/dist/types/web/search/types.d.ts +18 -2
  87. package/package.json +30 -15
  88. package/scripts/build-binary.ts +9 -9
  89. package/scripts/bundle-dist.ts +2 -2
  90. package/src/advisor/__tests__/advisor.test.ts +40 -4
  91. package/src/advisor/__tests__/emission-guard.test.ts +147 -0
  92. package/src/advisor/advise-tool.ts +1 -1
  93. package/src/advisor/emission-guard.ts +172 -0
  94. package/src/advisor/index.ts +1 -0
  95. package/src/advisor/runtime.ts +11 -0
  96. package/src/advisor/watchdog.ts +12 -1
  97. package/src/cli/args.ts +5 -2
  98. package/src/cli/auth-broker-cli.ts +17 -0
  99. package/src/cli/config-cli.ts +4 -0
  100. package/src/cli/flag-tables.ts +7 -4
  101. package/src/cli/gallery-cli.ts +14 -2
  102. package/src/cli/gallery-fixtures/edit.ts +60 -0
  103. package/src/cli/gallery-fixtures/fs.ts +17 -17
  104. package/src/cli/gallery-fixtures/search.ts +4 -4
  105. package/src/cli/usage-cli.ts +20 -1
  106. package/src/cli/web-search-cli.ts +1 -1
  107. package/src/collab/display-name.ts +13 -0
  108. package/src/collab/guest.ts +5 -1
  109. package/src/collab/host.ts +2 -13
  110. package/src/commands/worktree.ts +6 -0
  111. package/src/config/inline-tool-descriptors-mode.ts +27 -0
  112. package/src/config/model-discovery.ts +77 -8
  113. package/src/config/model-registry.ts +89 -11
  114. package/src/config/model-resolver.ts +39 -1
  115. package/src/config/models-config-schema.ts +41 -5
  116. package/src/config/models-config.ts +4 -1
  117. package/src/config/settings-schema.ts +85 -27
  118. package/src/config/settings.ts +172 -1
  119. package/src/cursor.ts +1 -1
  120. package/src/dap/config.ts +152 -8
  121. package/src/dap/session.ts +1 -1
  122. package/src/discovery/helpers.ts +3 -1
  123. package/src/edit/hashline/diff.ts +14 -3
  124. package/src/edit/hashline/execute.ts +42 -6
  125. package/src/edit/hashline/filesystem.ts +38 -2
  126. package/src/edit/index.ts +1 -0
  127. package/src/edit/modes/patch.ts +11 -1
  128. package/src/edit/renderer.ts +140 -13
  129. package/src/eval/__tests__/agent-bridge.test.ts +101 -0
  130. package/src/eval/agent-bridge.ts +26 -3
  131. package/src/eval/js/tool-bridge.ts +2 -2
  132. package/src/export/html/index.ts +1 -1
  133. package/src/export/html/template.js +3 -1
  134. package/src/export/html/tool-views.generated.js +20 -20
  135. package/src/extensibility/extensions/types.ts +22 -22
  136. package/src/extensibility/hooks/types.ts +11 -11
  137. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +8 -3
  138. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +25 -10
  139. package/src/extensibility/plugins/marketplace/manager.ts +22 -0
  140. package/src/extensibility/plugins/marketplace/types.ts +1 -0
  141. package/src/internal-urls/__tests__/ssh-protocol.test.ts +331 -0
  142. package/src/internal-urls/docs-index.generated.txt +2 -2
  143. package/src/internal-urls/docs-index.ts +2 -3
  144. package/src/internal-urls/index.ts +1 -0
  145. package/src/internal-urls/registry-helpers.ts +19 -4
  146. package/src/internal-urls/router.ts +5 -3
  147. package/src/internal-urls/ssh-protocol.ts +367 -0
  148. package/src/internal-urls/types.ts +19 -2
  149. package/src/irc/bus.ts +11 -3
  150. package/src/mcp/tool-bridge.ts +32 -2
  151. package/src/memories/index.ts +1 -1
  152. package/src/modes/acp/acp-event-mapper.ts +2 -2
  153. package/src/modes/components/__tests__/move-overlay.test.ts +166 -0
  154. package/src/modes/components/agent-hub.ts +3 -0
  155. package/src/modes/components/agent-transcript-viewer.ts +9 -7
  156. package/src/modes/components/custom-editor.ts +90 -5
  157. package/src/modes/components/move-overlay.ts +282 -0
  158. package/src/modes/components/plan-review-overlay.ts +35 -35
  159. package/src/modes/components/plugin-selector.ts +6 -1
  160. package/src/modes/components/queue-mode-selector.ts +6 -1
  161. package/src/modes/components/select-list-mouse-routing.ts +35 -0
  162. package/src/modes/components/session-selector.ts +11 -10
  163. package/src/modes/components/settings-defs.ts +14 -1
  164. package/src/modes/components/settings-selector.ts +196 -29
  165. package/src/modes/components/show-images-selector.ts +6 -1
  166. package/src/modes/components/status-line/component.ts +108 -28
  167. package/src/modes/components/status-line/segments.ts +5 -1
  168. package/src/modes/components/status-line/types.ts +2 -0
  169. package/src/modes/components/theme-selector.ts +6 -1
  170. package/src/modes/components/thinking-selector.ts +6 -1
  171. package/src/modes/components/tool-execution.ts +25 -9
  172. package/src/modes/components/tree-selector.ts +5 -5
  173. package/src/modes/controllers/command-controller.ts +140 -47
  174. package/src/modes/controllers/event-controller.ts +59 -3
  175. package/src/modes/controllers/input-controller.ts +68 -2
  176. package/src/modes/controllers/selector-controller.ts +5 -2
  177. package/src/modes/controllers/streaming-reveal.ts +17 -0
  178. package/src/modes/controllers/tool-args-reveal.ts +1 -1
  179. package/src/modes/interactive-mode.ts +116 -50
  180. package/src/modes/internal-url-autocomplete.ts +17 -2
  181. package/src/modes/prompt-action-autocomplete.ts +3 -1
  182. package/src/modes/running-subagent-badge.ts +13 -0
  183. package/src/modes/setup-wizard/scenes/glyph.ts +2 -13
  184. package/src/modes/setup-wizard/scenes/providers.ts +2 -1
  185. package/src/modes/setup-wizard/scenes/theme.ts +6 -12
  186. package/src/modes/setup-wizard/scenes/web-search.ts +8 -13
  187. package/src/modes/setup-wizard/wizard-overlay.ts +6 -5
  188. package/src/modes/theme/theme.ts +9 -1
  189. package/src/modes/types.ts +9 -1
  190. package/src/modes/utils/interactive-context-helpers.ts +1 -1
  191. package/src/priority.json +15 -0
  192. package/src/prompts/advisor/active-repo-watchdog.md +6 -0
  193. package/src/prompts/advisor/system.md +21 -7
  194. package/src/prompts/agents/designer.md +1 -1
  195. package/src/prompts/agents/explore.md +1 -1
  196. package/src/prompts/agents/librarian.md +2 -2
  197. package/src/prompts/agents/plan.md +2 -2
  198. package/src/prompts/agents/reviewer.md +1 -1
  199. package/src/prompts/agents/task.md +2 -2
  200. package/src/prompts/system/active-repo-context.md +4 -0
  201. package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
  202. package/src/prompts/system/plan-mode-active.md +3 -3
  203. package/src/prompts/system/project-prompt.md +2 -2
  204. package/src/prompts/system/system-prompt.md +6 -6
  205. package/src/prompts/tools/bash.md +2 -2
  206. package/src/prompts/tools/checkpoint.md +1 -1
  207. package/src/prompts/tools/{find.md → glob.md} +1 -1
  208. package/src/prompts/tools/{search.md → grep.md} +3 -3
  209. package/src/prompts/tools/read.md +4 -2
  210. package/src/sdk.ts +55 -25
  211. package/src/session/agent-session.ts +508 -132
  212. package/src/session/session-history-format.ts +2 -2
  213. package/src/session/session-manager.ts +46 -0
  214. package/src/slash-commands/builtin-registry.ts +138 -20
  215. package/src/slash-commands/helpers/usage-report.ts +23 -2
  216. package/src/ssh/__tests__/connection-manager-args.test.ts +69 -0
  217. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +68 -0
  218. package/src/ssh/connection-manager.ts +12 -15
  219. package/src/ssh/file-transfer.ts +209 -0
  220. package/src/ssh/utils.ts +24 -0
  221. package/src/system-prompt.ts +56 -28
  222. package/src/task/executor.ts +96 -36
  223. package/src/task/index.ts +3 -3
  224. package/src/task/render.ts +14 -13
  225. package/src/task/worktree.ts +38 -1
  226. package/src/tiny/text.ts +49 -4
  227. package/src/tiny/worker.ts +3 -3
  228. package/src/tools/bash.ts +2 -2
  229. package/src/tools/builtin-names.ts +26 -2
  230. package/src/tools/{find.ts → glob.ts} +48 -42
  231. package/src/tools/{search.ts → grep.ts} +311 -129
  232. package/src/tools/index.ts +13 -14
  233. package/src/tools/irc.ts +6 -2
  234. package/src/tools/path-utils.ts +64 -1
  235. package/src/tools/read.ts +35 -6
  236. package/src/tools/render-utils.ts +14 -0
  237. package/src/tools/renderers.ts +15 -4
  238. package/src/tools/ssh.ts +17 -2
  239. package/src/tools/todo.ts +4 -26
  240. package/src/tools/write.ts +25 -9
  241. package/src/utils/active-repo-context.ts +143 -0
  242. package/src/utils/image-resize.ts +88 -7
  243. package/src/utils/lang-from-path.ts +3 -3
  244. package/src/utils/markit-cache.ts +166 -0
  245. package/src/utils/markit.ts +86 -18
  246. package/src/utils/prompt-path.ts +3 -0
  247. package/src/utils/title-generator.ts +1 -1
  248. package/src/web/search/provider.ts +54 -34
  249. package/src/web/search/providers/duckduckgo.ts +140 -0
  250. package/src/web/search/providers/firecrawl.ts +144 -0
  251. package/src/web/search/providers/tinyfish.ts +159 -0
  252. package/src/web/search/providers/xai.ts +292 -0
  253. package/src/web/search/providers/zai.ts +142 -56
  254. package/src/web/search/types.ts +6 -2
@@ -0,0 +1,292 @@
1
+ import { type ApiKey, type AuthStorage, withAuth } from "@oh-my-pi/pi-ai";
2
+ import type { SearchCitation, SearchResponse, SearchSource, SearchUsage } 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 XAI_RESPONSES_URL = "https://api.x.ai/v1/responses";
10
+ const XAI_WEB_SEARCH_MODEL = "grok-4.3";
11
+ const DEFAULT_NUM_RESULTS = 10;
12
+ const MAX_NUM_RESULTS = 30;
13
+ const RECENCY_DAYS: Record<NonNullable<SearchParams["recency"]>, number> = {
14
+ day: 1,
15
+ week: 7,
16
+ month: 30,
17
+ year: 365,
18
+ };
19
+
20
+ function formatUtcDate(date: Date): string {
21
+ return date.toISOString().slice(0, 10);
22
+ }
23
+
24
+ function buildRecencyDateBounds(
25
+ recency: NonNullable<SearchParams["recency"]>,
26
+ now = new Date(),
27
+ ): {
28
+ from_date: string;
29
+ to_date: string;
30
+ } {
31
+ const toDate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
32
+ const fromDate = new Date(toDate);
33
+ fromDate.setUTCDate(fromDate.getUTCDate() - RECENCY_DAYS[recency]);
34
+ return {
35
+ from_date: formatUtcDate(fromDate),
36
+ to_date: formatUtcDate(toDate),
37
+ };
38
+ }
39
+
40
+ interface XAIUrlCitationAnnotation {
41
+ type?: string;
42
+ url?: string | null;
43
+ title?: string | null;
44
+ text?: string | null;
45
+ cited_text?: string | null;
46
+ }
47
+
48
+ interface XAIResponseContentPart {
49
+ type?: string;
50
+ text?: string | null;
51
+ output_text?: string | null;
52
+ annotations?: XAIUrlCitationAnnotation[] | null;
53
+ }
54
+
55
+ interface XAIResponseOutputItem {
56
+ content?: XAIResponseContentPart[] | null;
57
+ annotations?: XAIUrlCitationAnnotation[] | null;
58
+ }
59
+
60
+ interface XAIResponsesUsage {
61
+ input_tokens?: number;
62
+ output_tokens?: number;
63
+ total_tokens?: number;
64
+ inputTokens?: number;
65
+ outputTokens?: number;
66
+ totalTokens?: number;
67
+ }
68
+
69
+ interface XAIResponsesResponse {
70
+ id?: string;
71
+ model?: string;
72
+ output_text?: string | null;
73
+ output?: XAIResponseOutputItem[] | null;
74
+ annotations?: XAIUrlCitationAnnotation[] | null;
75
+ citations?: string[] | null;
76
+ usage?: XAIResponsesUsage | null;
77
+ }
78
+
79
+ function buildRequestBody(params: SearchParams): Record<string, unknown> {
80
+ const body: Record<string, unknown> = {
81
+ model: XAI_WEB_SEARCH_MODEL,
82
+ input: [
83
+ { role: "system", content: params.systemPrompt },
84
+ { role: "user", content: params.query },
85
+ ],
86
+ tools: [{ type: "web_search" }],
87
+ };
88
+
89
+ const requestedSearchResults = params.numSearchResults ?? params.limit;
90
+ const searchParameters: Record<string, unknown> = {};
91
+ if (requestedSearchResults !== undefined) {
92
+ searchParameters.max_search_results = clampNumResults(
93
+ requestedSearchResults,
94
+ DEFAULT_NUM_RESULTS,
95
+ MAX_NUM_RESULTS,
96
+ );
97
+ }
98
+ if (params.recency) {
99
+ Object.assign(searchParameters, buildRecencyDateBounds(params.recency));
100
+ }
101
+ if (Object.keys(searchParameters).length > 0) {
102
+ body.search_parameters = searchParameters;
103
+ }
104
+
105
+ if (params.maxOutputTokens !== undefined) {
106
+ body.max_output_tokens = params.maxOutputTokens;
107
+ }
108
+ if (params.temperature !== undefined) {
109
+ body.temperature = params.temperature;
110
+ }
111
+
112
+ return body;
113
+ }
114
+
115
+ async function postXAIResponses(
116
+ apiKey: string,
117
+ params: SearchParams,
118
+ body: Record<string, unknown>,
119
+ ): Promise<Response> {
120
+ return (params.fetch ?? fetch)(XAI_RESPONSES_URL, {
121
+ method: "POST",
122
+ headers: {
123
+ "Content-Type": "application/json",
124
+ Authorization: `Bearer ${apiKey}`,
125
+ },
126
+ body: JSON.stringify(body),
127
+ signal: withHardTimeout(params.signal),
128
+ });
129
+ }
130
+
131
+ function throwXAIResponsesError(status: number, errorText: string): never {
132
+ const classified = classifyProviderHttpError("xai", status, errorText);
133
+ if (classified) throw classified;
134
+ throw new SearchProviderError("xai", `xAI Responses API error (${status}): ${errorText}`, status);
135
+ }
136
+
137
+ async function callXAIResponses(apiKey: string, params: SearchParams): Promise<XAIResponsesResponse> {
138
+ const requestBody = buildRequestBody(params);
139
+ const response = await postXAIResponses(apiKey, params, requestBody);
140
+
141
+ if (!response.ok) {
142
+ throwXAIResponsesError(response.status, await response.text());
143
+ }
144
+
145
+ return (await response.json()) as XAIResponsesResponse;
146
+ }
147
+
148
+ function addCitationSource(
149
+ sources: SearchSource[],
150
+ citations: SearchCitation[],
151
+ seenUrls: Set<string>,
152
+ url: string,
153
+ title?: string | null,
154
+ citedText?: string | null,
155
+ ): void {
156
+ const trimmedUrl = url.trim();
157
+ if (!trimmedUrl || seenUrls.has(trimmedUrl)) return;
158
+ seenUrls.add(trimmedUrl);
159
+ const sourceTitle = title?.trim() || trimmedUrl;
160
+ const sourceSnippet = citedText?.trim() || undefined;
161
+
162
+ sources.push({
163
+ title: sourceTitle,
164
+ url: trimmedUrl,
165
+ snippet: sourceSnippet,
166
+ });
167
+ citations.push({
168
+ title: sourceTitle,
169
+ url: trimmedUrl,
170
+ citedText: sourceSnippet,
171
+ });
172
+ }
173
+
174
+ function collectAnnotationSources(
175
+ annotations: readonly XAIUrlCitationAnnotation[] | null | undefined,
176
+ sources: SearchSource[],
177
+ citations: SearchCitation[],
178
+ seenUrls: Set<string>,
179
+ ): void {
180
+ if (!annotations) return;
181
+ for (const annotation of annotations) {
182
+ if (annotation.type !== "url_citation" || !annotation.url) continue;
183
+ addCitationSource(
184
+ sources,
185
+ citations,
186
+ seenUrls,
187
+ annotation.url,
188
+ annotation.title,
189
+ annotation.cited_text ?? annotation.text,
190
+ );
191
+ }
192
+ }
193
+
194
+ function parseAnswer(response: XAIResponsesResponse): string | undefined {
195
+ const topLevelText = response.output_text?.trim();
196
+ if (topLevelText) return topLevelText;
197
+
198
+ const answerParts: string[] = [];
199
+ for (const item of response.output ?? []) {
200
+ for (const part of item.content ?? []) {
201
+ const text = part.output_text ?? part.text;
202
+ if ((part.type === "output_text" || part.type === "text") && text?.trim()) {
203
+ answerParts.push(text.trim());
204
+ }
205
+ }
206
+ }
207
+
208
+ const answer = answerParts.join("\n").trim();
209
+ return answer ? answer : undefined;
210
+ }
211
+
212
+ function parseUsage(usage: XAIResponsesUsage | null | undefined): SearchUsage | undefined {
213
+ if (!usage) return undefined;
214
+ const parsed: SearchUsage = {};
215
+ const inputTokens = usage.input_tokens ?? usage.inputTokens;
216
+ const outputTokens = usage.output_tokens ?? usage.outputTokens;
217
+ const totalTokens = usage.total_tokens ?? usage.totalTokens;
218
+
219
+ if (typeof inputTokens === "number") parsed.inputTokens = inputTokens;
220
+ if (typeof outputTokens === "number") parsed.outputTokens = outputTokens;
221
+ if (typeof totalTokens === "number") parsed.totalTokens = totalTokens;
222
+
223
+ return Object.keys(parsed).length > 0 ? parsed : undefined;
224
+ }
225
+
226
+ function applyResultCap(
227
+ sources: SearchSource[],
228
+ citations: SearchCitation[],
229
+ resultCap: number,
230
+ ): { sources: SearchSource[]; citations: SearchCitation[] } {
231
+ return {
232
+ sources: sources.slice(0, resultCap),
233
+ citations: citations.slice(0, resultCap),
234
+ };
235
+ }
236
+
237
+ function parseResponse(response: XAIResponsesResponse, resultCap: number): SearchResponse {
238
+ const sources: SearchSource[] = [];
239
+ const citations: SearchCitation[] = [];
240
+ const seenUrls = new Set<string>();
241
+
242
+ collectAnnotationSources(response.annotations, sources, citations, seenUrls);
243
+ for (const item of response.output ?? []) {
244
+ collectAnnotationSources(item.annotations, sources, citations, seenUrls);
245
+ for (const part of item.content ?? []) {
246
+ collectAnnotationSources(part.annotations, sources, citations, seenUrls);
247
+ }
248
+ }
249
+ for (const url of response.citations ?? []) {
250
+ addCitationSource(sources, citations, seenUrls, url);
251
+ }
252
+ const limited = applyResultCap(sources, citations, resultCap);
253
+
254
+ return {
255
+ provider: "xai",
256
+ answer: parseAnswer(response),
257
+ sources: limited.sources,
258
+ citations: limited.citations.length > 0 ? limited.citations : undefined,
259
+ usage: parseUsage(response.usage),
260
+ model: response.model,
261
+ requestId: response.id,
262
+ authMode: "api_key",
263
+ };
264
+ }
265
+
266
+ /** Execute xAI Responses API web search. */
267
+ export async function searchXAI(params: SearchParams): Promise<SearchResponse> {
268
+ const keyOrResolver: ApiKey = params.authStorage.resolver("xai", {
269
+ sessionId: params.sessionId,
270
+ });
271
+
272
+ const resultCap = clampNumResults(params.numSearchResults ?? params.limit, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
273
+ const response = await withAuth(keyOrResolver, (key: string) => callXAIResponses(key, params), {
274
+ signal: params.signal,
275
+ missingKeyMessage: 'xAI credentials not found. Set XAI_API_KEY or configure an API key for provider "xai".',
276
+ });
277
+ return parseResponse(response, resultCap);
278
+ }
279
+
280
+ /** Search provider for xAI web search. */
281
+ export class XAIProvider extends SearchProvider {
282
+ readonly id = "xai";
283
+ readonly label = "xAI";
284
+
285
+ isAvailable(authStorage: AuthStorage): boolean {
286
+ return authStorage.hasAuth("xai");
287
+ }
288
+
289
+ search(params: SearchParams): Promise<SearchResponse> {
290
+ return searchXAI(params);
291
+ }
292
+ }
@@ -5,7 +5,7 @@
5
5
  * the unified SearchResponse shape used by the web search tool.
6
6
  */
7
7
  import { type ApiKey, type AuthStorage, type FetchImpl, getEnvApiKey, withAuth } from "@oh-my-pi/pi-ai";
8
- import { asRecord, asString } from "../../../web/scrapers/utils";
8
+ import { isRecord } from "@oh-my-pi/pi-utils";
9
9
  import type { SearchResponse, SearchSource } from "../../../web/search/types";
10
10
  import { SearchProviderError } from "../../../web/search/types";
11
11
  import { dateToAgeSeconds } from "../utils";
@@ -54,49 +54,24 @@ interface JsonRpcPayload {
54
54
  error?: JsonRpcError;
55
55
  }
56
56
 
57
- /** Resolve Z.AI API credentials through the unified auth storage pipeline. */
58
- export async function findApiKey(
59
- authStorage: AuthStorage,
60
- sessionId?: string,
61
- signal?: AbortSignal,
62
- ): Promise<string | null> {
63
- return (await authStorage.getApiKey("zai", sessionId, { signal })) ?? null;
57
+ interface ZaiMcpPostResult {
58
+ parsed?: unknown;
59
+ sessionId?: string;
64
60
  }
65
61
 
66
- async function callZaiTool(
67
- apiKey: string,
68
- args: Record<string, unknown>,
69
- signal: AbortSignal | undefined,
70
- fetchImpl: FetchImpl,
71
- ): Promise<unknown> {
72
- const response = await fetchImpl(ZAI_MCP_URL, {
73
- method: "POST",
74
- headers: {
75
- Authorization: `Bearer ${apiKey}`,
76
- "Content-Type": "application/json",
77
- Accept: "application/json, text/event-stream",
78
- },
79
- body: JSON.stringify({
80
- jsonrpc: "2.0",
81
- id: crypto.randomUUID(),
82
- method: "tools/call",
83
- params: {
84
- name: ZAI_TOOL_NAME,
85
- arguments: args,
86
- },
87
- }),
88
- signal: withHardTimeout(signal),
89
- });
90
-
91
- if (!response.ok) {
92
- const errorText = await response.text();
93
- const classified = classifyProviderHttpError("zai", response.status, errorText);
94
- if (classified) throw classified;
95
- throw new SearchProviderError("zai", `Z.AI MCP error (${response.status}): ${errorText}`, response.status);
96
- }
62
+ const ZAI_MCP_PROTOCOL_VERSION = "2025-03-26";
63
+ const ZAI_MCP_CLIENT_INFO = {
64
+ name: "omp-coding-agent",
65
+ version: "1.0.0",
66
+ };
97
67
 
98
- const rawText = await response.text();
68
+ function asString(value: unknown): string | null {
69
+ if (typeof value !== "string") return null;
70
+ const trimmed = value.trim();
71
+ return trimmed.length > 0 ? trimmed : null;
72
+ }
99
73
 
74
+ function parseZaiMcpResponse(rawText: string): unknown {
100
75
  const parsedMessages: unknown[] = [];
101
76
  for (const line of rawText.split("\n")) {
102
77
  const trimmed = line.trim();
@@ -118,8 +93,64 @@ async function callZaiTool(
118
93
  }
119
94
  }
120
95
 
121
- const parsed = parsedMessages[parsedMessages.length - 1];
122
- const parsedRecord = asRecord(parsed);
96
+ return parsedMessages[parsedMessages.length - 1];
97
+ }
98
+
99
+ async function postZaiMcp(
100
+ apiKey: string,
101
+ method: string,
102
+ params: Record<string, unknown>,
103
+ sessionId: string | undefined,
104
+ signal: AbortSignal | undefined,
105
+ fetchImpl: FetchImpl,
106
+ expectResponse: boolean,
107
+ ): Promise<ZaiMcpPostResult> {
108
+ const headers: Record<string, string> = {
109
+ Authorization: `Bearer ${apiKey}`,
110
+ "Content-Type": "application/json",
111
+ Accept: "application/json, text/event-stream",
112
+ };
113
+ if (sessionId) {
114
+ headers["Mcp-Session-Id"] = sessionId;
115
+ }
116
+
117
+ const body: Record<string, unknown> = {
118
+ jsonrpc: "2.0",
119
+ method,
120
+ params,
121
+ };
122
+ if (expectResponse) {
123
+ body.id = crypto.randomUUID();
124
+ }
125
+
126
+ const response = await fetchImpl(ZAI_MCP_URL, {
127
+ method: "POST",
128
+ headers,
129
+ body: JSON.stringify(body),
130
+ signal: withHardTimeout(signal),
131
+ });
132
+
133
+ if (!response.ok) {
134
+ const errorText = await response.text();
135
+ const classified = classifyProviderHttpError("zai", response.status, errorText);
136
+ if (classified) throw classified;
137
+ throw new SearchProviderError("zai", `Z.AI MCP error (${response.status}): ${errorText}`, response.status);
138
+ }
139
+
140
+ const nextSessionId = response.headers.get("Mcp-Session-Id") ?? sessionId;
141
+ if (!expectResponse) {
142
+ await response.body?.cancel();
143
+ return { sessionId: nextSessionId };
144
+ }
145
+
146
+ return {
147
+ parsed: parseZaiMcpResponse(await response.text()),
148
+ sessionId: nextSessionId,
149
+ };
150
+ }
151
+
152
+ function readJsonRpcPayload(parsed: unknown): JsonRpcPayload {
153
+ const parsedRecord = isRecord(parsed) ? parsed : null;
123
154
  const directErrorCode = typeof parsedRecord?.code === "number" ? parsedRecord.code : undefined;
124
155
  const directErrorSuccess = parsedRecord?.success;
125
156
  const directErrorMessage =
@@ -132,6 +163,10 @@ async function callZaiTool(
132
163
  );
133
164
  }
134
165
 
166
+ if (!isRecord(parsed)) {
167
+ throw new SearchProviderError("zai", "Failed to parse Z.AI MCP response", 500);
168
+ }
169
+
135
170
  const payload = parsed as JsonRpcPayload;
136
171
  if (payload.error) {
137
172
  const status = typeof payload.error.code === "number" ? payload.error.code : 400;
@@ -142,11 +177,64 @@ async function callZaiTool(
142
177
  );
143
178
  }
144
179
 
145
- const resultRecord = asRecord(payload.result);
180
+ return payload;
181
+ }
182
+
183
+ /** Resolve Z.AI API credentials through the unified auth storage pipeline. */
184
+ export async function findApiKey(
185
+ authStorage: AuthStorage,
186
+ sessionId?: string,
187
+ signal?: AbortSignal,
188
+ ): Promise<string | null> {
189
+ return (await authStorage.getApiKey("zai", sessionId, { signal })) ?? null;
190
+ }
191
+
192
+ async function callZaiTool(
193
+ apiKey: string,
194
+ args: Record<string, unknown>,
195
+ signal: AbortSignal | undefined,
196
+ fetchImpl: FetchImpl,
197
+ ): Promise<unknown> {
198
+ const initialized = await postZaiMcp(
199
+ apiKey,
200
+ "initialize",
201
+ {
202
+ protocolVersion: ZAI_MCP_PROTOCOL_VERSION,
203
+ capabilities: {},
204
+ clientInfo: ZAI_MCP_CLIENT_INFO,
205
+ },
206
+ undefined,
207
+ signal,
208
+ fetchImpl,
209
+ true,
210
+ );
211
+ if (initialized.parsed !== undefined) {
212
+ readJsonRpcPayload(initialized.parsed);
213
+ }
214
+
215
+ await postZaiMcp(apiKey, "notifications/initialized", {}, initialized.sessionId, signal, fetchImpl, false);
216
+
217
+ const toolCall = await postZaiMcp(
218
+ apiKey,
219
+ "tools/call",
220
+ {
221
+ name: ZAI_TOOL_NAME,
222
+ arguments: args,
223
+ },
224
+ initialized.sessionId,
225
+ signal,
226
+ fetchImpl,
227
+ true,
228
+ );
229
+ const payload = readJsonRpcPayload(toolCall.parsed);
230
+ const resultRecord = isRecord(payload.result) ? payload.result : null;
146
231
  if (resultRecord?.isError === true) {
147
232
  const content = Array.isArray(resultRecord.content) ? resultRecord.content : [];
148
233
  const errorText = content
149
- .map(item => asString(asRecord(item)?.text))
234
+ .map(item => {
235
+ if (!isRecord(item)) return null;
236
+ return asString(item.text);
237
+ })
150
238
  .filter((text): text is string => text != null)
151
239
  .join("\n")
152
240
  .trim();
@@ -159,7 +247,7 @@ async function callZaiTool(
159
247
  return payload.result;
160
248
  }
161
249
 
162
- return parsed;
250
+ return toolCall.parsed;
163
251
  }
164
252
 
165
253
  async function callZaiSearch(apiKey: string, params: ZaiSearchParams): Promise<unknown> {
@@ -204,8 +292,8 @@ function getSearchResults(value: unknown): ZaiSearchResult[] {
204
292
  if (Array.isArray(value)) {
205
293
  return value as ZaiSearchResult[];
206
294
  }
207
- const obj = asRecord(value);
208
- if (!obj) return [];
295
+ if (!isRecord(value)) return [];
296
+ const obj = value;
209
297
 
210
298
  const searchResult = obj.search_result;
211
299
  if (Array.isArray(searchResult)) return searchResult as ZaiSearchResult[];
@@ -224,17 +312,15 @@ function parseSearchPayload(rawResult: unknown): {
224
312
  const candidates: unknown[] = [rawResult];
225
313
  const textParts: string[] = [];
226
314
 
227
- const root = asRecord(rawResult);
228
- if (root) {
229
- if (root.structuredContent) candidates.push(root.structuredContent);
230
- if (root.data) candidates.push(root.data);
231
- if (root.result) candidates.push(root.result);
315
+ if (isRecord(rawResult)) {
316
+ if (rawResult.structuredContent) candidates.push(rawResult.structuredContent);
317
+ if (rawResult.data) candidates.push(rawResult.data);
318
+ if (rawResult.result) candidates.push(rawResult.result);
232
319
 
233
- const content = root.content;
320
+ const content = rawResult.content;
234
321
  if (Array.isArray(content)) {
235
322
  for (const part of content) {
236
- const partObj = asRecord(part);
237
- const text = asString(partObj?.text);
323
+ const text = isRecord(part) ? asString(part.text) : null;
238
324
  if (!text) continue;
239
325
  textParts.push(text);
240
326
  try {
@@ -249,7 +335,7 @@ function parseSearchPayload(rawResult: unknown): {
249
335
  for (const candidate of candidates) {
250
336
  const results = getSearchResults(candidate);
251
337
  if (results.length > 0) {
252
- const obj = asRecord(candidate) as ZaiWebSearchResponse | null;
338
+ const obj = isRecord(candidate) ? (candidate as ZaiWebSearchResponse) : null;
253
339
  return {
254
340
  results,
255
341
  answer: textParts.length > 0 ? textParts.join("\n\n") : undefined,
@@ -30,16 +30,20 @@ export const SEARCH_PROVIDER_OPTIONS = [
30
30
  label: "OpenAI",
31
31
  description: "OpenAI's native web_search (uses ChatGPT OAuth via /login openai-codex)",
32
32
  },
33
+ { value: "xai", label: "xAI", description: "Grok web search via xAI Responses API (requires XAI_API_KEY)" },
33
34
  { value: "zai", label: "Z.AI", description: "Calls Z.AI webSearchPrime MCP" },
34
35
  { value: "exa", label: "Exa", description: "Uses Exa API when EXA_API_KEY is set; falls back to Exa MCP" },
36
+ { value: "tinyfish", label: "TinyFish", description: "Requires TINYFISH_API_KEY" },
35
37
  { value: "jina", label: "Jina", description: "Requires JINA_API_KEY" },
36
38
  { value: "kagi", label: "Kagi", description: "Requires KAGI_API_KEY and Kagi Search API beta access" },
37
39
  { value: "tavily", label: "Tavily", description: "Requires TAVILY_API_KEY" },
40
+ { value: "firecrawl", label: "Firecrawl", description: "Requires FIRECRAWL_API_KEY" },
38
41
  { value: "brave", label: "Brave", description: "Requires BRAVE_API_KEY" },
39
42
  { value: "kimi", label: "Kimi", description: "Requires MOONSHOT_SEARCH_API_KEY or MOONSHOT_API_KEY" },
40
43
  { value: "parallel", label: "Parallel", description: "Requires PARALLEL_API_KEY" },
41
44
  { value: "synthetic", label: "Synthetic", description: "Requires SYNTHETIC_API_KEY" },
42
45
  { value: "searxng", label: "SearXNG", description: "Requires SEARXNG_ENDPOINT or searxng.endpoint" },
46
+ { value: "duckduckgo", label: "DuckDuckGo", description: "Uses DuckDuckGo Instant Answer API (no API key)" },
43
47
  ] as const;
44
48
 
45
49
  /** Supported web search providers (every option except `auto`). */
@@ -81,7 +85,7 @@ export interface SearchSource {
81
85
  author?: string;
82
86
  }
83
87
 
84
- /** Citation with text reference (anthropic, perplexity) */
88
+ /** Citation with text reference (LLM-mediated providers) */
85
89
  export interface SearchCitation {
86
90
  url: string;
87
91
  title: string;
@@ -101,7 +105,7 @@ export interface SearchUsage {
101
105
  /** Unified response across providers */
102
106
  export interface SearchResponse {
103
107
  provider: SearchProviderId | "none";
104
- /** Synthesized answer text (anthropic, perplexity) */
108
+ /** Synthesized answer text (LLM-mediated providers) */
105
109
  answer?: string;
106
110
  /** Search result sources */
107
111
  sources: SearchSource[];