@oh-my-pi/pi-coding-agent 17.1.0 → 17.1.2

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 (196) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/cli.js +6325 -4420
  3. package/dist/types/cli/bench-cli.d.ts +1 -0
  4. package/dist/types/config/model-resolver.d.ts +6 -3
  5. package/dist/types/config/settings-schema.d.ts +62 -0
  6. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
  7. package/dist/types/launch/broker-list-order.test.d.ts +1 -0
  8. package/dist/types/live/attestation.d.ts +2 -0
  9. package/dist/types/live/controller.d.ts +10 -2
  10. package/dist/types/live/protocol.d.ts +1 -1
  11. package/dist/types/live/transport.d.ts +6 -19
  12. package/dist/types/live/visualizer.d.ts +8 -11
  13. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  14. package/dist/types/modes/components/custom-message.d.ts +1 -1
  15. package/dist/types/modes/components/message-frame.d.ts +8 -4
  16. package/dist/types/modes/components/session-account-selector.d.ts +11 -0
  17. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  18. package/dist/types/modes/interactive-mode.d.ts +3 -6
  19. package/dist/types/modes/types.d.ts +9 -5
  20. package/dist/types/modes/utils/ui-helpers.d.ts +1 -6
  21. package/dist/types/session/agent-session-types.d.ts +8 -1
  22. package/dist/types/session/agent-session.d.ts +20 -1
  23. package/dist/types/session/auth-storage.d.ts +1 -1
  24. package/dist/types/session/eval-runner.d.ts +2 -0
  25. package/dist/types/session/messages.d.ts +2 -0
  26. package/dist/types/session/session-tools.d.ts +15 -0
  27. package/dist/types/session/streaming-output.d.ts +8 -0
  28. package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
  29. package/dist/types/stt/index.d.ts +0 -2
  30. package/dist/types/stt/stt-controller.d.ts +7 -0
  31. package/dist/types/task/executor.d.ts +3 -1
  32. package/dist/types/task/structured-subagent.d.ts +3 -0
  33. package/dist/types/task/types.d.ts +11 -11
  34. package/dist/types/thinking.d.ts +13 -0
  35. package/dist/types/tiny/title-client.d.ts +10 -0
  36. package/dist/types/tools/builtin-names.d.ts +1 -1
  37. package/dist/types/tools/computer/protocol.d.ts +43 -0
  38. package/dist/types/tools/computer/supervisor.d.ts +32 -0
  39. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  40. package/dist/types/tools/computer/worker.d.ts +15 -0
  41. package/dist/types/tools/computer-renderer.d.ts +22 -0
  42. package/dist/types/tools/computer.d.ts +71 -0
  43. package/dist/types/tools/context.d.ts +2 -0
  44. package/dist/types/tools/default-renderer.d.ts +21 -0
  45. package/dist/types/tools/eval-format/index.d.ts +7 -0
  46. package/dist/types/tools/eval-format/javascript.d.ts +2 -0
  47. package/dist/types/tools/eval-format/julia.d.ts +2 -0
  48. package/dist/types/tools/eval-format/python.d.ts +2 -0
  49. package/dist/types/tools/eval-format/ruby.d.ts +2 -0
  50. package/dist/types/tools/index.d.ts +2 -0
  51. package/dist/types/tools/resolve.d.ts +7 -0
  52. package/dist/types/tools/todo.d.ts +4 -1
  53. package/dist/types/tts/streaming-player.d.ts +10 -43
  54. package/dist/types/utils/tools-manager.d.ts +1 -2
  55. package/dist/types/web/search/providers/base.d.ts +16 -0
  56. package/dist/types/web/search/providers/brave.d.ts +2 -0
  57. package/dist/types/web/search/providers/firecrawl.d.ts +2 -0
  58. package/dist/types/web/search/providers/gemini.d.ts +3 -0
  59. package/dist/types/web/search/providers/jina.d.ts +2 -0
  60. package/dist/types/web/search/providers/kagi.d.ts +2 -0
  61. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  62. package/dist/types/web/search/providers/parallel.d.ts +2 -0
  63. package/dist/types/web/search/providers/perplexity.d.ts +3 -0
  64. package/dist/types/web/search/providers/searxng.d.ts +10 -0
  65. package/dist/types/web/search/providers/tavily.d.ts +8 -0
  66. package/dist/types/web/search/query.d.ts +190 -0
  67. package/package.json +12 -12
  68. package/src/cli/args.ts +1 -0
  69. package/src/cli/bench-cli.ts +12 -1
  70. package/src/cli/setup-cli.ts +2 -14
  71. package/src/cli/web-search-cli.ts +7 -0
  72. package/src/cli.ts +8 -0
  73. package/src/config/model-registry.ts +6 -0
  74. package/src/config/model-resolver.ts +17 -6
  75. package/src/config/settings-schema.ts +66 -0
  76. package/src/debug/raw-sse-buffer.ts +157 -30
  77. package/src/eval/executor-base.ts +1 -0
  78. package/src/eval/js/executor.ts +2 -0
  79. package/src/exec/bash-executor.ts +1 -0
  80. package/src/export/share.ts +4 -3
  81. package/src/extensibility/extensions/wrapper.ts +68 -12
  82. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
  83. package/src/launch/broker-list-order.test.ts +89 -0
  84. package/src/launch/broker.ts +49 -8
  85. package/src/live/attestation.ts +91 -0
  86. package/src/live/controller.ts +76 -23
  87. package/src/live/protocol.test.ts +3 -3
  88. package/src/live/protocol.ts +1 -1
  89. package/src/live/transport.ts +72 -140
  90. package/src/live/visualizer.ts +114 -134
  91. package/src/modes/components/assistant-message.ts +7 -2
  92. package/src/modes/components/custom-message.ts +4 -1
  93. package/src/modes/components/message-frame.ts +14 -8
  94. package/src/modes/components/session-account-selector.ts +62 -0
  95. package/src/modes/components/tool-execution.ts +17 -110
  96. package/src/modes/controllers/input-controller.ts +55 -47
  97. package/src/modes/controllers/live-command-controller.ts +82 -5
  98. package/src/modes/controllers/selector-controller.ts +62 -0
  99. package/src/modes/interactive-mode.ts +79 -5
  100. package/src/modes/types.ts +10 -4
  101. package/src/modes/utils/ui-helpers.ts +7 -8
  102. package/src/prompts/system/computer-safety.md +14 -0
  103. package/src/prompts/system/resolve-device-reminder.md +1 -1
  104. package/src/prompts/system/workflow-notice.md +1 -1
  105. package/src/prompts/tools/ast-edit.md +1 -0
  106. package/src/prompts/tools/bash.md +1 -0
  107. package/src/prompts/tools/computer.md +26 -0
  108. package/src/prompts/tools/eval.md +2 -2
  109. package/src/prompts/tools/task.md +5 -2
  110. package/src/prompts/tools/web-search.md +2 -0
  111. package/src/sdk.ts +13 -4
  112. package/src/session/agent-session-types.ts +9 -0
  113. package/src/session/agent-session.ts +70 -0
  114. package/src/session/auth-storage.ts +1 -0
  115. package/src/session/eval-runner.ts +5 -0
  116. package/src/session/messages.ts +3 -0
  117. package/src/session/queued-messages.ts +7 -1
  118. package/src/session/session-tools.ts +37 -0
  119. package/src/session/streaming-output.ts +52 -5
  120. package/src/slash-commands/builtin-registry.ts +165 -9
  121. package/src/slash-commands/helpers/session-pin.ts +44 -0
  122. package/src/stt/downloader.ts +0 -2
  123. package/src/stt/index.ts +0 -2
  124. package/src/stt/stt-controller.ts +57 -146
  125. package/src/system-prompt.ts +4 -0
  126. package/src/task/executor.ts +17 -9
  127. package/src/task/index.ts +14 -34
  128. package/src/task/structured-subagent.ts +4 -0
  129. package/src/task/types.ts +15 -13
  130. package/src/thinking.ts +27 -0
  131. package/src/tiny/title-client.ts +22 -0
  132. package/src/tools/ast-edit.ts +4 -1
  133. package/src/tools/bash-interactive.ts +90 -86
  134. package/src/tools/bash.ts +13 -0
  135. package/src/tools/browser/render.ts +2 -1
  136. package/src/tools/builtin-names.ts +1 -0
  137. package/src/tools/computer/protocol.ts +28 -0
  138. package/src/tools/computer/supervisor.ts +258 -0
  139. package/src/tools/computer/worker-entry.ts +25 -0
  140. package/src/tools/computer/worker.ts +135 -0
  141. package/src/tools/computer-renderer.ts +108 -0
  142. package/src/tools/computer.ts +433 -0
  143. package/src/tools/context.ts +2 -0
  144. package/src/tools/default-renderer.ts +139 -0
  145. package/src/tools/essential-tools.ts +1 -0
  146. package/src/tools/eval-format/index.ts +24 -0
  147. package/src/tools/eval-format/javascript.ts +952 -0
  148. package/src/tools/eval-format/julia.ts +446 -0
  149. package/src/tools/eval-format/python.ts +544 -0
  150. package/src/tools/eval-format/ruby.ts +380 -0
  151. package/src/tools/eval-render.ts +12 -6
  152. package/src/tools/index.ts +5 -0
  153. package/src/tools/renderers.ts +2 -0
  154. package/src/tools/resolve.ts +10 -1
  155. package/src/tools/todo.ts +58 -6
  156. package/src/tools/xdev.ts +54 -26
  157. package/src/tts/streaming-player.ts +81 -340
  158. package/src/utils/clipboard.ts +1 -30
  159. package/src/utils/mac-file-urls.applescript +37 -0
  160. package/src/utils/tool-choice.ts +14 -0
  161. package/src/utils/tools-manager.ts +1 -19
  162. package/src/web/search/index.ts +28 -5
  163. package/src/web/search/providers/anthropic.ts +62 -4
  164. package/src/web/search/providers/base.ts +16 -0
  165. package/src/web/search/providers/brave.ts +30 -3
  166. package/src/web/search/providers/codex.ts +14 -1
  167. package/src/web/search/providers/duckduckgo.ts +23 -1
  168. package/src/web/search/providers/ecosia.ts +5 -1
  169. package/src/web/search/providers/exa.ts +28 -1
  170. package/src/web/search/providers/firecrawl.ts +37 -3
  171. package/src/web/search/providers/gemini.ts +12 -2
  172. package/src/web/search/providers/google.ts +2 -1
  173. package/src/web/search/providers/jina.ts +31 -8
  174. package/src/web/search/providers/kagi.ts +10 -1
  175. package/src/web/search/providers/kimi.ts +16 -1
  176. package/src/web/search/providers/mojeek.ts +14 -1
  177. package/src/web/search/providers/parallel.ts +48 -2
  178. package/src/web/search/providers/perplexity.ts +94 -6
  179. package/src/web/search/providers/searxng.ts +145 -9
  180. package/src/web/search/providers/startpage.ts +8 -2
  181. package/src/web/search/providers/synthetic.ts +7 -1
  182. package/src/web/search/providers/tavily.ts +52 -3
  183. package/src/web/search/providers/tinyfish.ts +6 -1
  184. package/src/web/search/providers/xai.ts +49 -2
  185. package/src/web/search/providers/zai.ts +19 -1
  186. package/src/web/search/query.ts +850 -0
  187. package/dist/types/stt/recorder.d.ts +0 -30
  188. package/dist/types/stt/transcriber.d.ts +0 -14
  189. package/dist/types/stt/wav.d.ts +0 -29
  190. package/dist/types/tts/player.d.ts +0 -32
  191. package/src/live/audio-worklet.txt +0 -59
  192. package/src/live/browser-runtime.txt +0 -221
  193. package/src/stt/recorder.ts +0 -551
  194. package/src/stt/transcriber.ts +0 -60
  195. package/src/stt/wav.ts +0 -173
  196. package/src/tts/player.ts +0 -137
@@ -8,6 +8,7 @@
8
8
  import { type AuthStorage, type FetchImpl, getEnvApiKey } from "@oh-my-pi/pi-ai";
9
9
  import type { SearchResponse, SearchSource } from "../../../web/search/types";
10
10
  import { SearchProviderError } from "../../../web/search/types";
11
+ import { formatQuery, parseSearchQuery } from "../query";
11
12
  import type { SearchParams } from "./base";
12
13
  import { SearchProvider } from "./base";
13
14
  import { classifyProviderHttpError, withHardTimeout } from "./utils";
@@ -18,6 +19,8 @@ type SearchParamsWithFetch = SearchParams & { fetch?: FetchImpl };
18
19
  export interface JinaSearchParams {
19
20
  query: string;
20
21
  num_results?: number;
22
+ /** Single bare host for Jina's `X-Site` in-site search header. */
23
+ site?: string;
21
24
  signal?: AbortSignal;
22
25
  fetch?: FetchImpl;
23
26
  }
@@ -39,15 +42,18 @@ export function findApiKey(): string | null {
39
42
  async function callJinaSearch(
40
43
  apiKey: string,
41
44
  query: string,
45
+ site?: string,
42
46
  signal?: AbortSignal,
43
47
  fetchImpl: FetchImpl = fetch,
44
48
  ): Promise<JinaSearchResponse> {
45
49
  const requestUrl = `${JINA_SEARCH_URL}/${encodeURIComponent(query)}`;
50
+ const headers: Record<string, string> = {
51
+ Accept: "application/json",
52
+ Authorization: `Bearer ${apiKey}`,
53
+ };
54
+ if (site) headers["X-Site"] = site;
46
55
  const response = await fetchImpl(requestUrl, {
47
- headers: {
48
- Accept: "application/json",
49
- Authorization: `Bearer ${apiKey}`,
50
- },
56
+ headers,
51
57
  signal: withHardTimeout(signal),
52
58
  });
53
59
 
@@ -69,7 +75,7 @@ export async function searchJina(params: JinaSearchParams): Promise<SearchRespon
69
75
  throw new Error("JINA_API_KEY not found. Set it in environment or .env file.");
70
76
  }
71
77
 
72
- const response = await callJinaSearch(apiKey, params.query, params.signal, params.fetch);
78
+ const response = await callJinaSearch(apiKey, params.query, params.site, params.signal, params.fetch);
73
79
  const sources: SearchSource[] = [];
74
80
 
75
81
  for (const result of response) {
@@ -99,13 +105,30 @@ export class JinaProvider extends SearchProvider {
99
105
  }
100
106
 
101
107
  search(params: SearchParamsWithFetch): Promise<SearchResponse> {
102
- const fetchImpl = params.fetch;
108
+ const parsed = params.parsedQuery ?? parseSearchQuery(params.query);
109
+ let query = params.query;
110
+ let site: string | undefined;
111
+ if (parsed.hasDirectives) {
112
+ // Jina's X-Site header takes a single domain; with exactly one
113
+ // include site, send its host there and strip site: tokens from
114
+ // the query. Multiple sites stay inline (Bing-backed, parses them).
115
+ if (parsed.sites.length === 1) site = parsed.sites[0]!.split("/")[0];
116
+ query = formatQuery(parsed, {
117
+ phrases: true,
118
+ negation: true,
119
+ site: !site,
120
+ inTitle: true,
121
+ inUrl: true,
122
+ filetype: true,
123
+ });
124
+ }
103
125
 
104
126
  return searchJina({
105
- query: params.query,
127
+ query,
106
128
  num_results: params.numSearchResults ?? params.limit,
129
+ site,
107
130
  signal: params.signal,
108
- fetch: fetchImpl,
131
+ fetch: params.fetch,
109
132
  });
110
133
  }
111
134
  }
@@ -7,6 +7,8 @@ import type { AuthStorage, FetchImpl } from "@oh-my-pi/pi-ai";
7
7
  import type { SearchResponse } from "../../../web/search/types";
8
8
  import { SearchProviderError } from "../../../web/search/types";
9
9
  import { KagiApiError, searchWithKagi } from "../../kagi";
10
+ import type { StructuredQuery } from "../query";
11
+ import { formatQuery, GOOGLE_QUERY_SYNTAX, parseSearchQuery } from "../query";
10
12
  import { clampNumResults } from "../utils";
11
13
  import type { SearchParams } from "./base";
12
14
  import { SearchProvider } from "./base";
@@ -22,16 +24,22 @@ export async function searchKagi(params: {
22
24
  query: string;
23
25
  num_results?: number;
24
26
  recency?: SearchParams["recency"];
27
+ parsedQuery?: StructuredQuery;
25
28
  signal?: AbortSignal;
26
29
  authStorage: AuthStorage;
27
30
  sessionId?: string;
28
31
  fetch?: FetchImpl;
29
32
  }): Promise<SearchResponse> {
30
33
  const numResults = clampNumResults(params.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
34
+ // Kagi's index understands the classic Google operator set: canonicalize
35
+ // directives (domain: -> site:, until: -> before:YYYY-MM-DD, ...) and pass
36
+ // them through in the query string. Directive-free queries stay untouched.
37
+ const parsed = params.parsedQuery ?? parseSearchQuery(params.query);
38
+ const query = parsed.hasDirectives ? formatQuery(parsed, GOOGLE_QUERY_SYNTAX) : params.query;
31
39
 
32
40
  try {
33
41
  const result = await searchWithKagi(
34
- params.query,
42
+ query,
35
43
  {
36
44
  limit: numResults,
37
45
  recency: params.recency,
@@ -75,6 +83,7 @@ export class KagiProvider extends SearchProvider {
75
83
 
76
84
  return searchKagi({
77
85
  query: params.query,
86
+ parsedQuery: params.parsedQuery,
78
87
  num_results: params.numSearchResults ?? params.limit,
79
88
  recency: params.recency,
80
89
  signal: params.signal,
@@ -12,6 +12,7 @@ import { $env } from "@oh-my-pi/pi-utils";
12
12
 
13
13
  import type { SearchResponse, SearchSource } from "../../../web/search/types";
14
14
  import { SearchProviderError } from "../../../web/search/types";
15
+ import { formatQuery, parseSearchQuery, type QuerySyntax, type StructuredQuery } from "../query";
15
16
  import { clampNumResults, dateToAgeSeconds } from "../utils";
16
17
  import type { SearchParams } from "./base";
17
18
  import { SearchProvider } from "./base";
@@ -25,8 +26,19 @@ const DEFAULT_NUM_RESULTS = 10;
25
26
  const MAX_NUM_RESULTS = 20;
26
27
  const DEFAULT_TIMEOUT_SECONDS = 30;
27
28
 
29
+ /** Kimi Code search is Bing-flavored: re-emit the operators Bing parses; dates/lang stay with the central filter. */
30
+ const KIMI_QUERY_SYNTAX: QuerySyntax = {
31
+ phrases: true,
32
+ negation: true,
33
+ site: true,
34
+ inTitle: true,
35
+ inUrl: true,
36
+ filetype: true,
37
+ };
38
+
28
39
  export interface KimiSearchParams {
29
40
  query: string;
41
+ parsedQuery?: StructuredQuery;
30
42
  num_results?: number;
31
43
  include_content?: boolean;
32
44
  signal?: AbortSignal;
@@ -138,12 +150,14 @@ export async function searchKimi(params: KimiSearchParams): Promise<SearchRespon
138
150
  );
139
151
  }
140
152
 
153
+ const parsed = params.parsedQuery ?? parseSearchQuery(params.query);
154
+ const query = parsed.hasDirectives ? formatQuery(parsed, KIMI_QUERY_SYNTAX) : params.query;
141
155
  const limit = clampNumResults(params.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
142
156
  const { response, requestId } = await withAuth(
143
157
  keyOrResolver,
144
158
  key =>
145
159
  callKimiSearch(key, {
146
- query: params.query,
160
+ query,
147
161
  limit,
148
162
  includeContent: params.include_content ?? false,
149
163
  signal: params.signal,
@@ -192,6 +206,7 @@ export class KimiProvider extends SearchProvider {
192
206
 
193
207
  return searchKimi({
194
208
  query: params.query,
209
+ parsedQuery: params.parsedQuery,
195
210
  num_results: params.numSearchResults ?? params.limit,
196
211
  signal: params.signal,
197
212
  authStorage: params.authStorage,
@@ -4,6 +4,7 @@ import { parseHTML } from "linkedom";
4
4
  import type { Page } from "puppeteer-core";
5
5
  import type { SearchResponse, SearchSource } from "../../../web/search/types";
6
6
  import { SearchProviderError } from "../../../web/search/types";
7
+ import { formatScraperQuery, type QuerySyntax } from "../query";
7
8
  import { clampNumResults } from "../utils";
8
9
  import type { SearchParams } from "./base";
9
10
  import { SearchProvider } from "./base";
@@ -81,9 +82,21 @@ function parseHtmlResults(html: string): ParsedResult[] {
81
82
  return results;
82
83
  }
83
84
 
85
+ /**
86
+ * Syntax re-emitted to Mojeek for directive-carrying queries. Mojeek's
87
+ * support page (mojeek.com/support/search-operators.html) confirms `site:`,
88
+ * and the community docs confirm quoted phrases and `-` exclusions. Mojeek
89
+ * also parses `in*:` operators and its own date syntax (`since:`/`before:`
90
+ * with YYYYMMDD), but the latter differs from Google's `after:`/`before:`
91
+ * ISO form and `since` is already claimed by `recency`, so date bounds and
92
+ * `in*` constraints are conservatively left to the pipeline's lenient
93
+ * post-filter instead.
94
+ */
95
+ const MOJEEK_QUERY_SYNTAX: QuerySyntax = { phrases: true, negation: true, site: true };
96
+
84
97
  function buildSearchUrl(params: SearchParams, numResults: number): string {
85
98
  const url = new URL(MOJEEK_SEARCH_URL);
86
- url.searchParams.set("q", params.query);
99
+ url.searchParams.set("q", formatScraperQuery(params.query, params.parsedQuery, MOJEEK_QUERY_SYNTAX));
87
100
  url.searchParams.set("t", String(numResults));
88
101
  url.searchParams.set("arc", "none");
89
102
  url.searchParams.set("lang", "en");
@@ -9,6 +9,7 @@ import {
9
9
  parseParallelErrorResponse,
10
10
  parseParallelSearchPayload,
11
11
  } from "../../parallel";
12
+ import { formatQuery, parseSearchQuery, type StructuredQuery } from "../query";
12
13
  import { clampNumResults } from "../utils";
13
14
  import type { SearchParams } from "./base";
14
15
  import { SearchProvider } from "./base";
@@ -17,6 +18,42 @@ import { classifyProviderHttpError, toSearchSources, withHardTimeout } from "./u
17
18
  const DEFAULT_NUM_RESULTS = 10;
18
19
  const MAX_NUM_RESULTS = 40;
19
20
 
21
+ /** Query-string caps for Parallel: natural-language objective, no field operators. */
22
+ const PARALLEL_QUERY_SYNTAX = { phrases: true, negation: true, or: true } as const;
23
+
24
+ /** Parallel `source_policy` (beta Search API): bare-host allow/deny lists + freshness floor. */
25
+ interface ParallelSourcePolicy {
26
+ include_domains?: string[];
27
+ exclude_domains?: string[];
28
+ after_date?: string;
29
+ }
30
+
31
+ /** Site values may carry paths (`github.com/anthropics`); Parallel takes bare hosts. */
32
+ function toHosts(sites: readonly string[]): string[] {
33
+ const hosts = new Set<string>();
34
+ for (const site of sites) {
35
+ const host = site.split("/", 1)[0];
36
+ if (host) hosts.add(host);
37
+ }
38
+ return [...hosts];
39
+ }
40
+
41
+ /**
42
+ * Map parsed `site:`/`-site:`/`after:` directives onto Parallel's
43
+ * `source_policy`. Per Parallel docs, `exclude_domains` is ignored when
44
+ * `include_domains` is set, so exclusions are only sent without an allow
45
+ * list (the central lenient filter enforces them regardless).
46
+ */
47
+ function toSourcePolicy(parsed: StructuredQuery): ParallelSourcePolicy | undefined {
48
+ const policy: ParallelSourcePolicy = {};
49
+ const include = toHosts(parsed.sites);
50
+ const exclude = toHosts(parsed.excludedSites);
51
+ if (include.length) policy.include_domains = include;
52
+ else if (exclude.length) policy.exclude_domains = exclude;
53
+ if (parsed.after) policy.after_date = parsed.after;
54
+ return Object.keys(policy).length ? policy : undefined;
55
+ }
56
+
20
57
  async function searchWithAuthStorage(
21
58
  objective: string,
22
59
  queries: string[],
@@ -26,6 +63,7 @@ async function searchWithAuthStorage(
26
63
  },
27
64
  authStorage: AuthStorage,
28
65
  sessionId?: string,
66
+ sourcePolicy?: ParallelSourcePolicy,
29
67
  ): Promise<ParallelSearchResult> {
30
68
  const apiKey = await authStorage.getApiKey("parallel", sessionId, { signal: params.signal });
31
69
  if (!apiKey) {
@@ -57,6 +95,7 @@ async function searchWithAuthStorage(
57
95
  excerpts: {
58
96
  max_chars_per_result: 10_000,
59
97
  },
98
+ ...(sourcePolicy && { source_policy: sourcePolicy }),
60
99
  }),
61
100
  signal: withHardTimeout(params.signal),
62
101
  });
@@ -78,22 +117,28 @@ export async function searchParallel(
78
117
  num_results?: number;
79
118
  signal?: AbortSignal;
80
119
  fetch?: FetchImpl;
120
+ parsedQuery?: StructuredQuery;
81
121
  },
82
122
  authStorage: AuthStorage,
83
123
  sessionId?: string,
84
124
  ): Promise<SearchResponse> {
85
125
  const numResults = clampNumResults(params.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
126
+ const parsed = params.parsedQuery ?? parseSearchQuery(params.query);
127
+ // Back-compat: without directives the upstream request is byte-identical.
128
+ const query = parsed.hasDirectives ? formatQuery(parsed, PARALLEL_QUERY_SYNTAX) : params.query;
129
+ const sourcePolicy = parsed.hasDirectives ? toSourcePolicy(parsed) : undefined;
86
130
 
87
131
  try {
88
132
  const result = await searchWithAuthStorage(
89
- params.query,
90
- [params.query],
133
+ query,
134
+ [query],
91
135
  {
92
136
  signal: params.signal,
93
137
  fetch: params.fetch,
94
138
  },
95
139
  authStorage,
96
140
  sessionId,
141
+ sourcePolicy,
97
142
  );
98
143
 
99
144
  return {
@@ -128,6 +173,7 @@ export class ParallelProvider extends SearchProvider {
128
173
  num_results: params.numSearchResults ?? params.limit,
129
174
  signal: params.signal,
130
175
  fetch: params.fetch,
176
+ parsedQuery: params.parsedQuery,
131
177
  },
132
178
  params.authStorage,
133
179
  params.sessionId,
@@ -30,6 +30,7 @@ import type {
30
30
  SearchSource,
31
31
  } from "../../../web/search/types";
32
32
  import { SearchProviderError } from "../../../web/search/types";
33
+ import { formatQuery, parseSearchQuery, type QuerySyntax, type StructuredQuery } from "../query";
33
34
  import { dateToAgeSeconds } from "../utils";
34
35
  import type { SearchParams } from "./base";
35
36
  import { SearchProvider } from "./base";
@@ -46,6 +47,71 @@ const OAUTH_USER_AGENT = "Perplexity/641 CFNetwork/1568 Darwin/25.2.0";
46
47
  const ANONYMOUS_USER_AGENT =
47
48
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36";
48
49
 
50
+ /**
51
+ * Query-string operators Perplexity's search backend tolerates as text signal.
52
+ * `site:`/date/`lang:` directives are excluded: they map onto native request
53
+ * fields (`search_domain_filter`, `search_*_date_filter`,
54
+ * `search_language_filter`) and must be stripped from the query so the engine
55
+ * is not double-constrained.
56
+ */
57
+ const PERPLEXITY_QUERY_SYNTAX: QuerySyntax = {
58
+ phrases: true,
59
+ negation: true,
60
+ or: true,
61
+ inUrl: true,
62
+ inTitle: true,
63
+ filetype: true,
64
+ };
65
+
66
+ /** Native Perplexity search filters derived from parsed query directives. */
67
+ interface PerplexityNativeFilters {
68
+ /** Query rebuilt without natively-mapped directives. */
69
+ query: string;
70
+ /** `search_domain_filter`: allow entries as bare hosts, deny entries as `-host`. */
71
+ domainFilter?: string[];
72
+ /** `search_after_date_filter`, `%m/%d/%Y`. */
73
+ afterDate?: string;
74
+ /** `search_before_date_filter`, `%m/%d/%Y`. */
75
+ beforeDate?: string;
76
+ /** `search_language_filter`: ISO 639-1 two-letter codes. */
77
+ languageFilter?: string[];
78
+ }
79
+
80
+ /**
81
+ * Bare host of a `site:` value (`github.com/anthropics` → `github.com`);
82
+ * Perplexity's domain filter takes hosts only, the path part is enforced by
83
+ * the central lenient post-filter.
84
+ */
85
+ function siteHost(site: string): string {
86
+ const slash = site.indexOf("/");
87
+ return slash === -1 ? site : site.slice(0, slash);
88
+ }
89
+
90
+ /** ISO `YYYY-MM-DD` → Perplexity's documented `%m/%d/%Y` date-filter format (e.g. `3/1/2025`). */
91
+ function toPerplexityDate(iso: string): string {
92
+ const [year, month, day] = iso.split("-");
93
+ return `${Number(month)}/${Number(day)}/${year}`;
94
+ }
95
+
96
+ /** Map parsed query directives onto native Perplexity search filters. */
97
+ function buildNativeFilters(parsed: StructuredQuery, rawQuery: string): PerplexityNativeFilters {
98
+ if (!parsed.hasDirectives) return { query: rawQuery };
99
+ // Allow + deny share one array; the API caps it at 20 entries.
100
+ const domains = [
101
+ ...new Set([...parsed.sites.map(siteHost), ...parsed.excludedSites.map(site => `-${siteHost(site)}`)]),
102
+ ].slice(0, 20);
103
+ // search_language_filter takes ISO 639-1 two-letter codes; pass `en-us` as
104
+ // `en`, and leave anything else to the central post-filter.
105
+ const langCode = parsed.lang ? /^([a-z]{2})(?:[-_]|$)/.exec(parsed.lang)?.[1] : undefined;
106
+ return {
107
+ query: formatQuery(parsed, PERPLEXITY_QUERY_SYNTAX),
108
+ domainFilter: domains.length > 0 ? domains : undefined,
109
+ afterDate: parsed.after ? toPerplexityDate(parsed.after) : undefined,
110
+ beforeDate: parsed.before ? toPerplexityDate(parsed.before) : undefined,
111
+ languageFilter: langCode ? [langCode] : undefined,
112
+ };
113
+ }
114
+
49
115
  interface PerplexityOAuthStreamMarkdownBlock {
50
116
  answer?: string;
51
117
  chunks?: string[];
@@ -258,6 +324,8 @@ export interface PerplexitySearchParams {
258
324
  signal?: AbortSignal;
259
325
  query: string;
260
326
  system_prompt?: string;
327
+ /** Pre-parsed view of `query` from the search pipeline; parsed locally when absent. */
328
+ parsedQuery?: StructuredQuery;
261
329
  search_recency_filter?: "hour" | "day" | "week" | "month" | "year";
262
330
  num_results?: number;
263
331
  /** Maximum output tokens. Defaults to 8192. */
@@ -352,6 +420,10 @@ function buildPerplexityExtraBody(request: PerplexityRequest): Record<string, un
352
420
  language_preference: request.language_preference,
353
421
  return_related_questions: request.return_related_questions,
354
422
  search_recency_filter: request.search_recency_filter,
423
+ search_domain_filter: request.search_domain_filter,
424
+ search_after_date_filter: request.search_after_date_filter,
425
+ search_before_date_filter: request.search_before_date_filter,
426
+ search_language_filter: request.search_language_filter,
355
427
  };
356
428
  }
357
429
 
@@ -531,6 +603,7 @@ function buildOAuthAnswer(event: PerplexityOAuthStreamEvent): string {
531
603
  async function callPerplexityAsk(
532
604
  auth: { type: "oauth"; token: string } | { type: "cookies"; cookies: string } | { type: "anonymous" },
533
605
  params: PerplexitySearchParams,
606
+ filters: PerplexityNativeFilters,
534
607
  ): Promise<{ answer: string; sources: SearchSource[]; model?: string; requestId?: string }> {
535
608
  const requestId = crypto.randomUUID();
536
609
  // The consumer `perplexity_ask` endpoint is itself a research assistant and
@@ -539,7 +612,7 @@ async function callPerplexityAsk(
539
612
  // "I don't have access to web-search tools in this turn", so ask-endpoint
540
613
  // searches send the bare query. (The API-key path still uses system_prompt
541
614
  // as a proper `system` message.)
542
- const effectiveQuery = params.query;
615
+ const effectiveQuery = filters.query;
543
616
 
544
617
  const headers: Record<string, string> = {
545
618
  "Content-Type": "application/json",
@@ -578,7 +651,9 @@ async function callPerplexityAsk(
578
651
  version: OAUTH_API_VERSION,
579
652
  language: "en-US",
580
653
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
581
- search_recency_filter: params.search_recency_filter ?? null,
654
+ // Recency cannot be combined with absolute date filters; explicit
655
+ // before:/after: bounds take precedence.
656
+ search_recency_filter: filters.afterDate || filters.beforeDate ? null : (params.search_recency_filter ?? null),
582
657
  is_incognito: true,
583
658
  use_schematized_api: true,
584
659
  // `true` (the native app's default) lets the backend classifier skip
@@ -603,6 +678,10 @@ async function callPerplexityAsk(
603
678
  if (auth.type === "anonymous") {
604
679
  requestParams.send_back_text_in_streaming_api = true;
605
680
  }
681
+ if (filters.domainFilter) requestParams.search_domain_filter = filters.domainFilter;
682
+ if (filters.afterDate) requestParams.search_after_date_filter = filters.afterDate;
683
+ if (filters.beforeDate) requestParams.search_before_date_filter = filters.beforeDate;
684
+ if (filters.languageFilter) requestParams.search_language_filter = filters.languageFilter;
606
685
 
607
686
  const requestInit = {
608
687
  method: "POST",
@@ -781,12 +860,14 @@ function applySourceLimit(result: SearchResponse, limit?: number): SearchRespons
781
860
 
782
861
  /** Execute Perplexity web search */
783
862
  export async function searchPerplexity(params: PerplexitySearchParams): Promise<SearchResponse> {
863
+ const parsed = params.parsedQuery ?? parseSearchQuery(params.query);
864
+ const filters = buildNativeFilters(parsed, params.query);
784
865
  const systemPrompt = params.system_prompt;
785
866
  const messages: PerplexityRequest["messages"] = [];
786
867
  if (systemPrompt) {
787
868
  messages.push({ role: "system", content: systemPrompt });
788
869
  }
789
- messages.push({ role: "user", content: params.query });
870
+ messages.push({ role: "user", content: filters.query });
790
871
 
791
872
  const request: PerplexityRequest = {
792
873
  model: "sonar-pro",
@@ -805,7 +886,13 @@ export async function searchPerplexity(params: PerplexitySearchParams): Promise<
805
886
  return_related_questions: true,
806
887
  };
807
888
 
808
- if (params.search_recency_filter) {
889
+ if (filters.domainFilter) request.search_domain_filter = filters.domainFilter;
890
+ if (filters.afterDate) request.search_after_date_filter = filters.afterDate;
891
+ if (filters.beforeDate) request.search_before_date_filter = filters.beforeDate;
892
+ if (filters.languageFilter) request.search_language_filter = filters.languageFilter;
893
+ // The API rejects search_recency_filter combined with absolute date
894
+ // filters; explicit before:/after: bounds take precedence.
895
+ if (params.search_recency_filter && !filters.afterDate && !filters.beforeDate) {
809
896
  request.search_recency_filter = params.search_recency_filter;
810
897
  }
811
898
 
@@ -830,10 +917,10 @@ export async function searchPerplexity(params: PerplexitySearchParams): Promise<
830
917
  ? await withOAuthAccess(
831
918
  params.authStorage,
832
919
  "perplexity",
833
- access => callPerplexityAsk({ type: "oauth", token: access.accessToken }, params),
920
+ access => callPerplexityAsk({ type: "oauth", token: access.accessToken }, params, filters),
834
921
  { sessionId: params.sessionId, signal: params.signal, seed: auth.access },
835
922
  )
836
- : await callPerplexityAsk(auth, params);
923
+ : await callPerplexityAsk(auth, params, filters);
837
924
  return applySourceLimit(
838
925
  {
839
926
  provider: "perplexity",
@@ -892,6 +979,7 @@ export class PerplexityProvider extends SearchProvider {
892
979
  return searchPerplexity({
893
980
  signal: params.signal,
894
981
  query: params.query,
982
+ parsedQuery: params.parsedQuery,
895
983
  temperature: params.temperature,
896
984
  max_tokens: params.maxOutputTokens,
897
985
  num_search_results: params.numSearchResults,