@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,5 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { activityMonitor } from "./activity.ts";
3
+ import { normalizeDomain } from "./domain-filter-normalization.ts";
4
+ import { formatSearchResultsAsAnswer } from "./search-answer-formatting.ts";
3
5
  import type { SearchOptions, SearchResponse } from "./perplexity.ts";
4
6
  import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
5
7
  import { getWebSearchConfigPath } from "./utils.ts";
@@ -77,21 +79,6 @@ interface DomainFilters {
77
79
  exclude: string[];
78
80
  }
79
81
 
80
- function normalizeDomain(value: string): string | null {
81
- let input = value.trim().toLowerCase();
82
- if (!input) return null;
83
- if (input.startsWith("-")) input = input.slice(1).trim();
84
- if (!input) return null;
85
- try {
86
- const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
87
- input = parsed.hostname;
88
- } catch {
89
- input = input.split("/")[0]?.split(":")[0] ?? "";
90
- }
91
- input = input.replace(/^\.+|\.+$/g, "");
92
- return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
93
- }
94
-
95
82
  function parseDomainFilter(domainFilter: string[] | undefined): DomainFilters {
96
83
  const filters: DomainFilters = { include: [], exclude: [] };
97
84
  for (const raw of domainFilter ?? []) {
@@ -158,12 +145,6 @@ function parseSearchResponse(value: unknown): { results: SearchResponse["results
158
145
  return { results };
159
146
  }
160
147
 
161
- function buildAnswer(results: SearchResponse["results"]): string {
162
- return results.map((result) => result.snippet
163
- ? `${result.snippet}\nSource: ${result.title} (${result.url})`
164
- : `Source: ${result.title} (${result.url})`).join("\n\n");
165
- }
166
-
167
148
  export function isBochaAvailable(): boolean {
168
149
  return hasCredentialSource({ provider: "Bocha", configuredValue: loadConfig().bochaApiKey, environmentValue: process.env.BOCHA_API_KEY });
169
150
  }
@@ -217,5 +198,5 @@ export async function searchWithBocha(query: string, options: SearchOptions = {}
217
198
  }
218
199
  activityMonitor.logComplete(activityId, response.status);
219
200
  const results = parsed.results.filter((result) => passesDomainFilters(result.url, filters)).slice(0, numResults);
220
- return { answer: buildAnswer(results), results };
201
+ return { answer: formatSearchResultsAsAnswer(results), results };
221
202
  }
@@ -1,5 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { activityMonitor } from "./activity.ts";
3
+ import { normalizeDomain } from "./domain-filter-normalization.ts";
4
+ import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
3
5
  import type { SearchOptions, SearchResult, SearchResponse } from "./perplexity.ts";
4
6
  import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
5
7
  import { fetchWithCredentialRedirects, getWebSearchConfigPath, resolveApiBaseUrl } from "./utils.ts";
@@ -56,26 +58,6 @@ function getApiUrl(): string {
56
58
  })}/web/search`;
57
59
  }
58
60
 
59
- function normalizeCount(value: number | undefined): number {
60
- if (typeof value !== "number" || !Number.isFinite(value)) return 5;
61
- return Math.max(1, Math.min(Math.floor(value), 20));
62
- }
63
-
64
- function normalizeDomain(value: string): string | null {
65
- let input = value.trim().toLowerCase();
66
- if (!input) return null;
67
- if (input.startsWith("-")) input = input.slice(1).trim();
68
- if (!input) return null;
69
- try {
70
- const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
71
- input = parsed.hostname;
72
- } catch {
73
- input = input.split("/")[0]?.split(":")[0] ?? "";
74
- }
75
- input = input.replace(/^\.+|\.+$/g, "");
76
- return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
77
- }
78
-
79
61
  function normalizeDomainFilters(domainFilter: string[] | undefined): NormalizedDomainFilters {
80
62
  const filters: NormalizedDomainFilters = { allowed: [], blocked: [] };
81
63
  if (!domainFilter?.length) return filters;
@@ -151,7 +133,7 @@ export async function searchWithBrave(
151
133
  );
152
134
  }
153
135
 
154
- const numResults = normalizeCount(options.numResults);
136
+ const numResults = normalizeSearchResultCount(options.numResults);
155
137
  const domainFilters = normalizeDomainFilters(options.domainFilter);
156
138
  const searchQuery = buildBraveQuery(query, options.domainFilter);
157
139
  const activityId = activityMonitor.logStart({ type: "api", query: searchQuery });
@@ -1,5 +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";
4
+ import { formatSearchResultsAsAnswer } from "./search-answer-formatting.ts";
5
+ import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
3
6
  import type { SearchOptions, SearchResponse } from "./perplexity.ts";
4
7
  import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
5
8
  import { getWebSearchConfigPath } from "./utils.ts";
@@ -182,26 +185,6 @@ function requireSerpZone(): string {
182
185
  );
183
186
  }
184
187
 
185
- function normalizeCount(value: number | undefined): number {
186
- if (typeof value !== "number" || !Number.isFinite(value)) return 5;
187
- return Math.max(1, Math.min(Math.floor(value), 20));
188
- }
189
-
190
- function normalizeDomain(value: string): string | null {
191
- let input = value.trim().toLowerCase();
192
- if (!input) return null;
193
- if (input.startsWith("-")) input = input.slice(1).trim();
194
- if (!input) return null;
195
- try {
196
- const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
197
- input = parsed.hostname;
198
- } catch {
199
- input = input.split("/")[0]?.split(":")[0] ?? "";
200
- }
201
- input = input.replace(/^\.+|\.+$/g, "");
202
- return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
203
- }
204
-
205
188
  interface DomainFilters {
206
189
  include: string[];
207
190
  exclude: string[];
@@ -436,16 +419,6 @@ function mapResults(
436
419
  return mapped;
437
420
  }
438
421
 
439
- // A SERP zone returns ranked links, never a synthesized answer, so one is
440
- // assembled from the sources — the same shape brave.ts and searxng.ts produce.
441
- function buildAnswer(results: SearchResponse["results"]): string {
442
- return results
443
- .map((result) => result.snippet
444
- ? `${result.snippet}\nSource: ${result.title} (${result.url})`
445
- : `Source: ${result.title} (${result.url})`)
446
- .join("\n\n");
447
- }
448
-
449
422
  // Both halves are required: a token with no zone cannot make a request, and a
450
423
  // zone with no token cannot either. Availability therefore checks the config this
451
424
  // surface actually needs rather than merely a key, the way firecrawl.ts's
@@ -477,7 +450,7 @@ export async function searchWithBrightData(query: string, options: BrightDataSea
477
450
  // resolver, and must not reach a billable endpoint.
478
451
  const zone = requireSerpZone();
479
452
  const apiKey = await requireApiKey(options.signal);
480
- const numResults = normalizeCount(options.numResults);
453
+ const numResults = normalizeSearchResultCount(options.numResults);
481
454
  const filters = parseDomainFilter(options.domainFilter);
482
455
  const searchQuery = buildSearchQuery(query, filters);
483
456
  const body: Record<string, unknown> = {
@@ -560,5 +533,5 @@ export async function searchWithBrightData(query: string, options: BrightDataSea
560
533
 
561
534
  activityMonitor.logComplete(activityId, response.status);
562
535
  const results = mapResults(data.organic, numResults, filters);
563
- return { answer: buildAnswer(results), results };
536
+ return { answer: formatSearchResultsAsAnswer(results), results };
564
537
  }
@@ -12,7 +12,6 @@ interface Match {
12
12
  interface Range {
13
13
  start: number;
14
14
  end: number;
15
- matches: Match[];
16
15
  }
17
16
 
18
17
  function normalize(value: string): string {
@@ -27,9 +26,9 @@ function editDistanceWithin(left: string, right: string, maximum: number): boole
27
26
  let rowMinimum = i;
28
27
  for (let j = 1; j <= right.length; j++) {
29
28
  const value = Math.min(
30
- (previous[j] ?? 0) + 1,
31
- (current[j - 1] ?? 0) + 1,
32
- (previous[j - 1] ?? 0) + (left[i - 1] === right[j - 1] ? 0 : 1),
29
+ previous[j] + 1,
30
+ current[j - 1] + 1,
31
+ previous[j - 1] + (left[i - 1] === right[j - 1] ? 0 : 1),
33
32
  );
34
33
  current[j] = value;
35
34
  rowMinimum = Math.min(rowMinimum, value);
@@ -37,7 +36,7 @@ function editDistanceWithin(left: string, right: string, maximum: number): boole
37
36
  if (rowMinimum > maximum) return false;
38
37
  previous = current;
39
38
  }
40
- return (previous[right.length] ?? maximum + 1) <= maximum;
39
+ return previous[right.length] <= maximum;
41
40
  }
42
41
 
43
42
  function literalMatches(text: string, query: string, caseInsensitive: boolean): Match[] {
@@ -69,27 +68,54 @@ function fuzzyMatches(text: string, query: string): Match[] {
69
68
  const first = tokens.find(token => matched.some(queryToken => {
70
69
  const maximum = queryToken.length >= 9 ? 2 : queryToken.length >= 5 ? 1 : 0;
71
70
  return editDistanceWithin(queryToken, normalize(token[0]), maximum);
72
- }));
73
- const start = paragraph.index + (first?.index ?? 0);
74
- matches.push({ query, start, end: start + (first?.[0].length ?? query.length) });
71
+ }))!;
72
+ const start = paragraph.index + first.index;
73
+ matches.push({ query, start, end: start + first[0].length });
75
74
  }
76
75
  return matches;
77
76
  }
78
77
 
79
- function mergeRanges(textLength: number, matches: Match[]): Range[] {
78
+ function mergeRanges(ranges: Range[]): Range[] {
79
+ const merged: Range[] = [];
80
+ for (const range of [...ranges].sort((left, right) => left.start - right.start || left.end - right.end)) {
81
+ const previous = merged.at(-1);
82
+ if (previous && range.start <= previous.end) previous.end = Math.max(previous.end, range.end);
83
+ else merged.push({ ...range });
84
+ }
85
+ return merged;
86
+ }
87
+
88
+ function contextRanges(textLength: number, matches: Match[]): Range[] {
80
89
  const ranges: Range[] = [];
81
- for (const match of [...matches].sort((left, right) => left.start - right.start)) {
82
- const start = Math.max(0, match.start - CONTEXT_CHARS);
83
- const end = Math.min(textLength, match.end + CONTEXT_CHARS);
84
- const previous = ranges.at(-1);
85
- if (previous && start <= previous.end) {
86
- previous.end = Math.max(previous.end, end);
87
- previous.matches.push(match);
88
- } else {
89
- ranges.push({ start, end, matches: [match] });
90
- }
90
+ for (const match of matches) {
91
+ ranges.push({
92
+ start: Math.max(0, match.start - CONTEXT_CHARS),
93
+ end: Math.min(textLength, match.end + CONTEXT_CHARS),
94
+ });
91
95
  }
92
- return ranges;
96
+ return mergeRanges(ranges);
97
+ }
98
+
99
+ function lowerBound(values: number[], target: number): number {
100
+ let low = 0;
101
+ let high = values.length;
102
+ while (low < high) {
103
+ const middle = (low + high) >>> 1;
104
+ if (values[middle] < target) low = middle + 1;
105
+ else high = middle;
106
+ }
107
+ return low;
108
+ }
109
+
110
+ function upperBound(values: number[], target: number): number {
111
+ let low = 0;
112
+ let high = values.length;
113
+ while (low < high) {
114
+ const middle = (low + high) >>> 1;
115
+ if (values[middle] <= target) low = middle + 1;
116
+ else high = middle;
117
+ }
118
+ return low;
93
119
  }
94
120
 
95
121
  export function findContent(
@@ -98,42 +124,131 @@ export function findContent(
98
124
  mode: FindMode,
99
125
  ): { text: string; matchCount: number; returnedMatches: number; queryResults: Array<{ query: string; matchCount: number }> } {
100
126
  const normalizedQueries = [...new Set(queries.map(query => query.trim()).filter(Boolean))];
101
- const matches = normalizedQueries.flatMap(query => mode === "fuzzy"
102
- ? fuzzyMatches(text, query)
103
- : literalMatches(text, query, mode === "case-insensitive"));
104
- const queryResults = normalizedQueries.map(query => ({
105
- query,
106
- matchCount: matches.filter(match => match.query === query).length,
107
- }));
127
+ const occurrences = normalizedQueries.map(query => {
128
+ const matches = mode === "fuzzy" ? fuzzyMatches(text, query) : literalMatches(text, query, mode === "case-insensitive");
129
+ return { query, matches, starts: matches.map(match => match.start), ends: matches.map(match => match.end) };
130
+ });
131
+ const matches = occurrences.flatMap(result => result.matches);
132
+ const queryResults = occurrences.map(result => ({ query: result.query, matchCount: result.matches.length }));
108
133
 
109
134
  const heading = matches.length > 0 ? `Text matches (${mode})` : `Text matches (${mode}): no matches`;
110
- const sections = [heading];
111
- let formattedLength = heading.length;
112
- let returnedMatches = 0;
113
- for (const range of mergeRanges(text.length, matches)) {
114
- const prefix = range.start > 0 ? "…" : "";
115
- const suffix = range.end < text.length ? "…" : "";
116
- const snippet = `${prefix}${text.slice(range.start, range.end).replace(/\s+/g, " ").trim()}${suffix}`;
117
- const counts = [...new Set(range.matches.map(match => match.query))]
118
- .map(query => `\"${query}\" ×${range.matches.filter(match => match.query === query).length}`)
119
- .join(", ");
120
- const section = `${sections.length}. ${counts}\n${snippet}`;
121
- if (formattedLength + 2 + section.length > MAX_OUTPUT_CHARS) break;
122
- sections.push(section);
123
- formattedLength += 2 + section.length;
124
- returnedMatches += range.matches.length;
125
- }
126
-
127
135
  const missing = queryResults.filter(result => result.matchCount === 0).map(result => `\"${result.query}\"`);
128
- const footer = [
129
- ...(missing.length > 0 ? [`No matches: ${missing.join(", ")}`] : []),
130
- ...(returnedMatches < matches.length ? [`Showing ${returnedMatches} of ${matches.length} matches.`] : []),
131
- ];
132
- for (const section of footer) {
133
- if (formattedLength + 2 + section.length > MAX_OUTPUT_CHARS) break;
134
- sections.push(section);
135
- formattedLength += 2 + section.length;
136
+ const matchingQueries = occurrences.filter(result => result.matches.length > 0).map((result, index) => ({
137
+ ...result,
138
+ id: `Q${index + 1}`,
139
+ order: index,
140
+ }));
141
+ const whitespaceRuns = [...text.matchAll(/\s+/g)].map(run => ({ start: run.index, end: run.index + run[0].length }));
142
+ const whitespaceStarts = whitespaceRuns.map(run => run.start);
143
+ const whitespaceEnds = whitespaceRuns.map(run => run.end);
144
+ const whitespaceSavings = [0];
145
+ for (const run of whitespaceRuns) whitespaceSavings.push(whitespaceSavings.at(-1)! + run.end - run.start - 1);
146
+ function normalizedLength(start: number, end: number): number {
147
+ const firstRun = upperBound(whitespaceStarts, start) - 1;
148
+ if (firstRun >= 0 && whitespaceEnds[firstRun] > start) start = whitespaceEnds[firstRun];
149
+ if (start >= end) return 0;
150
+ const lastRun = upperBound(whitespaceStarts, end - 1) - 1;
151
+ if (lastRun >= 0 && whitespaceEnds[lastRun] >= end) end = whitespaceStarts[lastRun];
152
+ if (start >= end) return 0;
153
+ const first = lowerBound(whitespaceStarts, start);
154
+ const last = upperBound(whitespaceEnds, end);
155
+ return end - start - (whitespaceSavings[last] - whitespaceSavings[first]);
156
+ }
157
+ function rangeCounts(range: Range) {
158
+ // Literal occurrences have fixed widths; fuzzy occurrences come from successive disjoint paragraphs.
159
+ return matchingQueries.flatMap(result => {
160
+ const first = lowerBound(result.starts, range.start);
161
+ const last = upperBound(result.ends, range.end);
162
+ return last > first ? [{ ...result, count: last - first, firstStart: result.starts[first] }] : [];
163
+ }).sort((left, right) => left.firstStart - right.firstStart || left.order - right.order);
164
+ }
165
+ const legend = matchingQueries.length > 0
166
+ ? `Queries: ${matchingQueries.map(result => `${result.id} = \"${result.query}\"`).join(", ")}`
167
+ : "";
168
+ const missingNotice = missing.length > 0 ? `No matches: ${missing.join(", ")}` : "";
169
+ function measure(ranges: Range[], overflow = false, omitted: string[] = []) {
170
+ let length = heading.length;
171
+ let returnedMatches = 0;
172
+ if (overflow && legend) length += 2 + legend.length;
173
+ for (const [index, range] of ranges.entries()) {
174
+ const counts = rangeCounts(range);
175
+ if (counts.length === 0) continue;
176
+ const labelsLength = counts.reduce((total, result) => total
177
+ + (overflow ? result.id.length : result.query.length + 2)
178
+ + 2 + String(result.count).length, 2 * (counts.length - 1));
179
+ const snippetLength = normalizedLength(range.start, range.end)
180
+ + (range.start > 0 ? 1 : 0) + (range.end < text.length ? 1 : 0);
181
+ length += 2 + String(index + 1).length + 2 + labelsLength + 1 + snippetLength;
182
+ returnedMatches += counts.reduce((total, result) => total + result.count, 0);
183
+ }
184
+ if (missingNotice) length += 2 + missingNotice.length;
185
+ if (omitted.length > 0) length += 2 + `No representative excerpt: ${omitted.join(", ")}.`.length;
186
+ if (returnedMatches < matches.length) length += 2 + `Showing ${returnedMatches} of ${matches.length} matches.`.length;
187
+ return { length, returnedMatches };
188
+ }
189
+ function formatRanges(ranges: Range[], overflow = false, omitted: string[] = []) {
190
+ const sections = [heading];
191
+ let returnedMatches = 0;
192
+ if (overflow && legend) sections.push(legend);
193
+ for (const [index, range] of ranges.entries()) {
194
+ const contained = rangeCounts(range);
195
+ if (contained.length === 0) continue;
196
+ const prefix = range.start > 0 ? "…" : "";
197
+ const suffix = range.end < text.length ? "…" : "";
198
+ const snippet = `${prefix}${text.slice(range.start, range.end).replace(/\s+/g, " ").trim()}${suffix}`;
199
+ const counts = contained
200
+ .map(result => `${overflow ? result.id : `\"${result.query}\"`} ×${result.count}`)
201
+ .join(", ");
202
+ sections.push(`${index + 1}. ${counts}\n${snippet}`);
203
+ returnedMatches += contained.reduce((total, result) => total + result.count, 0);
204
+ }
205
+ if (missingNotice) sections.push(missingNotice);
206
+ if (omitted.length > 0) sections.push(`No representative excerpt: ${omitted.join(", ")}.`);
207
+ if (returnedMatches < matches.length) sections.push(`Showing ${returnedMatches} of ${matches.length} matches.`);
208
+ return { text: sections.join("\n\n"), matchCount: matches.length, returnedMatches, queryResults };
136
209
  }
137
210
 
138
- return { text: sections.join("\n\n"), matchCount: matches.length, returnedMatches, queryResults };
211
+ const fullRanges = contextRanges(text.length, matches);
212
+ const full = measure(fullRanges);
213
+ if (full.returnedMatches === matches.length && full.length <= MAX_OUTPUT_CHARS) return formatRanges(fullRanges);
214
+
215
+ let ranges: Range[] = [];
216
+ let omitted = matchingQueries.map(result => result.id);
217
+ if (measure(ranges, true, omitted).length > MAX_OUTPUT_CHARS) {
218
+ const text = `${heading}\n\nUnable to format bounded excerpts: query metadata exceeds ${MAX_OUTPUT_CHARS} characters.\n\nShowing 0 of ${matches.length} matches.`;
219
+ return { text, matchCount: matches.length, returnedMatches: 0, queryResults };
220
+ }
221
+ const witnesses: Match[] = [];
222
+ for (const { id, matches: queryMatches } of matchingQueries) {
223
+ const proposedOmitted = omitted.filter(queryId => queryId !== id);
224
+ const currentLength = measure(ranges, true, omitted).length;
225
+ let selected: { witness: Match; ranges: Range[]; cost: number } | undefined;
226
+ for (const witness of queryMatches) {
227
+ const proposedRanges = mergeRanges([...ranges, { start: witness.start, end: witness.end }]);
228
+ const summary = measure(proposedRanges, true, proposedOmitted);
229
+ const candidate = {
230
+ witness,
231
+ ranges: proposedRanges,
232
+ cost: summary.length - currentLength,
233
+ };
234
+ if (summary.length <= MAX_OUTPUT_CHARS && (!selected || candidate.cost < selected.cost
235
+ || candidate.cost === selected.cost && (witness.start < selected.witness.start
236
+ || witness.start === selected.witness.start && witness.end < selected.witness.end))) selected = candidate;
237
+ }
238
+ if (selected) {
239
+ ranges = selected.ranges;
240
+ omitted = proposedOmitted;
241
+ witnesses.push(selected.witness);
242
+ }
243
+ }
244
+ for (const witness of witnesses) {
245
+ const proposed = mergeRanges([...ranges, {
246
+ start: Math.max(0, witness.start - CONTEXT_CHARS),
247
+ end: Math.min(text.length, witness.end + CONTEXT_CHARS),
248
+ }]);
249
+ if (measure(proposed, true, omitted).length <= MAX_OUTPUT_CHARS) ranges = proposed;
250
+ }
251
+ const allOccurrences = mergeRanges([...ranges, ...matches.map(match => ({ start: match.start, end: match.end }))]);
252
+ if (measure(allOccurrences, true, omitted).length <= MAX_OUTPUT_CHARS) ranges = allOccurrences;
253
+ return formatRanges(ranges, true, omitted);
139
254
  }
@@ -45,6 +45,7 @@ function buildProviderButtons(
45
45
  { value: "serpbase", label: "SerpBase", available: available.serpbase },
46
46
  { value: "serpapi", label: "SerpApi", available: available.serpapi },
47
47
  { value: "serper", label: "Serper", available: available.serper },
48
+ { value: "serply", label: "Serply", available: available.serply },
48
49
  { value: "valyu", label: "Valyu", available: available.valyu },
49
50
  ];
50
51
 
@@ -1472,7 +1473,7 @@ const SCRIPT = `(function() {
1472
1473
  var token = DATA.sessionToken;
1473
1474
  var timeoutSec = DATA.timeout;
1474
1475
  var queries = Array.isArray(DATA.queries) ? DATA.queries : [];
1475
- var providers = ["all", "openai", "exa", "brave", "parallel", "parallel-mcp", "tinyfish", "search1api", "searchinfinity", "querit", "tavily", "firecrawl", "jina", "serpdive", "kagi", "bocha", "ollama", "searxng", "duckduckgo", "perplexity", "gemini", "kimi", "anysearch", "xcrawl", "xai", "mistral", "brightdata", "serpbase", "serpapi", "serper", "valyu"];
1476
+ var providers = ["auto", "all", "openai", "exa", "brave", "parallel", "parallel-mcp", "tinyfish", "search1api", "searchinfinity", "querit", "tavily", "firecrawl", "jina", "serpdive", "kagi", "bocha", "ollama", "searxng", "duckduckgo", "perplexity", "gemini", "kimi", "anysearch", "xcrawl", "xai", "mistral", "brightdata", "serpbase", "serpapi", "serper", "serply", "valyu"];
1476
1477
  var availProviders = DATA.availableProviders && typeof DATA.availableProviders === "object" ? DATA.availableProviders : {};
1477
1478
  var workflow = "summary-review";
1478
1479
  var initialDefaultProvider = typeof DATA.defaultProvider === "string" ? DATA.defaultProvider : "exa";
@@ -1676,6 +1677,7 @@ const SCRIPT = `(function() {
1676
1677
  }
1677
1678
 
1678
1679
  function providerLabel(provider) {
1680
+ if (provider === "auto") return "Auto";
1679
1681
  if (provider === "all") return "All";
1680
1682
  if (provider === "openai") return "OpenAI";
1681
1683
  if (provider === "brave") return "Brave";
@@ -1705,6 +1707,7 @@ const SCRIPT = `(function() {
1705
1707
  if (provider === "serpbase") return "SerpBase";
1706
1708
  if (provider === "serpapi") return "SerpApi";
1707
1709
  if (provider === "serper") return "Serper";
1710
+ if (provider === "serply") return "Serply";
1708
1711
  if (provider === "valyu") return "Valyu";
1709
1712
  return "Unknown";
1710
1713
  }
@@ -7,7 +7,8 @@ export function resolveWebSearchWorkflow(input: unknown, hasUI: boolean): WebSea
7
7
  if (normalized === "auto-summary") return "auto-summary";
8
8
  if (!hasUI) return "none";
9
9
  if (normalized === "none") return "none";
10
- return "summary-review";
10
+ if (normalized === "summary-review") return "summary-review";
11
+ return "none";
11
12
  }
12
13
 
13
14
  export class CuratorRunState {
@@ -304,6 +304,7 @@ export function startCuratorServer(
304
304
  if (provider === "brightdata") return availableProviders.brightdata;
305
305
  if (provider === "serpbase") return availableProviders.serpbase;
306
306
  if (provider === "serper") return availableProviders.serper;
307
+ if (provider === "serply") return availableProviders.serply;
307
308
  if (provider === "valyu") return availableProviders.valyu;
308
309
  return false;
309
310
  }