@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
@@ -0,0 +1,197 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { activityMonitor } from "./activity.ts";
3
+ import { normalizeDomain } from "./domain-filter-normalization.ts";
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";
7
+ import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
8
+ import { fetchWithCredentialRedirects, getWebSearchConfigPath } from "./utils.ts";
9
+
10
+ const SERPLY_SEARCH_URL = "https://api.serply.io/v1/search";
11
+ const CONFIG_PATH = getWebSearchConfigPath();
12
+ const SEARCH_TIMEOUT_MS = 60_000;
13
+ const RECENCY_TBS: Record<NonNullable<SearchOptions["recencyFilter"]>, string> = {
14
+ day: "qdr:d",
15
+ week: "qdr:w",
16
+ month: "qdr:m",
17
+ year: "qdr:y",
18
+ };
19
+
20
+ interface WebSearchConfig {
21
+ serplyApiKey?: unknown;
22
+ }
23
+
24
+ interface SerplyOrganicResult {
25
+ title?: unknown;
26
+ link?: unknown;
27
+ description?: unknown;
28
+ }
29
+
30
+ let cachedConfig: WebSearchConfig | null = null;
31
+
32
+ function loadConfig(): WebSearchConfig {
33
+ if (cachedConfig) return cachedConfig;
34
+ if (!existsSync(CONFIG_PATH)) {
35
+ cachedConfig = {};
36
+ return cachedConfig;
37
+ }
38
+ const raw = readFileSync(CONFIG_PATH, "utf-8");
39
+ let parsed: unknown;
40
+ try {
41
+ parsed = JSON.parse(raw);
42
+ } catch (err) {
43
+ const message = err instanceof Error ? err.message : String(err);
44
+ throw new Error(`Failed to parse ${CONFIG_PATH}: ${message}`);
45
+ }
46
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
47
+ throw new Error(`Invalid config in ${CONFIG_PATH}: expected a JSON object`);
48
+ }
49
+ cachedConfig = parsed as WebSearchConfig;
50
+ return cachedConfig;
51
+ }
52
+
53
+ async function getApiKey(signal?: AbortSignal): Promise<string | null> {
54
+ return resolveCredential({
55
+ provider: "Serply",
56
+ configuredValue: loadConfig().serplyApiKey,
57
+ environmentValue: process.env.SERPLY_API_KEY,
58
+ signal,
59
+ });
60
+ }
61
+
62
+ async function requireApiKey(signal?: AbortSignal): Promise<string> {
63
+ const apiKey = await getApiKey(signal);
64
+ if (!apiKey) {
65
+ throw new Error(
66
+ "Serply API key not found. Either:\n" +
67
+ ` 1. Create ${CONFIG_PATH} with { "serplyApiKey": "your-key" }\n` +
68
+ " 2. Set SERPLY_API_KEY environment variable\n" +
69
+ "Get a key at https://serply.io",
70
+ );
71
+ }
72
+ return apiKey;
73
+ }
74
+
75
+ interface DomainFilters {
76
+ include: string[];
77
+ exclude: string[];
78
+ }
79
+
80
+ function parseDomainFilter(domainFilter: string[] | undefined): DomainFilters {
81
+ const filters: DomainFilters = { include: [], exclude: [] };
82
+ for (const raw of domainFilter ?? []) {
83
+ const domain = normalizeDomain(raw);
84
+ if (!domain) continue;
85
+ const target = raw.trim().startsWith("-") ? filters.exclude : filters.include;
86
+ if (!target.includes(domain)) target.push(domain);
87
+ }
88
+ return filters;
89
+ }
90
+
91
+ function passesDomainFilters(url: URL, filters: DomainFilters): boolean {
92
+ if (filters.include.length === 0 && filters.exclude.length === 0) return true;
93
+ const hostname = url.hostname.toLowerCase();
94
+ const matches = (domain: string) => hostname === domain || hostname.endsWith(`.${domain}`);
95
+ if (filters.exclude.some(matches)) return false;
96
+ return filters.include.length === 0 || filters.include.some(matches);
97
+ }
98
+
99
+ function buildQuery(query: string, filters: DomainFilters): string {
100
+ const parts = [query];
101
+ if (filters.include.length === 1) parts.push(`site:${filters.include[0]}`);
102
+ if (filters.include.length > 1) parts.push(`(${filters.include.map(domain => `site:${domain}`).join(" OR ")})`);
103
+ for (const domain of filters.exclude) parts.push(`-site:${domain}`);
104
+ return parts.join(" ");
105
+ }
106
+
107
+ function errorMessage(err: unknown): string {
108
+ return err instanceof Error ? err.message : String(err);
109
+ }
110
+
111
+ function invalidResponse(message: string): Error {
112
+ return new Error(`Serply returned invalid response: ${message}`);
113
+ }
114
+
115
+ function parseResponse(value: unknown): SerplyOrganicResult[] {
116
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw invalidResponse("expected an object envelope");
117
+ const envelope = value as Record<string, unknown>;
118
+ if (typeof envelope.detail === "string" && envelope.detail.trim()) throw invalidResponse(envelope.detail.trim());
119
+ if (!Array.isArray(envelope.results)) throw invalidResponse("expected results array");
120
+ return envelope.results as SerplyOrganicResult[];
121
+ }
122
+
123
+ export function isSerplyAvailable(): boolean {
124
+ return hasCredentialSource({ provider: "Serply", configuredValue: loadConfig().serplyApiKey, environmentValue: process.env.SERPLY_API_KEY });
125
+ }
126
+
127
+ export async function searchWithSerply(query: string, options: SearchOptions = {}): Promise<SearchResponse> {
128
+ const apiKey = await requireApiKey(options.signal);
129
+ const numResults = normalizeSearchResultCount(options.numResults);
130
+ const filters = parseDomainFilter(options.domainFilter);
131
+ const requestCount = options.domainFilter?.length ? Math.min(20, numResults + 5) : numResults;
132
+ const url = new URL(SERPLY_SEARCH_URL);
133
+ url.searchParams.set("q", buildQuery(query, filters));
134
+ url.searchParams.set("num", String(requestCount));
135
+ if (options.recencyFilter) url.searchParams.set("tbs", RECENCY_TBS[options.recencyFilter]);
136
+ const activityId = activityMonitor.logStart({ type: "api", query });
137
+ const timeoutSignal = AbortSignal.timeout(SEARCH_TIMEOUT_MS);
138
+ let response: Response;
139
+ let entries: SerplyOrganicResult[];
140
+ try {
141
+ response = await fetchWithCredentialRedirects(String(url), {
142
+ headers: { Accept: "application/json", "X-Api-Key": apiKey },
143
+ signal: options.signal ? AbortSignal.any([timeoutSignal, options.signal]) : timeoutSignal,
144
+ }, ["X-Api-Key"]);
145
+ if (!response.ok) {
146
+ const errorText = await response.text();
147
+ throw new Error(`Serply error ${response.status}: ${redactCredential(errorText, apiKey).slice(0, 300)}`);
148
+ }
149
+ let rawData: unknown;
150
+ try {
151
+ rawData = await response.json();
152
+ } catch (err) {
153
+ if (err instanceof Error && err.name === "TimeoutError") throw err;
154
+ throw new Error(`Serply returned invalid JSON: ${errorMessage(err)}`);
155
+ }
156
+ entries = parseResponse(rawData);
157
+ } catch (err) {
158
+ if (options.signal?.aborted) {
159
+ activityMonitor.logComplete(activityId, 0);
160
+ throw new Error("Aborted");
161
+ }
162
+ const message = errorMessage(err);
163
+ const providerTimeout = timeoutSignal.aborted || (err instanceof Error && err.name === "TimeoutError");
164
+ const outgoing = providerTimeout
165
+ ? new Error(`Serply request timed out after ${Math.round(SEARCH_TIMEOUT_MS / 1000)}s`)
166
+ : (() => {
167
+ const redactedMessage = redactCredential(message, apiKey);
168
+ if (redactedMessage === message && err instanceof Error) return err;
169
+ const redactedError = new Error(redactedMessage);
170
+ if (err instanceof Error) redactedError.name = err.name;
171
+ return redactedError;
172
+ })();
173
+ activityMonitor.logError(activityId, redactCredential(errorMessage(outgoing), apiKey));
174
+ throw outgoing;
175
+ }
176
+ activityMonitor.logComplete(activityId, response.status);
177
+ const results: SearchResponse["results"] = [];
178
+ for (const entry of entries) {
179
+ if (!entry || typeof entry !== "object") continue;
180
+ if (typeof entry.link !== "string" || !entry.link) continue;
181
+ let resultUrl: URL;
182
+ try {
183
+ resultUrl = new URL(entry.link);
184
+ } catch {
185
+ continue;
186
+ }
187
+ if (resultUrl.protocol !== "http:" && resultUrl.protocol !== "https:") continue;
188
+ if (!passesDomainFilters(resultUrl, filters)) continue;
189
+ results.push({
190
+ title: typeof entry.title === "string" && entry.title.trim() ? entry.title.trim() : `Source ${results.length + 1}`,
191
+ url: resultUrl.href,
192
+ snippet: typeof entry.description === "string" ? entry.description : "",
193
+ });
194
+ if (results.length >= numResults) break;
195
+ }
196
+ return { answer: formatSearchResultsAsAnswer(results), results };
197
+ }
@@ -1,4 +1,4 @@
1
- // Structured source checking and machine-readable research artifacts.
1
+ // Structured source evidence and machine-readable research artifacts.
2
2
  import { createHash } from "node:crypto";
3
3
  import { generateId, getResult, storeResult } from "./storage.ts";
4
4
  import type { SearchResult } from "./perplexity.ts";
@@ -159,54 +159,18 @@ export function buildPassages(sources: ResearchSource[], fetched: ExtractedConte
159
159
  return passages;
160
160
  }
161
161
 
162
- const CONTRADICTION_MARKERS = ["not true", "false", "incorrect", "debunked", "retracted", "no longer", "never", "denied", "contrary", "misleading"];
163
- const SUPPORT_MARKERS = ["yes", "true", "correct", "confirmed", "according to", "shows that", "demonstrates", "reported", "verified", "established"];
164
-
165
- function containsPhrase(value: string, phrase: string): boolean {
166
- const escaped = phrase.trim().toLowerCase().split(/\s+/).map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+");
167
- return new RegExp(`(?:^|[^a-z0-9])${escaped}(?=$|[^a-z0-9])`, "i").test(value);
168
- }
169
-
170
- function markerIsNegated(value: string, marker: string): boolean {
171
- const escaped = marker.trim().toLowerCase().split(/\s+/).map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+");
172
- const markerPattern = new RegExp(`(?:^|[^a-z0-9])${escaped}(?=$|[^a-z0-9])`, "i");
173
- const match = markerPattern.exec(value);
174
- if (!match || match.index === undefined) return false;
175
- const matchedMarker = match[0].replace(/^[^a-z0-9]+/i, "");
176
- const beforeMarker = value.slice(0, match.index + match[0].length - matchedMarker.length);
177
- return /(?:^|[^a-z0-9])(?:not|no|never|without)\s+$/i.test(beforeMarker);
178
- }
179
-
180
- function hasPolarityMarker(value: string, markers: string[], allowNegated = false): boolean {
181
- return markers.some((marker) => containsPhrase(value, marker) && (allowNegated || !markerIsNegated(value, marker)));
182
- }
183
-
184
162
  export function assessClaim(claim: string, passages: ResearchPassage[]): ClaimAssessment {
185
- const terms = tokenize(claim);
186
- if (terms.length === 0 || passages.length === 0) {
187
- return { claim, status: "missing-evidence", supporting_passages: [], contradicting_passages: [], rationale: "No passages available that discuss the claim's terms.", confidence: 0.2 };
163
+ if (passages.length === 0) {
164
+ return { claim, status: "missing-evidence", supporting_passages: [], contradicting_passages: [], rationale: "No passages were retrieved for the claim.", confidence: 0.2 };
188
165
  }
189
- const supporting: string[] = [];
190
- const contradicting: string[] = [];
191
- for (const passage of passages) {
192
- const lower = passage.text.toLowerCase();
193
- const overlap = terms.filter((term) => containsPhrase(lower, term)).length;
194
- if (overlap < Math.max(2, Math.ceil(terms.length / 4))) continue;
195
- const contra = hasPolarityMarker(lower, CONTRADICTION_MARKERS);
196
- const support = hasPolarityMarker(lower, SUPPORT_MARKERS);
197
- if (contra && !support) contradicting.push(passage.passage_id);
198
- else if (support && !contra) supporting.push(passage.passage_id);
199
- }
200
- if (contradicting.length > 0 && supporting.length === 0) {
201
- return { claim, status: "contradicted", supporting_passages: [], contradicting_passages: contradicting, rationale: `${contradicting.length} passage(s) contradict the claim; none support it.`, confidence: Math.min(0.85, 0.5 + contradicting.length * 0.1) };
202
- }
203
- if (supporting.length > 0 && contradicting.length === 0) {
204
- return { claim, status: "supported", supporting_passages: supporting, contradicting_passages: [], rationale: `${supporting.length} passage(s) support the claim; none contradict it.`, confidence: Math.min(0.85, 0.5 + supporting.length * 0.1) };
205
- }
206
- if (supporting.length > 0 || contradicting.length > 0) {
207
- return { claim, status: "unclear", supporting_passages: supporting, contradicting_passages: contradicting, rationale: `${supporting.length} supporting and ${contradicting.length} contradicting passage(s); evidence is mixed.`, confidence: 0.4 };
208
- }
209
- return { claim, status: "unclear", supporting_passages: [], contradicting_passages: [], rationale: "Passages mention the claim's terms but contain no clear support or contradiction markers.", confidence: 0.3 };
166
+ return {
167
+ claim,
168
+ status: "unclear",
169
+ supporting_passages: [],
170
+ contradicting_passages: [],
171
+ rationale: "Passages were retrieved, but automated semantic support or contradiction assessment is unavailable; review the cited passages manually.",
172
+ confidence: 0.3,
173
+ };
210
174
  }
211
175
 
212
176
  interface RankedSearchResult extends SearchResult {
@@ -1,6 +1,7 @@
1
1
  import type { ModelThinkingLevel, ThinkingLevel } from "@earendil-works/pi-ai";
2
2
  import type { complete, Api, Message, Model } from "@earendil-works/pi-ai/compat";
3
3
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import { openCodeSessionHeaders } from "./opencode-session-headers.ts";
4
5
  import { findModelWithProviderRouting, loadEnabledModelPatterns, modelMatchesEnabledPatterns, splitThinkingSuffix, type SummaryThinkingLevel } from "./summary-model-scope.ts";
5
6
  import type { QueryResultData } from "./storage.ts";
6
7
 
@@ -29,7 +30,7 @@ export interface SummaryMeta {
29
30
  edited?: boolean;
30
31
  }
31
32
 
32
- export type SummaryGenerationContext = Pick<ExtensionContext, "model" | "modelRegistry" | "cwd" | "isProjectTrusted">;
33
+ export type SummaryGenerationContext = Pick<ExtensionContext, "model" | "modelRegistry" | "sessionManager" | "cwd" | "isProjectTrusted">;
33
34
 
34
35
  function estimateTokens(text: string): number {
35
36
  const trimmed = text.trim();
@@ -368,6 +369,7 @@ export async function generateSummaryDraft(
368
369
  for (const { model, apiKey, headers, thinkingLevel } of resolved.candidates) {
369
370
  const startedAt = Date.now();
370
371
  try {
372
+ const sessionHeaders = openCodeSessionHeaders(model, ctx.sessionManager);
371
373
  const userMessage: Message = {
372
374
  role: "user",
373
375
  content: [{ type: "text", text: prompt }],
@@ -378,14 +380,16 @@ export async function generateSummaryDraft(
378
380
  ? requestedThinkingLevel as ThinkingLevel
379
381
  : undefined;
380
382
  const completionOptions = {
381
- ...(usesRegistryComplete ? {} : { apiKey, headers }),
383
+ ...(usesRegistryComplete
384
+ ? (sessionHeaders ? { transformHeaders: (requestHeaders: Record<string, string>) => ({ ...requestHeaders, ...sessionHeaders }) } : {})
385
+ : { apiKey, headers: sessionHeaders ? { ...headers, ...sessionHeaders } : headers }),
382
386
  signal: completionSignal,
383
387
  ...(requestedThinkingLevel ? { reasoning: requestedThinkingLevel } : {}),
384
388
  ...(enabledThinkingLevel ? { reasoningEffort: enabledThinkingLevel } : {}),
385
389
  };
386
390
  checkSummaryDeadline();
387
391
  const completion = thinkingLevel !== undefined && !customCompleteFn && !usesRegistryComplete
388
- ? piAiCompat!.completeSimple(model, { messages: [userMessage] }, { apiKey, headers, signal: completionSignal, ...(enabledThinkingLevel ? { reasoning: enabledThinkingLevel } : {}) })
392
+ ? piAiCompat!.completeSimple(model, { messages: [userMessage] }, { apiKey, headers: sessionHeaders ? { ...headers, ...sessionHeaders } : headers, signal: completionSignal, ...(enabledThinkingLevel ? { reasoning: enabledThinkingLevel } : {}) })
389
393
  : completeFn(model, { messages: [userMessage] }, completionOptions);
390
394
 
391
395
  const response = await raceSummaryOperation(Promise.resolve(completion));
@@ -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 { fetchWithCredentialRedirects, getWebSearchConfigPath, resolveApiBaseUrl } from "./utils.ts";
7
9
 
@@ -81,26 +83,6 @@ async function requireApiKey(signal?: AbortSignal): Promise<string> {
81
83
  return apiKey;
82
84
  }
83
85
 
84
- function normalizeCount(value: number | undefined): number {
85
- if (typeof value !== "number" || !Number.isFinite(value)) return 5;
86
- return Math.max(1, Math.min(Math.floor(value), 20));
87
- }
88
-
89
- function normalizeDomain(value: string): string | null {
90
- let input = value.trim().toLowerCase();
91
- if (!input) return null;
92
- if (input.startsWith("-")) input = input.slice(1).trim();
93
- if (!input) return null;
94
- try {
95
- const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
96
- input = parsed.hostname;
97
- } catch {
98
- input = input.split("/")[0]?.split(":")[0] ?? "";
99
- }
100
- input = input.replace(/^\.+|\.+$/g, "");
101
- return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
102
- }
103
-
104
86
  function mapDomainFilter(domainFilter: string[] | undefined): { include_domains?: string[]; exclude_domains?: string[] } {
105
87
  if (!domainFilter?.length) return {};
106
88
  const include_domains: string[] = [];
@@ -165,7 +147,7 @@ export function isTavilyAvailable(): boolean {
165
147
  export async function searchWithTavily(query: string, options: TavilySearchOptions = {}): Promise<SearchResponse> {
166
148
  const apiUrl = getApiUrl();
167
149
  const apiKey = await requireApiKey(options.signal);
168
- const numResults = normalizeCount(options.numResults);
150
+ const numResults = normalizeSearchResultCount(options.numResults);
169
151
  const body: Record<string, unknown> = {
170
152
  query,
171
153
  search_depth: "basic",
@@ -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 type { ExtractedContent, ExtractOptions } from "./extract.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 { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
6
9
  import { getWebSearchConfigPath } from "./utils.ts";
7
10
 
@@ -115,21 +118,6 @@ function requestSignal(signal: AbortSignal | undefined, timeoutMs: number): Abor
115
118
  return signal ? AbortSignal.any([signal, timeout]) : timeout;
116
119
  }
117
120
 
118
- function normalizeDomain(value: string): string | null {
119
- let input = value.trim().toLowerCase();
120
- if (!input) return null;
121
- if (input.startsWith("-")) input = input.slice(1).trim();
122
- if (!input) return null;
123
- try {
124
- const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
125
- input = parsed.hostname;
126
- } catch {
127
- input = input.split("/")[0]?.split(":")[0] ?? "";
128
- }
129
- input = input.replace(/^\.+|\.+$/g, "");
130
- return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
131
- }
132
-
133
121
  function mapDomainFilter(domainFilter: string[] | undefined): { includeDomains: string[]; excludeDomains: string[] } {
134
122
  const includeDomains: string[] = [];
135
123
  const excludeDomains: string[] = [];
@@ -153,11 +141,6 @@ function recencyMinutes(filter: SearchOptions["recencyFilter"]): number | undefi
153
141
  return minutes[filter];
154
142
  }
155
143
 
156
- function normalizeNumResults(value: number | undefined): number {
157
- if (typeof value !== "number" || !Number.isFinite(value)) return 5;
158
- return Math.max(1, Math.min(Math.floor(value), 20));
159
- }
160
-
161
144
  function buildSearchUrl(query: string, options: SearchOptions, page: number): string {
162
145
  const params = new URLSearchParams({ query });
163
146
  const { includeDomains, excludeDomains } = mapDomainFilter(options.domainFilter);
@@ -233,13 +216,6 @@ function deduplicateResults(results: SearchResponse["results"], limit: number):
233
216
  return unique;
234
217
  }
235
218
 
236
- function buildAnswer(results: SearchResponse["results"]): string {
237
- return results.map((result) => {
238
- if (result.snippet) return `${result.snippet}\nSource: ${result.title} (${result.url})`;
239
- return `Source: ${result.title} (${result.url})`;
240
- }).join("\n\n");
241
- }
242
-
243
219
  function fetchPerUrlTimeout(value: number | undefined): number {
244
220
  if (typeof value !== "number" || !Number.isFinite(value)) return MAX_FETCH_PER_URL_TIMEOUT_MS;
245
221
  return Math.max(1, Math.min(Math.floor(value), MAX_FETCH_PER_URL_TIMEOUT_MS));
@@ -320,7 +296,7 @@ async function fetchInlineContent(
320
296
 
321
297
  export async function searchWithTinyFish(query: string, options: TinyFishSearchOptions = {}): Promise<SearchResponse> {
322
298
  const apiKey = await getApiKey(options.signal);
323
- const numResults = normalizeNumResults(options.numResults);
299
+ const numResults = normalizeSearchResultCount(options.numResults);
324
300
  const activityId = activityMonitor.logStart({ type: "api", query });
325
301
  try {
326
302
  const combined: SearchResponse["results"] = [];
@@ -340,7 +316,7 @@ export async function searchWithTinyFish(query: string, options: TinyFishSearchO
340
316
  }
341
317
 
342
318
  const results = deduplicateResults(combined, numResults);
343
- const response: SearchResponse = { answer: buildAnswer(results), results };
319
+ const response: SearchResponse = { answer: formatSearchResultsAsAnswer(results), results };
344
320
  if (options.includeContent && results.length > 0) {
345
321
  const inlineContent = await fetchInlineContent(results.map(result => result.url), apiKey, options.signal);
346
322
  if (inlineContent.length > 0) response.inlineContent = inlineContent;
@@ -22,7 +22,13 @@ export function getWebSearchConfigDir(): string {
22
22
  if (existsSync(join(legacyDir, "web-search.json"))) return cachedWebSearchConfigDir = legacyDir;
23
23
  return cachedWebSearchConfigDir = xdgDir;
24
24
  }
25
- return cachedWebSearchConfigDir = join(homedir(), ".pi", "agent");
25
+ const agentDir = join(homedir(), ".pi", "agent");
26
+ if (existsSync(join(agentDir, "web-search.json"))) return cachedWebSearchConfigDir = agentDir;
27
+
28
+ const legacyDir = join(homedir(), ".pi");
29
+ if (existsSync(join(legacyDir, "web-search.json"))) return cachedWebSearchConfigDir = legacyDir;
30
+
31
+ return cachedWebSearchConfigDir = agentDir;
26
32
  }
27
33
 
28
34
  export function getWebSearchConfigPath(): string {
@@ -256,9 +262,11 @@ export function runWithProxy<T>(proxy: string | undefined, fn: () => T): T {
256
262
  if (proxy === undefined) {
257
263
  const configured = loadConfiguredProxy();
258
264
  if (configured === null) return fn();
265
+ installGlobalProxyFetch();
259
266
  return proxyStorage.run(configured, fn);
260
267
  }
261
268
  const normalized = normalizeProxyUrl(proxy, "proxy");
269
+ if (normalized !== null) installGlobalProxyFetch();
262
270
  return proxyStorage.run(normalized, fn);
263
271
  }
264
272
 
@@ -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 type { ExtractedContent } from "./extract.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 { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
6
9
  import { getWebSearchConfigPath } from "./utils.ts";
7
10
 
@@ -67,26 +70,6 @@ async function requireApiKey(signal?: AbortSignal): Promise<string> {
67
70
  return apiKey;
68
71
  }
69
72
 
70
- function normalizeCount(value: number | undefined): number {
71
- if (typeof value !== "number" || !Number.isFinite(value)) return 5;
72
- return Math.max(1, Math.min(Math.floor(value), 20));
73
- }
74
-
75
- function normalizeDomain(value: string): string | null {
76
- let input = value.trim().toLowerCase();
77
- if (!input) return null;
78
- if (input.startsWith("-")) input = input.slice(1).trim();
79
- if (!input) return null;
80
- try {
81
- const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
82
- input = parsed.hostname;
83
- } catch {
84
- input = input.split("/")[0]?.split(":")[0] ?? "";
85
- }
86
- input = input.replace(/^\.+|\.+$/g, "");
87
- return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
88
- }
89
-
90
73
  function mapDomainFilter(domainFilter: string[] | undefined): { included_sources?: string[]; excluded_sources?: string[] } {
91
74
  if (!domainFilter?.length) return {};
92
75
  const included_sources: string[] = [];
@@ -129,19 +112,13 @@ function parseResponse(value: unknown): ValyuResult[] {
129
112
  return envelope.results as ValyuResult[];
130
113
  }
131
114
 
132
- function buildAnswer(results: SearchResponse["results"]): string {
133
- return results.map((result) => result.snippet
134
- ? `${result.snippet}\nSource: ${result.title} (${result.url})`
135
- : `Source: ${result.title} (${result.url})`).join("\n\n");
136
- }
137
-
138
115
  export function isValyuAvailable(): boolean {
139
116
  return hasCredentialSource({ provider: "Valyu", configuredValue: loadConfig().valyuApiKey, environmentValue: process.env.VALYU_API_KEY });
140
117
  }
141
118
 
142
119
  export async function searchWithValyu(query: string, options: SearchOptions & { includeContent?: boolean } = {}): Promise<SearchResponse> {
143
120
  const apiKey = await requireApiKey(options.signal);
144
- const numResults = normalizeCount(options.numResults);
121
+ const numResults = normalizeSearchResultCount(options.numResults);
145
122
  const startDate = recencyToStartDate(options.recencyFilter);
146
123
  const activityId = activityMonitor.logStart({ type: "api", query });
147
124
  let response: Response;
@@ -195,5 +172,5 @@ export async function searchWithValyu(query: string, options: SearchOptions & {
195
172
  if (options.includeContent && content) inlineContent.push({ url, title, content, error: null });
196
173
  if (results.length >= numResults) break;
197
174
  }
198
- return { answer: buildAnswer(results), results, ...(inlineContent.length > 0 ? { inlineContent } : {}) };
175
+ return { answer: formatSearchResultsAsAnswer(results), results, ...(inlineContent.length > 0 ? { inlineContent } : {}) };
199
176
  }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import { activityMonitor } from "./activity.ts";
4
+ import { normalizeDomain } from "./domain-filter-normalization.ts";
4
5
  import type { SearchOptions, SearchResponse, SearchResult } from "./perplexity.ts";
5
6
  import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
6
7
  import { getWebSearchConfigPath } from "./utils.ts";
@@ -174,21 +175,6 @@ export async function isXaiSearchAvailable(ctx?: ExtensionContext): Promise<bool
174
175
  });
175
176
  }
176
177
 
177
- function normalizeDomain(value: string): string | null {
178
- let input = value.trim().toLowerCase();
179
- if (!input) return null;
180
- if (input.startsWith("-")) input = input.slice(1).trim();
181
- if (!input) return null;
182
- try {
183
- const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
184
- input = parsed.hostname;
185
- } catch {
186
- input = input.split("/")[0]?.split(":")[0] ?? "";
187
- }
188
- input = input.replace(/^\.+|\.+$/g, "");
189
- return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
190
- }
191
-
192
178
  /**
193
179
  * Recency, result count and domain filters are folded into the prompt rather
194
180
  * than sent as tool parameters. xAI's filter field names are not verified, and
@@ -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
 
@@ -70,11 +73,6 @@ function invalidResponse(message: string): Error {
70
73
  return new Error(`XCrawl API returned invalid response: ${message}`);
71
74
  }
72
75
 
73
- function normalizeCount(value: number | undefined): number {
74
- if (typeof value !== "number" || !Number.isFinite(value)) return 5;
75
- return Math.max(1, Math.min(Math.floor(value), 20));
76
- }
77
-
78
76
  function hostnameOf(url: string): string {
79
77
  try {
80
78
  return new URL(url).hostname.toLowerCase();
@@ -96,23 +94,6 @@ function absolutizeLink(link: string): string {
96
94
  }
97
95
  }
98
96
 
99
- // Normalize a shared domainFilter entry the same way Valyu does before
100
- // matching: trim, lowercase, strip URL/paths/ports, validate the shape.
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
97
  // XCrawl's SERP API has no server-side domain filter, so apply the shared
117
98
  // include/exclude domainFilter locally instead of returning off-domain results.
118
99
  function applyDomainFilter(results: SearchResponse["results"], domainFilter: NonNullable<SearchOptions["domainFilter"]>): SearchResponse["results"] {
@@ -175,17 +156,9 @@ function parseResponse(value: unknown): SearchResponse["results"] {
175
156
  return results;
176
157
  }
177
158
 
178
- function buildAnswer(results: SearchResponse["results"]): string {
179
- return results
180
- .map((result) => result.snippet
181
- ? `${result.snippet}\nSource: ${result.title} (${result.url})`
182
- : `Source: ${result.title} (${result.url})`)
183
- .join("\n\n");
184
- }
185
-
186
159
  export async function searchWithXCrawl(query: string, options: SearchOptions = {}): Promise<SearchResponse> {
187
160
  const apiKey = await getApiKey(options.signal);
188
- const numResults = normalizeCount(options.numResults);
161
+ const numResults = normalizeSearchResultCount(options.numResults);
189
162
  if (!apiKey) {
190
163
  throw new Error(
191
164
  "XCrawl search requires an API key. Set xcrawlApiKey in " + CONFIG_PATH +
@@ -258,7 +231,7 @@ export async function searchWithXCrawl(query: string, options: SearchOptions = {
258
231
  const filtered = (options.domainFilter?.length ? applyDomainFilter(results, options.domainFilter) : results).slice(0, numResults);
259
232
 
260
233
  return {
261
- answer: buildAnswer(filtered),
234
+ answer: formatSearchResultsAsAnswer(filtered),
262
235
  results: filtered,
263
236
  };
264
237
  }