@herbertgao/pi-extensions 2026.9.9 → 2026.9.11

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 (170) hide show
  1. package/README.md +1 -0
  2. package/THIRD_PARTY_NOTICES.md +26 -0
  3. package/node_modules/@herbertgao/pi-bark/package.json +2 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +2 -2
  5. package/node_modules/@herbertgao/pi-cc-extensions/README.md +2 -2
  6. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -2
  7. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +12 -0
  8. package/node_modules/@herbertgao/pi-subagents/README.md +427 -120
  9. package/node_modules/@herbertgao/pi-subagents/docs/rpc.md +184 -0
  10. package/node_modules/@herbertgao/pi-subagents/docs/workflows.md +466 -0
  11. package/node_modules/@herbertgao/pi-subagents/examples/agent-tool-description.md +6 -6
  12. package/node_modules/@herbertgao/pi-subagents/examples/workflows/compose.js +52 -0
  13. package/node_modules/@herbertgao/pi-subagents/examples/workflows/fan-out-audit.js +56 -0
  14. package/node_modules/@herbertgao/pi-subagents/examples/workflows/gated-fix.js +60 -0
  15. package/node_modules/@herbertgao/pi-subagents/examples/workflows/lib/count-child.js +30 -0
  16. package/node_modules/@herbertgao/pi-subagents/examples/workflows/review-panel.js +68 -0
  17. package/node_modules/@herbertgao/pi-subagents/examples/workflows/structured-findings.js +81 -0
  18. package/node_modules/@herbertgao/pi-subagents/package.json +12 -10
  19. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +52 -12
  20. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +837 -146
  21. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +213 -39
  22. package/node_modules/@herbertgao/pi-subagents/src/cross-extension-rpc.ts +73 -14
  23. package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +101 -47
  24. package/node_modules/@herbertgao/pi-subagents/src/index.ts +2249 -914
  25. package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +13 -0
  26. package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +215 -0
  27. package/node_modules/@herbertgao/pi-subagents/src/mention.ts +147 -0
  28. package/node_modules/@herbertgao/pi-subagents/src/model-resolver.ts +9 -1
  29. package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +40 -26
  30. package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +18 -8
  31. package/node_modules/@herbertgao/pi-subagents/src/prompts.ts +46 -9
  32. package/node_modules/@herbertgao/pi-subagents/src/schedule.ts +21 -16
  33. package/node_modules/@herbertgao/pi-subagents/src/settings.ts +137 -7
  34. package/node_modules/@herbertgao/pi-subagents/src/structured-output.ts +136 -0
  35. package/node_modules/@herbertgao/pi-subagents/src/types.ts +126 -8
  36. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-mention.ts +274 -0
  37. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +20 -5
  38. package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +10 -4
  39. package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +167 -22
  40. package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-card.ts +555 -0
  41. package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-dialog.ts +1304 -0
  42. package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-menu.ts +226 -0
  43. package/node_modules/@herbertgao/pi-subagents/src/workflow/collisions.ts +122 -0
  44. package/node_modules/@herbertgao/pi-subagents/src/workflow/entry.ts +47 -0
  45. package/node_modules/@herbertgao/pi-subagents/src/workflow/host.ts +463 -0
  46. package/node_modules/@herbertgao/pi-subagents/src/workflow/journal.ts +164 -0
  47. package/node_modules/@herbertgao/pi-subagents/src/workflow/json-schema.ts +142 -0
  48. package/node_modules/@herbertgao/pi-subagents/src/workflow/meta.ts +401 -0
  49. package/node_modules/@herbertgao/pi-subagents/src/workflow/progress.ts +622 -0
  50. package/node_modules/@herbertgao/pi-subagents/src/workflow/runtime.ts +1399 -0
  51. package/node_modules/@herbertgao/pi-subagents/src/workflow/saved.ts +230 -0
  52. package/node_modules/@herbertgao/pi-subagents/src/workflow/task.ts +333 -0
  53. package/node_modules/@herbertgao/pi-subagents/src/workflow/tool-description.ts +200 -0
  54. package/node_modules/@herbertgao/pi-subagents/src/workflow/worker-source.ts +781 -0
  55. package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +97 -95
  56. package/node_modules/@herbertgao/pi-subagents/src/xml.ts +13 -0
  57. package/node_modules/@herbertgao/resume-from/package.json +1 -1
  58. package/node_modules/@herbertgao/sol-pi/README.md +3 -3
  59. package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +4 -4
  60. package/node_modules/@herbertgao/sol-pi/agents-install.md +4 -4
  61. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +6 -6
  62. package/node_modules/@herbertgao/sol-pi/package.json +2 -2
  63. package/node_modules/@narumitw/pi-btw/dist/index.ts +39 -89
  64. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  65. package/node_modules/@narumitw/pi-btw/package.json +4 -4
  66. package/node_modules/@narumitw/pi-btw/src/btw.ts +28 -87
  67. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +8 -0
  68. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +40 -37
  69. package/node_modules/@narumitw/pi-caffeinate/README.md +21 -66
  70. package/node_modules/@narumitw/pi-caffeinate/dist/index.ts +10 -41
  71. package/node_modules/@narumitw/pi-caffeinate/dist/index.ts.map +2 -2
  72. package/node_modules/@narumitw/pi-caffeinate/package.json +50 -51
  73. package/node_modules/@narumitw/pi-caffeinate/src/caffeinate.ts +637 -663
  74. package/node_modules/@narumitw/pi-caffeinate/src/dbus-inhibit.ts +114 -120
  75. package/node_modules/@narumitw/pi-caffeinate/src/inhibitor-process.ts +29 -29
  76. package/node_modules/@narumitw/pi-caffeinate/src/inhibitors.ts +108 -126
  77. package/node_modules/@narumitw/pi-caffeinate/src/settings.ts +124 -128
  78. package/node_modules/pi-multi-account/CHANGELOG.md +1209 -0
  79. package/node_modules/pi-multi-account/CONTRIBUTING.md +61 -0
  80. package/node_modules/pi-multi-account/LICENSE +21 -0
  81. package/node_modules/pi-multi-account/README.md +197 -0
  82. package/node_modules/pi-multi-account/SECURITY.md +27 -0
  83. package/node_modules/pi-multi-account/auth-file-transaction.ts +56 -0
  84. package/node_modules/pi-multi-account/child-usability.ts +233 -0
  85. package/node_modules/pi-multi-account/compaction-summary.ts +32 -0
  86. package/node_modules/pi-multi-account/completion-route-planner.ts +224 -0
  87. package/node_modules/pi-multi-account/context-guard.ts +420 -0
  88. package/node_modules/pi-multi-account/cursor/LICENSE +21 -0
  89. package/node_modules/pi-multi-account/cursor/NOTICE +2 -0
  90. package/node_modules/pi-multi-account/cursor/auth.ts +165 -0
  91. package/node_modules/pi-multi-account/cursor/bridge-handle.ts +155 -0
  92. package/node_modules/pi-multi-account/cursor/conversation-registry.ts +104 -0
  93. package/node_modules/pi-multi-account/cursor/cursor-models-raw.json +611 -0
  94. package/node_modules/pi-multi-account/cursor/cursor-shared.ts +192 -0
  95. package/node_modules/pi-multi-account/cursor/h2-bridge.mjs +175 -0
  96. package/node_modules/pi-multi-account/cursor/index.ts +572 -0
  97. package/node_modules/pi-multi-account/cursor/message-parsing.ts +323 -0
  98. package/node_modules/pi-multi-account/cursor/prompt-usage.ts +53 -0
  99. package/node_modules/pi-multi-account/cursor/proto/agent_pb.ts +15294 -0
  100. package/node_modules/pi-multi-account/cursor/proxy.ts +2510 -0
  101. package/node_modules/pi-multi-account/cursor/session-lifecycle.ts +40 -0
  102. package/node_modules/pi-multi-account/cursor/sse-keepalive.ts +24 -0
  103. package/node_modules/pi-multi-account/cursor/stream-lifecycle.ts +193 -0
  104. package/node_modules/pi-multi-account/cursor/upstream-watchdog.ts +88 -0
  105. package/node_modules/pi-multi-account/cursor-bridge.ts +240 -0
  106. package/node_modules/pi-multi-account/cursor-model-name.ts +12 -0
  107. package/node_modules/pi-multi-account/index.ts +11825 -0
  108. package/node_modules/pi-multi-account/model-catalog.ts +354 -0
  109. package/node_modules/pi-multi-account/package.json +101 -0
  110. package/node_modules/pi-multi-account/pi-contract.ts +281 -0
  111. package/node_modules/pi-multi-account/provider-payload-stream.ts +44 -0
  112. package/node_modules/pi-multi-account/provider-priority.ts +189 -0
  113. package/node_modules/pi-multi-account/slot-proxy-auth.ts +167 -0
  114. package/node_modules/pi-multi-account/slot-proxy.ts +344 -0
  115. package/node_modules/pi-multi-account/state-file-transaction.ts +67 -0
  116. package/node_modules/pi-multi-account/usage.ts +1099 -0
  117. package/node_modules/pi-typesafe/README.md +6 -2
  118. package/node_modules/pi-typesafe/dist/client.d.ts +11 -0
  119. package/node_modules/pi-typesafe/dist/client.js +45 -10
  120. package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
  121. package/node_modules/pi-typesafe/dist/index.js +1 -1
  122. package/node_modules/pi-typesafe/package.json +2 -2
  123. package/node_modules/pi-web-access/CHANGELOG.md +36 -0
  124. package/node_modules/pi-web-access/README.md +75 -18
  125. package/node_modules/pi-web-access/anysearch.ts +4 -15
  126. package/node_modules/pi-web-access/bocha.ts +3 -22
  127. package/node_modules/pi-web-access/brave.ts +3 -21
  128. package/node_modules/pi-web-access/brightdata.ts +5 -32
  129. package/node_modules/pi-web-access/content-find.ts +168 -53
  130. package/node_modules/pi-web-access/curator-page.ts +4 -1
  131. package/node_modules/pi-web-access/curator-run.ts +2 -1
  132. package/node_modules/pi-web-access/curator-server.ts +1 -0
  133. package/node_modules/pi-web-access/dist/index.js +24620 -0
  134. package/node_modules/pi-web-access/domain-filter-normalization.ts +14 -0
  135. package/node_modules/pi-web-access/duckduckgo.ts +3 -21
  136. package/node_modules/pi-web-access/extract.ts +3 -1
  137. package/node_modules/pi-web-access/firecrawl.ts +5 -29
  138. package/node_modules/pi-web-access/gemini-search.ts +81 -32
  139. package/node_modules/pi-web-access/index.ts +149 -148
  140. package/node_modules/pi-web-access/jina-search.ts +4 -15
  141. package/node_modules/pi-web-access/kagi.ts +4 -13
  142. package/node_modules/pi-web-access/kimi-search.ts +5 -30
  143. package/node_modules/pi-web-access/mistral-search.ts +1 -15
  144. package/node_modules/pi-web-access/ollama.ts +2 -7
  145. package/node_modules/pi-web-access/openai-search.ts +174 -36
  146. package/node_modules/pi-web-access/opencode-session-headers.ts +24 -0
  147. package/node_modules/pi-web-access/package.json +10 -4
  148. package/node_modules/pi-web-access/page-query.ts +10 -2
  149. package/node_modules/pi-web-access/parallel.ts +1 -15
  150. package/node_modules/pi-web-access/pdf-extract.ts +3 -0
  151. package/node_modules/pi-web-access/querit.ts +5 -29
  152. package/node_modules/pi-web-access/search-answer-formatting.ts +11 -0
  153. package/node_modules/pi-web-access/search-result-count-normalization.ts +4 -0
  154. package/node_modules/pi-web-access/search1api.ts +5 -29
  155. package/node_modules/pi-web-access/searchinfinity.ts +5 -29
  156. package/node_modules/pi-web-access/searxng.ts +3 -21
  157. package/node_modules/pi-web-access/serpapi.ts +5 -28
  158. package/node_modules/pi-web-access/serpbase.ts +3 -22
  159. package/node_modules/pi-web-access/serpdive.ts +3 -21
  160. package/node_modules/pi-web-access/serper.ts +5 -28
  161. package/node_modules/pi-web-access/serply.ts +197 -0
  162. package/node_modules/pi-web-access/source-check.ts +11 -47
  163. package/node_modules/pi-web-access/summary-review.ts +7 -3
  164. package/node_modules/pi-web-access/tavily.ts +3 -21
  165. package/node_modules/pi-web-access/tinyfish.ts +5 -29
  166. package/node_modules/pi-web-access/utils.ts +9 -1
  167. package/node_modules/pi-web-access/valyu.ts +5 -28
  168. package/node_modules/pi-web-access/xai-search.ts +1 -15
  169. package/node_modules/pi-web-access/xcrawl.ts +5 -32
  170. package/package.json +17 -11
@@ -1,8 +1,11 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { activityMonitor } from "./activity.ts";
3
+ import { normalizeDomain } from "./domain-filter-normalization.ts";
3
4
  import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
4
5
  import type { ExtractedContent, ExtractOptions } from "./extract.ts";
5
6
  import type { SearchOptions, SearchResponse } from "./perplexity.ts";
7
+ import { formatSearchResultsAsAnswer } from "./search-answer-formatting.ts";
8
+ import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
6
9
  import { getWebSearchConfigPath } from "./utils.ts";
7
10
 
8
11
  const QUERIT_SEARCH_URL = "https://api.querit.ai/v1/search";
@@ -123,26 +126,6 @@ function requestSignal(signal: AbortSignal | undefined, timeoutMs: number): Abor
123
126
  return signal ? AbortSignal.any([signal, timeout]) : timeout;
124
127
  }
125
128
 
126
- function normalizeNumResults(value: number | undefined): number {
127
- if (typeof value !== "number" || !Number.isFinite(value)) return 5;
128
- return Math.max(1, Math.min(Math.floor(value), 20));
129
- }
130
-
131
- function normalizeDomain(value: string): string | null {
132
- let input = value.trim().toLowerCase();
133
- if (!input) return null;
134
- if (input.startsWith("-")) input = input.slice(1).trim();
135
- if (!input) return null;
136
- try {
137
- const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
138
- input = parsed.hostname;
139
- } catch {
140
- input = input.split("/")[0]?.split(":")[0] ?? "";
141
- }
142
- input = input.replace(/^\.+|\.+$/g, "");
143
- return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
144
- }
145
-
146
129
  function mapDomainFilter(domainFilter: string[] | undefined): { include: string[]; exclude: string[] } {
147
130
  const include: string[] = [];
148
131
  const exclude: string[] = [];
@@ -176,7 +159,7 @@ function buildSearchBody(query: string, options: QueritSearchOptions): Record<st
176
159
  if (date) filters.timeRange = { date };
177
160
  return {
178
161
  query,
179
- count: normalizeNumResults(options.numResults),
162
+ count: normalizeSearchResultCount(options.numResults),
180
163
  ...(Object.keys(filters).length > 0 ? { filters } : {}),
181
164
  };
182
165
  }
@@ -273,13 +256,6 @@ function mapSearchResults(data: QueritSearchResponse): SearchResponse["results"]
273
256
  });
274
257
  }
275
258
 
276
- function buildAnswer(results: SearchResponse["results"]): string {
277
- return results.map((result) => {
278
- if (result.snippet) return `${result.snippet}\nSource: ${result.title} (${result.url})`;
279
- return `Source: ${result.title} (${result.url})`;
280
- }).join("\n\n");
281
- }
282
-
283
259
  function mapContentResult(result: QueritContentResult | undefined, requestedUrl: string): ExtractedContent | null {
284
260
  if (!result || typeof result.content !== "string" || result.content.trim().length === 0) return null;
285
261
  const metadata = result.extrasMeta;
@@ -365,7 +341,7 @@ export async function searchWithQuerit(
365
341
  );
366
342
  assertApiSuccess("Search", data);
367
343
  const results = mapSearchResults(data);
368
- const response: SearchResponse = { answer: buildAnswer(results), results };
344
+ const response: SearchResponse = { answer: formatSearchResultsAsAnswer(results), results };
369
345
  if (options.includeContent && results.length > 0) {
370
346
  const inlineContent = await fetchInlineContent(results.map((result) => result.url), apiKey, options.signal);
371
347
  if (inlineContent.length > 0) response.inlineContent = inlineContent;
@@ -0,0 +1,11 @@
1
+ interface AnswerResult {
2
+ title: string;
3
+ url: string;
4
+ snippet: string;
5
+ }
6
+
7
+ export function formatSearchResultsAsAnswer(results: readonly AnswerResult[]): string {
8
+ return results.map((result) => result.snippet
9
+ ? `${result.snippet}\nSource: ${result.title} (${result.url})`
10
+ : `Source: ${result.title} (${result.url})`).join("\n\n");
11
+ }
@@ -0,0 +1,4 @@
1
+ export function normalizeSearchResultCount(value: number | undefined): number {
2
+ if (typeof value !== "number" || !Number.isFinite(value)) return 5;
3
+ return Math.max(1, Math.min(Math.floor(value), 20));
4
+ }
@@ -1,8 +1,11 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { activityMonitor } from "./activity.ts";
3
+ import { normalizeDomain } from "./domain-filter-normalization.ts";
3
4
  import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
4
5
  import type { ExtractedContent, ExtractOptions } from "./extract.ts";
5
6
  import type { SearchOptions, SearchResponse } from "./perplexity.ts";
7
+ import { formatSearchResultsAsAnswer } from "./search-answer-formatting.ts";
8
+ import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
6
9
  import { getWebSearchConfigPath } from "./utils.ts";
7
10
 
8
11
  const SEARCH1API_SEARCH_URL = "https://api.search1api.com/search";
@@ -102,26 +105,6 @@ function requestSignal(signal: AbortSignal | undefined, timeoutMs: number): Abor
102
105
  return signal ? AbortSignal.any([signal, timeout]) : timeout;
103
106
  }
104
107
 
105
- function normalizeNumResults(value: number | undefined): number {
106
- if (typeof value !== "number" || !Number.isFinite(value)) return 5;
107
- return Math.max(1, Math.min(Math.floor(value), 20));
108
- }
109
-
110
- function normalizeDomain(value: string): string | null {
111
- let input = value.trim().toLowerCase();
112
- if (!input) return null;
113
- if (input.startsWith("-")) input = input.slice(1).trim();
114
- if (!input) return null;
115
- try {
116
- const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
117
- input = parsed.hostname;
118
- } catch {
119
- input = input.split("/")[0]?.split(":")[0] ?? "";
120
- }
121
- input = input.replace(/^\.+|\.+$/g, "");
122
- return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
123
- }
124
-
125
108
  function mapDomainFilter(domainFilter: string[] | undefined): { includeSites: string[]; excludeSites: string[] } {
126
109
  const includeSites: string[] = [];
127
110
  const excludeSites: string[] = [];
@@ -135,7 +118,7 @@ function mapDomainFilter(domainFilter: string[] | undefined): { includeSites: st
135
118
  }
136
119
 
137
120
  function buildSearchBody(query: string, options: Search1APISearchOptions): Record<string, unknown> {
138
- const numResults = normalizeNumResults(options.numResults);
121
+ const numResults = normalizeSearchResultCount(options.numResults);
139
122
  const { includeSites, excludeSites } = mapDomainFilter(options.domainFilter);
140
123
  return {
141
124
  query,
@@ -215,13 +198,6 @@ function mapInlineContent(results: Search1APISearchResult[] | undefined): Extrac
215
198
  });
216
199
  }
217
200
 
218
- function buildAnswer(results: SearchResponse["results"]): string {
219
- return results.map((result) => {
220
- if (result.snippet) return `${result.snippet}\nSource: ${result.title} (${result.url})`;
221
- return `Source: ${result.title} (${result.url})`;
222
- }).join("\n\n");
223
- }
224
-
225
201
  export async function searchWithSearch1API(
226
202
  query: string,
227
203
  options: Search1APISearchOptions = {},
@@ -238,7 +214,7 @@ export async function searchWithSearch1API(
238
214
  options.signal,
239
215
  );
240
216
  const results = mapSearchResults(data.results);
241
- const response: SearchResponse = { answer: buildAnswer(results), results };
217
+ const response: SearchResponse = { answer: formatSearchResultsAsAnswer(results), results };
242
218
  if (options.includeContent) {
243
219
  const inlineContent = mapInlineContent(data.results);
244
220
  if (inlineContent.length > 0) response.inlineContent = inlineContent;
@@ -1,7 +1,10 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { activityMonitor } from "./activity.ts";
3
+ import { normalizeDomain } from "./domain-filter-normalization.ts";
3
4
  import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
4
5
  import type { SearchOptions, SearchResponse } from "./perplexity.ts";
6
+ import { formatSearchResultsAsAnswer } from "./search-answer-formatting.ts";
7
+ import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
5
8
  import { getWebSearchConfigPath } from "./utils.ts";
6
9
 
7
10
  const SEARCHINFINITY_SEARCH_URL = "https://torchlight.byteintlapi.com/search_api/web_search";
@@ -109,26 +112,6 @@ function requestSignal(signal: AbortSignal | undefined, timeoutMs: number): Abor
109
112
  return signal ? AbortSignal.any([signal, timeout]) : timeout;
110
113
  }
111
114
 
112
- function normalizeNumResults(value: number | undefined): number {
113
- if (typeof value !== "number" || !Number.isFinite(value)) return 5;
114
- return Math.max(1, Math.min(Math.floor(value), 20));
115
- }
116
-
117
- function normalizeDomain(value: string): string | null {
118
- let input = value.trim().toLowerCase();
119
- if (!input) return null;
120
- if (input.startsWith("-")) input = input.slice(1).trim();
121
- if (!input) return null;
122
- try {
123
- const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
124
- input = parsed.hostname;
125
- } catch {
126
- input = input.split("/")[0]?.split(":")[0] ?? "";
127
- }
128
- input = input.replace(/^\.+|\.+$/g, "");
129
- return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
130
- }
131
-
132
115
  function mapRecencyFilter(recency: SearchOptions["recencyFilter"]): string | undefined {
133
116
  if (recency === "day") return "OneDay";
134
117
  if (recency === "week") return "OneWeek";
@@ -153,7 +136,7 @@ function buildSearchBody(query: string, options: SearchinfinitySearchOptions): R
153
136
  const timeRange = mapRecencyFilter(options.recencyFilter);
154
137
  return {
155
138
  Query: query,
156
- Count: normalizeNumResults(options.numResults),
139
+ Count: normalizeSearchResultCount(options.numResults),
157
140
  ...(Object.keys(filter).length > 0 ? { Filter: filter } : {}),
158
141
  ...(timeRange ? { TimeRange: timeRange } : {}),
159
142
  };
@@ -237,13 +220,6 @@ function mapSearchResults(results: SearchinfinityWebResult[] | undefined): Searc
237
220
  });
238
221
  }
239
222
 
240
- function buildAnswer(results: SearchResponse["results"]): string {
241
- return results.map((result) => {
242
- if (result.snippet) return `${result.snippet}\nSource: ${result.title} (${result.url})`;
243
- return `Source: ${result.title} (${result.url})`;
244
- }).join("\n\n");
245
- }
246
-
247
223
  export async function searchWithSearchinfinity(
248
224
  query: string,
249
225
  options: SearchinfinitySearchOptions = {},
@@ -253,7 +229,7 @@ export async function searchWithSearchinfinity(
253
229
  try {
254
230
  const data = await searchinfinityJsonRequest(apiKey, buildSearchBody(query, options), options.signal);
255
231
  const results = mapSearchResults(data.Result?.WebResults);
256
- const response: SearchResponse = { answer: buildAnswer(results), results };
232
+ const response: SearchResponse = { answer: formatSearchResultsAsAnswer(results), results };
257
233
  activityMonitor.logComplete(activityId, 200);
258
234
  return response;
259
235
  } catch (err) {
@@ -1,7 +1,9 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { activityMonitor } from "./activity.ts";
3
+ import { normalizeDomain } from "./domain-filter-normalization.ts";
3
4
  import { fetchRemoteUrl, loadSsrfConfig } from "./ssrf-protection.ts";
4
5
  import type { SearchOptions, SearchResult, SearchResponse } from "./perplexity.ts";
6
+ import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
5
7
  import { getWebSearchConfigPath } from "./utils.ts";
6
8
 
7
9
  const CONFIG_PATH = getWebSearchConfigPath();
@@ -121,26 +123,6 @@ function requireBaseUrl(): string {
121
123
  return baseUrl;
122
124
  }
123
125
 
124
- function normalizeCount(value: number | undefined): number {
125
- if (typeof value !== "number" || !Number.isFinite(value)) return 5;
126
- return Math.max(1, Math.min(Math.floor(value), 20));
127
- }
128
-
129
- function normalizeDomain(value: string): string | null {
130
- let input = value.trim().toLowerCase();
131
- if (!input) return null;
132
- if (input.startsWith("-")) input = input.slice(1).trim();
133
- if (!input) return null;
134
- try {
135
- const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
136
- input = parsed.hostname;
137
- } catch {
138
- input = input.split("/")[0]?.split(":")[0] ?? "";
139
- }
140
- input = input.replace(/^\.+|\.+$/g, "");
141
- return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
142
- }
143
-
144
126
  function normalizeDomainFilters(domainFilter: string[] | undefined): NormalizedDomainFilters {
145
127
  const filters: NormalizedDomainFilters = { allowed: [], blocked: [] };
146
128
  for (const raw of domainFilter ?? []) {
@@ -194,7 +176,7 @@ export function isSearXNGAvailable(): boolean {
194
176
 
195
177
  export async function searchWithSearXNG(query: string, options: SearchOptions = {}): Promise<SearchResponse> {
196
178
  const baseUrl = requireBaseUrl();
197
- const numResults = normalizeCount(options.numResults);
179
+ const numResults = normalizeSearchResultCount(options.numResults);
198
180
  const filters = normalizeDomainFilters(options.domainFilter);
199
181
  const searchQuery = buildSearXNGQuery(query, filters);
200
182
  const url = new URL(`${baseUrl}/search`);
@@ -1,6 +1,9 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { activityMonitor } from "./activity.ts";
3
+ import { normalizeDomain } from "./domain-filter-normalization.ts";
3
4
  import type { SearchOptions, SearchResponse } from "./perplexity.ts";
5
+ import { formatSearchResultsAsAnswer } from "./search-answer-formatting.ts";
6
+ import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
4
7
  import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
5
8
  import { getWebSearchConfigPath } from "./utils.ts";
6
9
 
@@ -69,31 +72,11 @@ async function requireApiKey(signal?: AbortSignal): Promise<string> {
69
72
  return apiKey;
70
73
  }
71
74
 
72
- function normalizeCount(value: number | undefined): number {
73
- if (typeof value !== "number" || !Number.isFinite(value)) return 5;
74
- return Math.max(1, Math.min(Math.floor(value), 20));
75
- }
76
-
77
75
  interface DomainFilters {
78
76
  include: string[];
79
77
  exclude: string[];
80
78
  }
81
79
 
82
- function normalizeDomain(value: string): string | null {
83
- let input = value.trim().toLowerCase();
84
- if (!input) return null;
85
- if (input.startsWith("-")) input = input.slice(1).trim();
86
- if (!input) return null;
87
- try {
88
- const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
89
- input = parsed.hostname;
90
- } catch {
91
- input = input.split("/")[0]?.split(":")[0] ?? "";
92
- }
93
- input = input.replace(/^\.+|\.+$/g, "");
94
- return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
95
- }
96
-
97
80
  function parseDomainFilter(domainFilter: string[] | undefined): DomainFilters {
98
81
  const filters: DomainFilters = { include: [], exclude: [] };
99
82
  for (const raw of domainFilter ?? []) {
@@ -142,19 +125,13 @@ function parseResponse(value: unknown): SerpApiOrganicResult[] {
142
125
  return envelope.organic_results as SerpApiOrganicResult[];
143
126
  }
144
127
 
145
- function buildAnswer(results: SearchResponse["results"]): string {
146
- return results.map((result) => result.snippet
147
- ? `${result.snippet}\nSource: ${result.title} (${result.url})`
148
- : `Source: ${result.title} (${result.url})`).join("\n\n");
149
- }
150
-
151
128
  export function isSerpApiAvailable(): boolean {
152
129
  return hasCredentialSource({ provider: "SerpApi", configuredValue: loadConfig().serpapiApiKey, environmentValue: process.env.SERPAPI_KEY });
153
130
  }
154
131
 
155
132
  export async function searchWithSerpApi(query: string, options: SearchOptions = {}): Promise<SearchResponse> {
156
133
  const apiKey = await requireApiKey(options.signal);
157
- const numResults = normalizeCount(options.numResults);
134
+ const numResults = normalizeSearchResultCount(options.numResults);
158
135
  const filters = parseDomainFilter(options.domainFilter);
159
136
  const requestCount = options.domainFilter?.length ? Math.min(20, numResults + 5) : numResults;
160
137
  const url = new URL(SERPAPI_SEARCH_URL);
@@ -216,5 +193,5 @@ export async function searchWithSerpApi(query: string, options: SearchOptions =
216
193
  });
217
194
  if (results.length >= numResults) break;
218
195
  }
219
- return { answer: buildAnswer(results), results };
196
+ return { answer: formatSearchResultsAsAnswer(results), results };
220
197
  }
@@ -1,6 +1,8 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { activityMonitor } from "./activity.ts";
3
+ import { normalizeDomain } from "./domain-filter-normalization.ts";
3
4
  import type { SearchOptions, SearchResponse } from "./perplexity.ts";
5
+ import { formatSearchResultsAsAnswer } from "./search-answer-formatting.ts";
4
6
  import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
5
7
  import { getWebSearchConfigPath } from "./utils.ts";
6
8
 
@@ -85,21 +87,6 @@ function normalizeCount(value: number | undefined): number {
85
87
  return Math.max(1, Math.min(Math.floor(value), 20));
86
88
  }
87
89
 
88
- function normalizeDomain(value: string): string | null {
89
- let input = value.trim().toLowerCase();
90
- if (!input) return null;
91
- if (input.startsWith("-")) input = input.slice(1).trim();
92
- if (!input) return null;
93
- try {
94
- const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
95
- input = parsed.hostname;
96
- } catch {
97
- input = input.split("/")[0]?.split(":")[0] ?? "";
98
- }
99
- input = input.replace(/^\.+|\.+$/g, "");
100
- return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
101
- }
102
-
103
90
  interface DomainFilters {
104
91
  include: string[];
105
92
  exclude: string[];
@@ -162,12 +149,6 @@ function parseResponse(value: unknown): SerpBaseResponse {
162
149
  return { ...envelope, organic_results: organic };
163
150
  }
164
151
 
165
- function buildAnswer(results: SearchResponse["results"]): string {
166
- return results.map((result) => result.snippet
167
- ? `${result.snippet}\nSource: ${result.title} (${result.url})`
168
- : `Source: ${result.title} (${result.url})`).join("\n\n");
169
- }
170
-
171
152
  export function isSerpBaseAvailable(): boolean {
172
153
  return hasCredentialSource({ provider: "SerpBase", configuredValue: loadConfig().serpbaseApiKey, environmentValue: process.env.SERPBASE_API_KEY });
173
154
  }
@@ -224,5 +205,5 @@ export async function searchWithSerpBase(query: string, options: SearchOptions =
224
205
  });
225
206
  if (results.length >= numResults) break;
226
207
  }
227
- return { answer: buildAnswer(results), results };
208
+ return { answer: formatSearchResultsAsAnswer(results), results };
228
209
  }
@@ -1,7 +1,9 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { activityMonitor } from "./activity.ts";
3
+ import { normalizeDomain } from "./domain-filter-normalization.ts";
3
4
  import type { ExtractedContent } from "./extract.ts";
4
5
  import type { SearchOptions, SearchResponse } from "./perplexity.ts";
6
+ import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
5
7
  import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
6
8
  import { getWebSearchConfigPath } from "./utils.ts";
7
9
 
@@ -93,26 +95,6 @@ function resolveModel(): SerpdiveModel {
93
95
  return (MODELS as readonly string[]).includes(value) ? value as SerpdiveModel : DEFAULT_MODEL;
94
96
  }
95
97
 
96
- function normalizeCount(value: number | undefined): number {
97
- if (typeof value !== "number" || !Number.isFinite(value)) return 5;
98
- return Math.max(1, Math.min(Math.floor(value), 20));
99
- }
100
-
101
- function normalizeDomain(value: string): string | null {
102
- let input = value.trim().toLowerCase();
103
- if (!input) return null;
104
- if (input.startsWith("-")) input = input.slice(1).trim();
105
- if (!input) return null;
106
- try {
107
- const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
108
- input = parsed.hostname;
109
- } catch {
110
- input = input.split("/")[0]?.split(":")[0] ?? "";
111
- }
112
- input = input.replace(/^\.+|\.+$/g, "");
113
- return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
114
- }
115
-
116
98
  interface DomainFilters {
117
99
  include: string[];
118
100
  exclude: string[];
@@ -232,7 +214,7 @@ export function isSerpdiveAvailable(): boolean {
232
214
 
233
215
  export async function searchWithSerpdive(query: string, options: SerpdiveSearchOptions = {}): Promise<SearchResponse> {
234
216
  const apiKey = await requireApiKey(options.signal);
235
- const numResults = normalizeCount(options.numResults);
217
+ const numResults = normalizeSearchResultCount(options.numResults);
236
218
  const filters = parseDomainFilter(options.domainFilter);
237
219
  const model = resolveModel();
238
220
  const body: Record<string, unknown> = {
@@ -1,6 +1,9 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { activityMonitor } from "./activity.ts";
3
+ import { normalizeDomain } from "./domain-filter-normalization.ts";
3
4
  import type { SearchOptions, SearchResponse } from "./perplexity.ts";
5
+ import { formatSearchResultsAsAnswer } from "./search-answer-formatting.ts";
6
+ import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
4
7
  import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
5
8
  import { getWebSearchConfigPath } from "./utils.ts";
6
9
 
@@ -69,31 +72,11 @@ async function requireApiKey(signal?: AbortSignal): Promise<string> {
69
72
  return apiKey;
70
73
  }
71
74
 
72
- function normalizeCount(value: number | undefined): number {
73
- if (typeof value !== "number" || !Number.isFinite(value)) return 5;
74
- return Math.max(1, Math.min(Math.floor(value), 20));
75
- }
76
-
77
75
  interface DomainFilters {
78
76
  include: string[];
79
77
  exclude: string[];
80
78
  }
81
79
 
82
- function normalizeDomain(value: string): string | null {
83
- let input = value.trim().toLowerCase();
84
- if (!input) return null;
85
- if (input.startsWith("-")) input = input.slice(1).trim();
86
- if (!input) return null;
87
- try {
88
- const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
89
- input = parsed.hostname;
90
- } catch {
91
- input = input.split("/")[0]?.split(":")[0] ?? "";
92
- }
93
- input = input.replace(/^\.+|\.+$/g, "");
94
- return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
95
- }
96
-
97
80
  function parseDomainFilter(domainFilter: string[] | undefined): DomainFilters {
98
81
  const filters: DomainFilters = { include: [], exclude: [] };
99
82
  for (const raw of domainFilter ?? []) {
@@ -141,19 +124,13 @@ function parseResponse(value: unknown): SerperResult[] {
141
124
  return envelope.organic as SerperResult[];
142
125
  }
143
126
 
144
- function buildAnswer(results: SearchResponse["results"]): string {
145
- return results.map((result) => result.snippet
146
- ? `${result.snippet}\nSource: ${result.title} (${result.url})`
147
- : `Source: ${result.title} (${result.url})`).join("\n\n");
148
- }
149
-
150
127
  export function isSerperAvailable(): boolean {
151
128
  return hasCredentialSource({ provider: "Serper", configuredValue: loadConfig().serperApiKey, environmentValue: process.env.SERPER_API_KEY });
152
129
  }
153
130
 
154
131
  export async function searchWithSerper(query: string, options: SearchOptions = {}): Promise<SearchResponse> {
155
132
  const apiKey = await requireApiKey(options.signal);
156
- const numResults = normalizeCount(options.numResults);
133
+ const numResults = normalizeSearchResultCount(options.numResults);
157
134
  const filters = parseDomainFilter(options.domainFilter);
158
135
  const requestCount = options.domainFilter?.length ? Math.min(20, numResults + 5) : numResults;
159
136
  const activityId = activityMonitor.logStart({ type: "api", query });
@@ -207,5 +184,5 @@ export async function searchWithSerper(query: string, options: SearchOptions = {
207
184
  });
208
185
  if (results.length >= numResults) break;
209
186
  }
210
- return { answer: buildAnswer(results), results };
187
+ return { answer: formatSearchResultsAsAnswer(results), results };
211
188
  }