@oh-my-pi/pi-coding-agent 15.3.2 → 15.4.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 (193) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/types/cli/file-processor.d.ts +1 -1
  3. package/dist/types/config/settings-schema.d.ts +45 -3
  4. package/dist/types/config/settings.d.ts +1 -1
  5. package/dist/types/debug/raw-sse.d.ts +2 -0
  6. package/dist/types/edit/file-read-cache.d.ts +15 -4
  7. package/dist/types/edit/index.d.ts +3 -8
  8. package/dist/types/edit/renderer.d.ts +1 -2
  9. package/dist/types/eval/__tests__/shared-executors.test.d.ts +1 -0
  10. package/dist/types/eval/js/shared/local-module-loader.d.ts +16 -0
  11. package/dist/types/eval/js/shared/rewrite-imports.d.ts +4 -0
  12. package/dist/types/eval/js/shared/runtime.d.ts +14 -8
  13. package/dist/types/eval/py/executor.d.ts +1 -2
  14. package/dist/types/eval/py/kernel.d.ts +6 -0
  15. package/dist/types/eval/py/tool-bridge.d.ts +1 -5
  16. package/dist/types/eval/session-id.d.ts +3 -0
  17. package/dist/types/extensibility/extensions/types.d.ts +1 -3
  18. package/dist/types/hashline/anchors.d.ts +15 -9
  19. package/dist/types/hashline/constants.d.ts +0 -2
  20. package/dist/types/hashline/diff.d.ts +1 -2
  21. package/dist/types/hashline/executor.d.ts +52 -0
  22. package/dist/types/hashline/hash.d.ts +44 -93
  23. package/dist/types/hashline/index.d.ts +2 -1
  24. package/dist/types/hashline/input.d.ts +2 -9
  25. package/dist/types/hashline/recovery.d.ts +3 -9
  26. package/dist/types/hashline/tokenizer.d.ts +91 -0
  27. package/dist/types/hashline/types.d.ts +5 -7
  28. package/dist/types/modes/components/extensions/types.d.ts +0 -4
  29. package/dist/types/modes/types.d.ts +1 -0
  30. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  31. package/dist/types/sdk.d.ts +2 -0
  32. package/dist/types/session/agent-session.d.ts +11 -15
  33. package/dist/types/session/agent-storage.d.ts +11 -10
  34. package/dist/types/slash-commands/acp-builtins.d.ts +3 -3
  35. package/dist/types/slash-commands/types.d.ts +0 -5
  36. package/dist/types/task/executor.d.ts +2 -0
  37. package/dist/types/tool-discovery/tool-index.d.ts +0 -50
  38. package/dist/types/tools/index.d.ts +2 -8
  39. package/dist/types/tools/match-line-format.d.ts +4 -4
  40. package/dist/types/tools/output-schema-validator.d.ts +64 -0
  41. package/dist/types/tools/review.d.ts +13 -0
  42. package/dist/types/tools/search-tool-bm25.d.ts +1 -1
  43. package/dist/types/tools/search.d.ts +4 -3
  44. package/dist/types/utils/edit-mode.d.ts +1 -1
  45. package/dist/types/web/kagi.d.ts +4 -2
  46. package/dist/types/web/parallel.d.ts +4 -3
  47. package/dist/types/web/scrapers/types.d.ts +2 -1
  48. package/dist/types/web/search/index.d.ts +12 -4
  49. package/dist/types/web/search/provider.d.ts +2 -1
  50. package/dist/types/web/search/providers/anthropic.d.ts +9 -4
  51. package/dist/types/web/search/providers/base.d.ts +34 -2
  52. package/dist/types/web/search/providers/brave.d.ts +8 -1
  53. package/dist/types/web/search/providers/codex.d.ts +13 -9
  54. package/dist/types/web/search/providers/exa.d.ts +10 -1
  55. package/dist/types/web/search/providers/gemini.d.ts +20 -23
  56. package/dist/types/web/search/providers/jina.d.ts +2 -1
  57. package/dist/types/web/search/providers/kagi.d.ts +4 -1
  58. package/dist/types/web/search/providers/kimi.d.ts +10 -1
  59. package/dist/types/web/search/providers/parallel.d.ts +3 -2
  60. package/dist/types/web/search/providers/perplexity.d.ts +5 -2
  61. package/dist/types/web/search/providers/searxng.d.ts +2 -1
  62. package/dist/types/web/search/providers/synthetic.d.ts +5 -8
  63. package/dist/types/web/search/providers/tavily.d.ts +11 -4
  64. package/dist/types/web/search/providers/utils.d.ts +8 -6
  65. package/dist/types/web/search/providers/zai.d.ts +12 -3
  66. package/package.json +7 -7
  67. package/src/cli/file-processor.ts +12 -2
  68. package/src/cli.ts +0 -8
  69. package/src/commands/commit.ts +8 -8
  70. package/src/config/prompt-templates.ts +6 -6
  71. package/src/config/settings-schema.ts +47 -3
  72. package/src/config/settings.ts +5 -5
  73. package/src/debug/raw-sse.ts +68 -3
  74. package/src/edit/file-read-cache.ts +68 -25
  75. package/src/edit/index.ts +6 -37
  76. package/src/edit/renderer.ts +9 -47
  77. package/src/edit/streaming.ts +43 -56
  78. package/src/eval/__tests__/shared-executors.test.ts +520 -0
  79. package/src/eval/js/context-manager.ts +64 -53
  80. package/src/eval/js/shared/local-module-loader.ts +265 -0
  81. package/src/eval/js/shared/prelude.txt +4 -0
  82. package/src/eval/js/shared/rewrite-imports.ts +85 -0
  83. package/src/eval/js/shared/runtime.ts +129 -86
  84. package/src/eval/js/worker-core.ts +23 -38
  85. package/src/eval/py/executor.ts +155 -84
  86. package/src/eval/py/kernel.ts +10 -1
  87. package/src/eval/py/prelude.py +22 -24
  88. package/src/eval/py/runner.py +203 -85
  89. package/src/eval/py/tool-bridge.ts +17 -10
  90. package/src/eval/session-id.ts +8 -0
  91. package/src/exec/bash-executor.ts +27 -16
  92. package/src/extensibility/extensions/runner.ts +0 -1
  93. package/src/extensibility/extensions/types.ts +1 -3
  94. package/src/hashline/anchors.ts +56 -65
  95. package/src/hashline/apply.ts +29 -31
  96. package/src/hashline/constants.ts +0 -3
  97. package/src/hashline/diff-preview.ts +4 -5
  98. package/src/hashline/diff.ts +30 -4
  99. package/src/hashline/execute.ts +91 -26
  100. package/src/hashline/executor.ts +239 -0
  101. package/src/hashline/grammar.lark +12 -10
  102. package/src/hashline/hash.ts +69 -114
  103. package/src/hashline/index.ts +2 -1
  104. package/src/hashline/input.ts +48 -41
  105. package/src/hashline/prefixes.ts +21 -11
  106. package/src/hashline/recovery.ts +63 -71
  107. package/src/hashline/stream.ts +2 -2
  108. package/src/hashline/tokenizer.ts +467 -0
  109. package/src/hashline/types.ts +6 -8
  110. package/src/internal-urls/docs-index.generated.ts +7 -7
  111. package/src/modes/components/extensions/types.ts +0 -5
  112. package/src/modes/components/session-observer-overlay.ts +11 -2
  113. package/src/modes/components/settings-selector.ts +10 -1
  114. package/src/modes/components/tree-selector.ts +10 -2
  115. package/src/modes/controllers/command-controller.ts +1 -3
  116. package/src/modes/controllers/extension-ui-controller.ts +10 -11
  117. package/src/modes/controllers/selector-controller.ts +5 -5
  118. package/src/modes/theme/theme.ts +4 -2
  119. package/src/modes/types.ts +4 -1
  120. package/src/modes/utils/ui-helpers.ts +4 -0
  121. package/src/prompts/agents/explore.md +1 -1
  122. package/src/prompts/tools/ast-edit.md +1 -1
  123. package/src/prompts/tools/ast-grep.md +1 -1
  124. package/src/prompts/tools/eval.md +1 -1
  125. package/src/prompts/tools/hashline.md +73 -94
  126. package/src/prompts/tools/read.md +4 -4
  127. package/src/prompts/tools/search.md +3 -3
  128. package/src/sdk.ts +33 -26
  129. package/src/session/agent-session.ts +59 -66
  130. package/src/session/agent-storage.ts +13 -14
  131. package/src/slash-commands/acp-builtins.ts +3 -3
  132. package/src/slash-commands/types.ts +0 -6
  133. package/src/task/executor.ts +26 -57
  134. package/src/task/index.ts +8 -4
  135. package/src/tool-discovery/tool-index.ts +0 -134
  136. package/src/tools/ast-edit.ts +36 -13
  137. package/src/tools/ast-grep.ts +45 -4
  138. package/src/tools/browser/tab-worker.ts +3 -2
  139. package/src/tools/eval.ts +2 -1
  140. package/src/tools/fetch.ts +23 -14
  141. package/src/tools/index.ts +2 -8
  142. package/src/tools/irc.ts +59 -5
  143. package/src/tools/match-line-format.ts +5 -7
  144. package/src/tools/output-schema-validator.ts +132 -0
  145. package/src/tools/read.ts +142 -31
  146. package/src/tools/review.ts +23 -0
  147. package/src/tools/search-tool-bm25.ts +3 -30
  148. package/src/tools/search.ts +48 -16
  149. package/src/tools/write.ts +3 -3
  150. package/src/tools/yield.ts +32 -41
  151. package/src/utils/edit-mode.ts +1 -2
  152. package/src/utils/file-mentions.ts +2 -2
  153. package/src/web/kagi.ts +15 -6
  154. package/src/web/parallel.ts +9 -6
  155. package/src/web/scrapers/types.ts +7 -1
  156. package/src/web/scrapers/youtube.ts +13 -7
  157. package/src/web/search/index.ts +37 -11
  158. package/src/web/search/provider.ts +5 -3
  159. package/src/web/search/providers/anthropic.ts +30 -21
  160. package/src/web/search/providers/base.ts +35 -2
  161. package/src/web/search/providers/brave.ts +4 -4
  162. package/src/web/search/providers/codex.ts +118 -89
  163. package/src/web/search/providers/exa.ts +3 -2
  164. package/src/web/search/providers/gemini.ts +58 -155
  165. package/src/web/search/providers/jina.ts +4 -4
  166. package/src/web/search/providers/kagi.ts +17 -11
  167. package/src/web/search/providers/kimi.ts +29 -13
  168. package/src/web/search/providers/parallel.ts +171 -23
  169. package/src/web/search/providers/perplexity.ts +38 -37
  170. package/src/web/search/providers/searxng.ts +3 -1
  171. package/src/web/search/providers/synthetic.ts +16 -19
  172. package/src/web/search/providers/tavily.ts +23 -18
  173. package/src/web/search/providers/utils.ts +11 -17
  174. package/src/web/search/providers/zai.ts +16 -8
  175. package/dist/types/hashline/parser.d.ts +0 -7
  176. package/dist/types/mcp/discoverable-tool-metadata.d.ts +0 -7
  177. package/dist/types/tools/vim.d.ts +0 -58
  178. package/dist/types/vim/buffer.d.ts +0 -41
  179. package/dist/types/vim/commands.d.ts +0 -6
  180. package/dist/types/vim/engine.d.ts +0 -47
  181. package/dist/types/vim/parser.d.ts +0 -3
  182. package/dist/types/vim/render.d.ts +0 -25
  183. package/dist/types/vim/types.d.ts +0 -182
  184. package/src/hashline/parser.ts +0 -246
  185. package/src/mcp/discoverable-tool-metadata.ts +0 -24
  186. package/src/prompts/tools/vim.md +0 -98
  187. package/src/tools/vim.ts +0 -949
  188. package/src/vim/buffer.ts +0 -309
  189. package/src/vim/commands.ts +0 -382
  190. package/src/vim/engine.ts +0 -2409
  191. package/src/vim/parser.ts +0 -134
  192. package/src/vim/render.ts +0 -252
  193. package/src/vim/types.ts +0 -197
@@ -3,9 +3,10 @@
3
3
  *
4
4
  * Thin wrapper that adapts shared Kagi API utilities to SearchResponse shape.
5
5
  */
6
+ import type { AuthStorage } from "@oh-my-pi/pi-ai";
6
7
  import type { SearchResponse } from "../../../web/search/types";
7
8
  import { SearchProviderError } from "../../../web/search/types";
8
- import { findKagiApiKey, KagiApiError, searchWithKagi } from "../../kagi";
9
+ import { KagiApiError, searchWithKagi } from "../../kagi";
9
10
  import { clampNumResults } from "../utils";
10
11
  import type { SearchParams } from "./base";
11
12
  import { SearchProvider } from "./base";
@@ -19,14 +20,21 @@ export async function searchKagi(params: {
19
20
  query: string;
20
21
  num_results?: number;
21
22
  signal?: AbortSignal;
23
+ authStorage: AuthStorage;
24
+ sessionId?: string;
22
25
  }): Promise<SearchResponse> {
23
26
  const numResults = clampNumResults(params.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
24
27
 
25
28
  try {
26
- const result = await searchWithKagi(params.query, {
27
- limit: numResults,
28
- signal: params.signal,
29
- });
29
+ const result = await searchWithKagi(
30
+ params.query,
31
+ {
32
+ limit: numResults,
33
+ sessionId: params.sessionId,
34
+ signal: params.signal,
35
+ },
36
+ params.authStorage,
37
+ );
30
38
 
31
39
  return {
32
40
  provider: "kagi",
@@ -51,12 +59,8 @@ export class KagiProvider extends SearchProvider {
51
59
  readonly id = "kagi";
52
60
  readonly label = "Kagi";
53
61
 
54
- async isAvailable() {
55
- try {
56
- return !!(await findKagiApiKey());
57
- } catch {
58
- return false;
59
- }
62
+ isAvailable(authStorage: AuthStorage): boolean {
63
+ return authStorage.hasAuth("kagi");
60
64
  }
61
65
 
62
66
  search(params: SearchParams): Promise<SearchResponse> {
@@ -64,6 +68,8 @@ export class KagiProvider extends SearchProvider {
64
68
  query: params.query,
65
69
  num_results: params.numSearchResults ?? params.limit,
66
70
  signal: params.signal,
71
+ authStorage: params.authStorage,
72
+ sessionId: params.sessionId,
67
73
  });
68
74
  }
69
75
  }
@@ -4,14 +4,15 @@
4
4
  * Uses Moonshot Kimi Code search API to retrieve web results.
5
5
  * Endpoint: POST https://api.kimi.com/coding/v1/search
6
6
  */
7
- import { getEnvApiKey } from "@oh-my-pi/pi-ai";
7
+ import type { AuthStorage } from "@oh-my-pi/pi-ai";
8
8
  import { $env } from "@oh-my-pi/pi-utils";
9
+
9
10
  import type { SearchResponse, SearchSource } from "../../../web/search/types";
10
11
  import { SearchProviderError } from "../../../web/search/types";
11
12
  import { clampNumResults, dateToAgeSeconds } from "../utils";
12
13
  import type { SearchParams } from "./base";
13
14
  import { SearchProvider } from "./base";
14
- import { classifyProviderHttpError, findCredential, isApiKeyAvailable, withHardTimeout } from "./utils";
15
+ import { classifyProviderHttpError, withHardTimeout } from "./utils";
15
16
 
16
17
  const KIMI_SEARCH_URL = "https://api.kimi.com/coding/v1/search";
17
18
 
@@ -24,6 +25,8 @@ export interface KimiSearchParams {
24
25
  num_results?: number;
25
26
  include_content?: boolean;
26
27
  signal?: AbortSignal;
28
+ authStorage: AuthStorage;
29
+ sessionId?: string;
27
30
  }
28
31
 
29
32
  interface KimiSearchResult {
@@ -51,14 +54,20 @@ function resolveBaseUrl(): string {
51
54
  return asTrimmed($env.MOONSHOT_SEARCH_BASE_URL) ?? asTrimmed($env.KIMI_SEARCH_BASE_URL) ?? KIMI_SEARCH_URL;
52
55
  }
53
56
 
54
- /** Find Kimi search credentials from environment or agent.db credentials. */
55
- async function findApiKey(): Promise<string | null> {
56
- const envKey =
57
- asTrimmed($env.MOONSHOT_SEARCH_API_KEY) ??
58
- asTrimmed($env.KIMI_SEARCH_API_KEY) ??
59
- getEnvApiKey("moonshot") ??
60
- null;
61
- return findCredential(envKey, "moonshot", "kimi-code");
57
+ /** Find Kimi search credentials from environment or AuthStorage. */
58
+ async function findApiKey(
59
+ authStorage: AuthStorage,
60
+ sessionId: string | undefined,
61
+ signal: AbortSignal | undefined,
62
+ ): Promise<string | null> {
63
+ const envKey = asTrimmed($env.MOONSHOT_SEARCH_API_KEY) ?? asTrimmed($env.KIMI_SEARCH_API_KEY);
64
+ if (envKey) return envKey;
65
+
66
+ return (
67
+ (await authStorage.getApiKey("moonshot", sessionId, { signal })) ??
68
+ (await authStorage.getApiKey("kimi-code", sessionId, { signal })) ??
69
+ null
70
+ );
62
71
  }
63
72
 
64
73
  async function callKimiSearch(
@@ -99,7 +108,7 @@ async function callKimiSearch(
99
108
 
100
109
  /** Execute Kimi web search. */
101
110
  export async function searchKimi(params: KimiSearchParams): Promise<SearchResponse> {
102
- const apiKey = await findApiKey();
111
+ const apiKey = await findApiKey(params.authStorage, params.sessionId, params.signal);
103
112
  if (!apiKey) {
104
113
  throw new Error(
105
114
  "Kimi search credentials not found. Set MOONSHOT_SEARCH_API_KEY, KIMI_SEARCH_API_KEY, MOONSHOT_API_KEY, or login with 'omp /login moonshot'.",
@@ -141,8 +150,13 @@ export class KimiProvider extends SearchProvider {
141
150
  readonly id = "kimi";
142
151
  readonly label = "Kimi";
143
152
 
144
- isAvailable(): Promise<boolean> {
145
- return isApiKeyAvailable(findApiKey);
153
+ isAvailable(authStorage: AuthStorage): boolean {
154
+ return (
155
+ !!asTrimmed($env.MOONSHOT_SEARCH_API_KEY) ||
156
+ !!asTrimmed($env.KIMI_SEARCH_API_KEY) ||
157
+ authStorage.hasAuth("moonshot") ||
158
+ authStorage.hasAuth("kimi-code")
159
+ );
146
160
  }
147
161
 
148
162
  search(params: SearchParams): Promise<SearchResponse> {
@@ -150,6 +164,8 @@ export class KimiProvider extends SearchProvider {
150
164
  query: params.query,
151
165
  num_results: params.numSearchResults ?? params.limit,
152
166
  signal: params.signal,
167
+ authStorage: params.authStorage,
168
+ sessionId: params.sessionId,
153
169
  });
154
170
  }
155
171
  }
@@ -1,27 +1,175 @@
1
+ import { type AuthStorage, getEnvApiKey } from "@oh-my-pi/pi-ai";
1
2
  import type { SearchResponse } from "../../../web/search/types";
2
3
  import { SearchProviderError } from "../../../web/search/types";
3
- import { findParallelApiKey, ParallelApiError, searchWithParallel } from "../../parallel";
4
+ import { ParallelApiError, type ParallelSearchResult, type ParallelSearchSource } from "../../parallel";
4
5
  import { clampNumResults } from "../utils";
5
6
  import type { SearchParams } from "./base";
6
7
  import { SearchProvider } from "./base";
7
- import { classifyProviderHttpError, toSearchSources } from "./utils";
8
+ import { classifyProviderHttpError, toSearchSources, withHardTimeout } from "./utils";
8
9
 
9
10
  const DEFAULT_NUM_RESULTS = 10;
10
11
  const MAX_NUM_RESULTS = 40;
12
+ const PARALLEL_SEARCH_URL = "https://api.parallel.ai/v1beta/search";
13
+ const PARALLEL_BETA_HEADER = "search-extract-2025-10-10";
11
14
 
12
- export async function searchParallel(params: {
13
- query: string;
14
- num_results?: number;
15
- signal?: AbortSignal;
16
- }): Promise<SearchResponse> {
17
- const numResults = clampNumResults(params.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
15
+ function isObject(value: unknown): value is object {
16
+ return typeof value === "object" && value !== null;
17
+ }
18
+
19
+ function getOwnValue(value: object, key: string): unknown {
20
+ return Object.getOwnPropertyDescriptor(value, key)?.value;
21
+ }
22
+
23
+ function getString(value: object, key: string): string | undefined {
24
+ const field = getOwnValue(value, key);
25
+ return typeof field === "string" ? field : undefined;
26
+ }
27
+
28
+ function getObjectArray(value: object, key: string): object[] {
29
+ const field = getOwnValue(value, key);
30
+ return Array.isArray(field) ? field.filter(isObject) : [];
31
+ }
32
+
33
+ function getStringArray(value: object, key: string): string[] {
34
+ const field = getOwnValue(value, key);
35
+ return Array.isArray(field) ? field.filter((item): item is string => typeof item === "string") : [];
36
+ }
37
+
38
+ function extractParallelErrorMessage(payload: unknown): string | null {
39
+ if (!isObject(payload)) return null;
40
+
41
+ const directMessage = getString(payload, "message") ?? getString(payload, "detail") ?? getString(payload, "error");
42
+ if (directMessage && directMessage.trim().length > 0) {
43
+ return directMessage.trim();
44
+ }
45
+
46
+ const errorObject = getOwnValue(payload, "error");
47
+ if (isObject(errorObject)) {
48
+ const nestedMessage = getString(errorObject, "message") ?? getString(errorObject, "detail");
49
+ if (nestedMessage && nestedMessage.trim().length > 0) {
50
+ return nestedMessage.trim();
51
+ }
52
+ }
53
+
54
+ return null;
55
+ }
56
+
57
+ function createParallelApiError(statusCode: number, detail?: string): ParallelApiError {
58
+ return new ParallelApiError(
59
+ detail ? `Parallel API error (${statusCode}): ${detail}` : `Parallel API error (${statusCode})`,
60
+ statusCode,
61
+ );
62
+ }
63
+
64
+ function parseParallelErrorResponse(statusCode: number, responseText: string): ParallelApiError {
65
+ const trimmedResponseText = responseText.trim();
66
+ if (trimmedResponseText.length === 0) {
67
+ return createParallelApiError(statusCode);
68
+ }
18
69
 
19
70
  try {
20
- const result = await searchWithParallel(params.query, [params.query], {
21
- mode: "fast",
22
- maxCharsPerResult: 10_000,
23
- signal: params.signal,
71
+ const payload: unknown = JSON.parse(trimmedResponseText);
72
+ return createParallelApiError(statusCode, extractParallelErrorMessage(payload) ?? trimmedResponseText);
73
+ } catch {
74
+ return createParallelApiError(statusCode, trimmedResponseText);
75
+ }
76
+ }
77
+
78
+ function parseSearchPayload(payload: unknown): ParallelSearchResult {
79
+ if (!isObject(payload)) {
80
+ throw new ParallelApiError("Parallel search returned an invalid response payload.");
81
+ }
82
+
83
+ const requestId = getString(payload, "search_id") ?? "";
84
+ const rawResults = getObjectArray(payload, "results");
85
+ const sources: ParallelSearchSource[] = [];
86
+
87
+ for (const item of rawResults) {
88
+ const url = getString(item, "url");
89
+ if (!url) continue;
90
+
91
+ const excerpts = getStringArray(item, "excerpts");
92
+ const snippet = excerpts.length > 0 ? excerpts.join("\n\n") : undefined;
93
+ sources.push({
94
+ title: getString(item, "title") ?? url,
95
+ url,
96
+ snippet,
97
+ publishedDate: getString(item, "publish_date"),
98
+ excerpts,
24
99
  });
100
+ }
101
+
102
+ return {
103
+ requestId,
104
+ sources,
105
+ warnings: [],
106
+ usage: [],
107
+ };
108
+ }
109
+
110
+ async function searchWithAuthStorage(
111
+ objective: string,
112
+ queries: string[],
113
+ params: {
114
+ signal?: AbortSignal;
115
+ },
116
+ authStorage: AuthStorage,
117
+ sessionId?: string,
118
+ ): Promise<ParallelSearchResult> {
119
+ const apiKey = await authStorage.getApiKey("parallel", sessionId, { signal: params.signal });
120
+ if (!apiKey) {
121
+ throw new ParallelApiError(
122
+ "Parallel credentials not found. Set PARALLEL_API_KEY or login with 'omp /login parallel'.",
123
+ );
124
+ }
125
+
126
+ const response = await fetch(PARALLEL_SEARCH_URL, {
127
+ method: "POST",
128
+ headers: {
129
+ Accept: "application/json",
130
+ "Content-Type": "application/json",
131
+ "x-api-key": apiKey,
132
+ "parallel-beta": PARALLEL_BETA_HEADER,
133
+ },
134
+ body: JSON.stringify({
135
+ objective,
136
+ search_queries: queries,
137
+ mode: "fast",
138
+ excerpts: {
139
+ max_chars_per_result: 10_000,
140
+ },
141
+ }),
142
+ signal: withHardTimeout(params.signal),
143
+ });
144
+ if (!response.ok) {
145
+ throw parseParallelErrorResponse(response.status, await response.text());
146
+ }
147
+
148
+ const payload: unknown = await response.json();
149
+ return parseSearchPayload(payload);
150
+ }
151
+
152
+ export async function searchParallel(
153
+ params: {
154
+ query: string;
155
+ num_results?: number;
156
+ signal?: AbortSignal;
157
+ },
158
+ authStorage: AuthStorage,
159
+ sessionId?: string,
160
+ ): Promise<SearchResponse> {
161
+ const numResults = clampNumResults(params.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
162
+
163
+ try {
164
+ const result = await searchWithAuthStorage(
165
+ params.query,
166
+ [params.query],
167
+ {
168
+ signal: params.signal,
169
+ },
170
+ authStorage,
171
+ sessionId,
172
+ );
25
173
 
26
174
  return {
27
175
  provider: "parallel",
@@ -44,19 +192,19 @@ export class ParallelProvider extends SearchProvider {
44
192
  readonly id = "parallel";
45
193
  readonly label = "Parallel";
46
194
 
47
- async isAvailable() {
48
- try {
49
- return !!(await findParallelApiKey());
50
- } catch {
51
- return false;
52
- }
195
+ isAvailable(authStorage: AuthStorage) {
196
+ return !!getEnvApiKey("parallel") || authStorage.hasAuth("parallel");
53
197
  }
54
198
 
55
199
  search(params: SearchParams): Promise<SearchResponse> {
56
- return searchParallel({
57
- query: params.query,
58
- num_results: params.numSearchResults ?? params.limit,
59
- signal: params.signal,
60
- });
200
+ return searchParallel(
201
+ {
202
+ query: params.query,
203
+ num_results: params.numSearchResults ?? params.limit,
204
+ signal: params.signal,
205
+ },
206
+ params.authStorage,
207
+ params.sessionId,
208
+ );
61
209
  }
62
210
  }
@@ -3,13 +3,12 @@
3
3
  *
4
4
  * Supports three auth modes:
5
5
  * - Cookies (`PERPLEXITY_COOKIES`) via `www.perplexity.ai/rest/sse/perplexity_ask`
6
- * - OAuth JWT (stored in `agent.db`) via `www.perplexity.ai/rest/sse/perplexity_ask`
6
+ * - OAuth/session bearer via `AuthStorage` and `www.perplexity.ai/rest/sse/perplexity_ask`
7
7
  * - API key (`PERPLEXITY_API_KEY`) via `api.perplexity.ai/chat/completions`
8
8
  */
9
9
 
10
- import { getEnvApiKey } from "@oh-my-pi/pi-ai";
11
- import { $env, getAgentDbPath, readSseJson } from "@oh-my-pi/pi-utils";
12
- import { AgentStorage } from "../../../session/agent-storage";
10
+ import { type AuthStorage, getEnvApiKey } from "@oh-my-pi/pi-ai";
11
+ import { $env, readSseJson } from "@oh-my-pi/pi-utils";
13
12
  import type {
14
13
  PerplexityMessageOutput,
15
14
  PerplexityRequest,
@@ -34,12 +33,6 @@ const OAUTH_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
34
33
  const OAUTH_API_VERSION = "2.18";
35
34
  const OAUTH_USER_AGENT = "Perplexity/641 CFNetwork/1568 Darwin/25.2.0";
36
35
 
37
- interface PerplexityOAuthCredential {
38
- type: "oauth";
39
- access: string;
40
- expires: number;
41
- }
42
-
43
36
  type PerplexityAuth =
44
37
  | {
45
38
  type: "api_key";
@@ -168,6 +161,8 @@ export interface PerplexitySearchParams {
168
161
  temperature?: number;
169
162
  /** Number of search results to retrieve. Defaults to 10. */
170
163
  num_search_results?: number;
164
+ authStorage: AuthStorage;
165
+ sessionId?: string;
171
166
  }
172
167
 
173
168
  /** Find PERPLEXITY_API_KEY from environment or .env files (also checks PPLX_API_KEY) */
@@ -194,41 +189,49 @@ function jwtExpiryMs(token: string): number | undefined {
194
189
  }
195
190
  }
196
191
 
197
- async function findOAuthToken(): Promise<string | null> {
198
- const now = Date.now();
192
+ async function findOAuthToken(
193
+ authStorage: AuthStorage,
194
+ sessionId: string | undefined,
195
+ signal: AbortSignal | undefined,
196
+ ): Promise<string | null> {
199
197
  try {
200
- const storage = await AgentStorage.open(getAgentDbPath());
201
- const records = storage.listAuthCredentials("perplexity");
202
- for (const record of records) {
203
- if (record.credential.type !== "oauth") continue;
204
- const credential = record.credential as PerplexityOAuthCredential;
205
- if (!credential.access) continue;
206
- // Trust the JWT's own `exp` claim if it has one; otherwise treat as
207
- // non-expiring. The stored `expires` field is unreliable: older logins
208
- // wrote `loginTime + 1h` even though Perplexity JWTs typically lack `exp`.
209
- const jwtExpiry = jwtExpiryMs(credential.access);
210
- if (jwtExpiry !== undefined && jwtExpiry <= now + OAUTH_EXPIRY_BUFFER_MS) continue;
211
- return credential.access;
212
- }
198
+ // `getOAuthAccess` returns the raw OAuth bearer only — runtime/config
199
+ // api_key overrides and stored api_key credentials are intentionally
200
+ // suppressed so we don't POST an `api.perplexity.ai` key to the
201
+ // `www.perplexity.ai` session/SSE endpoint.
202
+ const access = await authStorage.getOAuthAccess("perplexity", sessionId, { signal });
203
+ const token = access?.accessToken;
204
+ if (!token) return null;
205
+ // Trust the JWT's own `exp` claim if it has one; otherwise treat as
206
+ // non-expiring. Perplexity session JWTs commonly omit `exp`.
207
+ const jwtExpiry = jwtExpiryMs(token);
208
+ if (jwtExpiry !== undefined && jwtExpiry <= Date.now() + OAUTH_EXPIRY_BUFFER_MS) return null;
209
+ return token;
213
210
  } catch {
214
211
  return null;
215
212
  }
216
- return null;
217
213
  }
218
214
 
219
- async function findPerplexityAuth(): Promise<PerplexityAuth | null> {
215
+ async function findPerplexityAuth(
216
+ authStorage: AuthStorage,
217
+ sessionId: string | undefined,
218
+ signal: AbortSignal | undefined,
219
+ ): Promise<PerplexityAuth | null> {
220
220
  // 1. PERPLEXITY_COOKIES env var
221
221
  const cookies = $env.PERPLEXITY_COOKIES?.trim();
222
222
  if (cookies) {
223
223
  return { type: "cookies", cookies };
224
224
  }
225
- // 2. OAuth token from agent.db
226
- const oauthToken = await findOAuthToken();
225
+
226
+ const apiKey = findApiKey();
227
+
228
+ // 2. OAuth/session bearer from AuthStorage.
229
+ const oauthToken = await findOAuthToken(authStorage, sessionId, signal);
227
230
  if (oauthToken) {
228
231
  return { type: "oauth", token: oauthToken };
229
232
  }
233
+
230
234
  // 3. PERPLEXITY_API_KEY env var
231
- const apiKey = findApiKey();
232
235
  if (apiKey) {
233
236
  return { type: "api_key", token: apiKey };
234
237
  }
@@ -493,7 +496,7 @@ function applySourceLimit(result: SearchResponse, limit?: number): SearchRespons
493
496
 
494
497
  /** Execute Perplexity web search */
495
498
  export async function searchPerplexity(params: PerplexitySearchParams): Promise<SearchResponse> {
496
- const auth = await findPerplexityAuth();
499
+ const auth = await findPerplexityAuth(params.authStorage, params.sessionId, params.signal);
497
500
  if (!auth) {
498
501
  throw new Error("Perplexity auth not found. Set PERPLEXITY_COOKIES, PERPLEXITY_API_KEY, or login via OAuth.");
499
502
  }
@@ -551,12 +554,8 @@ export class PerplexityProvider extends SearchProvider {
551
554
  readonly id = "perplexity";
552
555
  readonly label = "Perplexity";
553
556
 
554
- async isAvailable() {
555
- try {
556
- return !!(await findPerplexityAuth());
557
- } catch {
558
- return false;
559
- }
557
+ isAvailable(authStorage: AuthStorage): boolean {
558
+ return !!$env.PERPLEXITY_COOKIES?.trim() || authStorage.hasAuth("perplexity") || !!findApiKey();
560
559
  }
561
560
 
562
561
  search(params: SearchParams): Promise<SearchResponse> {
@@ -569,6 +568,8 @@ export class PerplexityProvider extends SearchProvider {
569
568
  system_prompt: params.systemPrompt,
570
569
  search_recency_filter: params.recency,
571
570
  num_results: params.limit,
571
+ authStorage: params.authStorage,
572
+ sessionId: params.sessionId,
572
573
  });
573
574
  }
574
575
  }
@@ -25,6 +25,8 @@
25
25
  * Reference: https://docs.searxng.org/dev/search_api.html
26
26
  */
27
27
 
28
+ import type { AuthStorage } from "@oh-my-pi/pi-ai";
29
+
28
30
  import { settings } from "../../../config/settings";
29
31
  import type { SearchResponse, SearchSource } from "../../../web/search/types";
30
32
  import { SearchProviderError } from "../../../web/search/types";
@@ -288,7 +290,7 @@ export class SearXNGProvider extends SearchProvider {
288
290
  readonly id = "searxng";
289
291
  readonly label = "SearXNG";
290
292
 
291
- isAvailable() {
293
+ isAvailable(_authStorage: AuthStorage): boolean {
292
294
  try {
293
295
  return !!findEndpoint();
294
296
  } catch {
@@ -5,12 +5,12 @@
5
5
  * Endpoint: POST https://api.synthetic.new/v2/search
6
6
  */
7
7
 
8
- import { getEnvApiKey } from "@oh-my-pi/pi-ai";
8
+ import { type AuthStorage, 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
11
  import type { SearchParams } from "./base";
12
12
  import { SearchProvider } from "./base";
13
- import { classifyProviderHttpError, findCredential, isApiKeyAvailable, withHardTimeout } from "./utils";
13
+ import { classifyProviderHttpError, withHardTimeout } from "./utils";
14
14
 
15
15
  const SYNTHETIC_SEARCH_URL = "https://api.synthetic.new/v2/search";
16
16
 
@@ -25,9 +25,13 @@ interface SyntheticSearchResponse {
25
25
  results: SyntheticSearchResult[];
26
26
  }
27
27
 
28
- /** Find Synthetic API key from environment or agent.db credentials. */
29
- export async function findApiKey(): Promise<string | null> {
30
- return findCredential(getEnvApiKey("synthetic"), "synthetic");
28
+ /** Resolve Synthetic API key through the shared auth storage pipeline. */
29
+ export function findApiKey(
30
+ authStorage: AuthStorage,
31
+ sessionId?: string,
32
+ signal?: AbortSignal,
33
+ ): Promise<string | undefined> {
34
+ return authStorage.getApiKey("synthetic", sessionId, { signal });
31
35
  }
32
36
 
33
37
  /** Call Synthetic search API. */
@@ -61,12 +65,8 @@ async function callSyntheticSearch(
61
65
  }
62
66
 
63
67
  /** Execute Synthetic web search. */
64
- export async function searchSynthetic(params: {
65
- query: string;
66
- num_results?: number;
67
- signal?: AbortSignal;
68
- }): Promise<SearchResponse> {
69
- const apiKey = await findApiKey();
68
+ export async function searchSynthetic(params: SearchParams): Promise<SearchResponse> {
69
+ const apiKey = await findApiKey(params.authStorage, params.sessionId, params.signal);
70
70
  if (!apiKey) {
71
71
  throw new Error("Synthetic credentials not found. Set SYNTHETIC_API_KEY or login with 'omp /login synthetic'.");
72
72
  }
@@ -84,7 +84,8 @@ export async function searchSynthetic(params: {
84
84
  });
85
85
  }
86
86
 
87
- const limitedSources = params.num_results ? sources.slice(0, params.num_results) : sources;
87
+ const numResults = params.numSearchResults ?? params.limit;
88
+ const limitedSources = numResults ? sources.slice(0, numResults) : sources;
88
89
 
89
90
  return {
90
91
  provider: "synthetic",
@@ -97,15 +98,11 @@ export class SyntheticProvider extends SearchProvider {
97
98
  readonly id = "synthetic";
98
99
  readonly label = "Synthetic";
99
100
 
100
- isAvailable(): Promise<boolean> {
101
- return isApiKeyAvailable(findApiKey);
101
+ isAvailable(authStorage: AuthStorage): boolean {
102
+ return authStorage.hasAuth("synthetic") || !!getEnvApiKey("synthetic");
102
103
  }
103
104
 
104
105
  search(params: SearchParams): Promise<SearchResponse> {
105
- return searchSynthetic({
106
- query: params.query,
107
- num_results: params.numSearchResults ?? params.limit,
108
- signal: params.signal,
109
- });
106
+ return searchSynthetic(params);
110
107
  }
111
108
  }