@oh-my-pi/pi-coding-agent 15.3.2 → 15.4.1

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 (191) hide show
  1. package/CHANGELOG.md +104 -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/tree-selector.ts +10 -2
  114. package/src/modes/controllers/command-controller.ts +1 -3
  115. package/src/modes/controllers/extension-ui-controller.ts +10 -11
  116. package/src/modes/controllers/selector-controller.ts +5 -5
  117. package/src/modes/types.ts +4 -1
  118. package/src/modes/utils/ui-helpers.ts +4 -0
  119. package/src/prompts/agents/explore.md +1 -1
  120. package/src/prompts/tools/ast-edit.md +1 -1
  121. package/src/prompts/tools/ast-grep.md +1 -1
  122. package/src/prompts/tools/eval.md +1 -1
  123. package/src/prompts/tools/hashline.md +73 -94
  124. package/src/prompts/tools/read.md +4 -4
  125. package/src/prompts/tools/search.md +3 -3
  126. package/src/sdk.ts +17 -23
  127. package/src/session/agent-session.ts +59 -66
  128. package/src/session/agent-storage.ts +13 -14
  129. package/src/slash-commands/acp-builtins.ts +3 -3
  130. package/src/slash-commands/types.ts +0 -6
  131. package/src/task/executor.ts +26 -57
  132. package/src/task/index.ts +8 -4
  133. package/src/tool-discovery/tool-index.ts +0 -134
  134. package/src/tools/ast-edit.ts +36 -13
  135. package/src/tools/ast-grep.ts +45 -4
  136. package/src/tools/browser/tab-worker.ts +3 -2
  137. package/src/tools/eval.ts +2 -1
  138. package/src/tools/fetch.ts +23 -14
  139. package/src/tools/index.ts +2 -8
  140. package/src/tools/irc.ts +59 -5
  141. package/src/tools/match-line-format.ts +5 -7
  142. package/src/tools/output-schema-validator.ts +132 -0
  143. package/src/tools/read.ts +142 -31
  144. package/src/tools/review.ts +23 -0
  145. package/src/tools/search-tool-bm25.ts +3 -30
  146. package/src/tools/search.ts +48 -16
  147. package/src/tools/write.ts +3 -3
  148. package/src/tools/yield.ts +32 -41
  149. package/src/utils/edit-mode.ts +1 -2
  150. package/src/utils/file-mentions.ts +2 -2
  151. package/src/web/kagi.ts +15 -6
  152. package/src/web/parallel.ts +9 -6
  153. package/src/web/scrapers/types.ts +7 -1
  154. package/src/web/scrapers/youtube.ts +13 -7
  155. package/src/web/search/index.ts +37 -11
  156. package/src/web/search/provider.ts +5 -3
  157. package/src/web/search/providers/anthropic.ts +30 -21
  158. package/src/web/search/providers/base.ts +35 -2
  159. package/src/web/search/providers/brave.ts +4 -4
  160. package/src/web/search/providers/codex.ts +118 -89
  161. package/src/web/search/providers/exa.ts +3 -2
  162. package/src/web/search/providers/gemini.ts +58 -155
  163. package/src/web/search/providers/jina.ts +4 -4
  164. package/src/web/search/providers/kagi.ts +17 -11
  165. package/src/web/search/providers/kimi.ts +29 -13
  166. package/src/web/search/providers/parallel.ts +171 -23
  167. package/src/web/search/providers/perplexity.ts +38 -37
  168. package/src/web/search/providers/searxng.ts +3 -1
  169. package/src/web/search/providers/synthetic.ts +16 -19
  170. package/src/web/search/providers/tavily.ts +23 -18
  171. package/src/web/search/providers/utils.ts +11 -17
  172. package/src/web/search/providers/zai.ts +16 -8
  173. package/dist/types/hashline/parser.d.ts +0 -7
  174. package/dist/types/mcp/discoverable-tool-metadata.d.ts +0 -7
  175. package/dist/types/tools/vim.d.ts +0 -58
  176. package/dist/types/vim/buffer.d.ts +0 -41
  177. package/dist/types/vim/commands.d.ts +0 -6
  178. package/dist/types/vim/engine.d.ts +0 -47
  179. package/dist/types/vim/parser.d.ts +0 -3
  180. package/dist/types/vim/render.d.ts +0 -25
  181. package/dist/types/vim/types.d.ts +0 -182
  182. package/src/hashline/parser.ts +0 -246
  183. package/src/mcp/discoverable-tool-metadata.ts +0 -24
  184. package/src/prompts/tools/vim.md +0 -98
  185. package/src/tools/vim.ts +0 -949
  186. package/src/vim/buffer.ts +0 -309
  187. package/src/vim/commands.ts +0 -382
  188. package/src/vim/engine.ts +0 -2409
  189. package/src/vim/parser.ts +0 -134
  190. package/src/vim/render.ts +0 -252
  191. package/src/vim/types.ts +0 -197
@@ -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
  }
@@ -4,13 +4,13 @@
4
4
  * Uses Tavily's agent-focused search API to return structured results with an
5
5
  * optional synthesized answer.
6
6
  */
7
- import { getEnvApiKey } from "@oh-my-pi/pi-ai";
7
+ import { type AuthStorage, getEnvApiKey } from "@oh-my-pi/pi-ai";
8
8
  import type { SearchResponse, SearchSource } from "../../../web/search/types";
9
9
  import { SearchProviderError } from "../../../web/search/types";
10
10
  import { clampNumResults, dateToAgeSeconds } from "../utils";
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 TAVILY_SEARCH_URL = "https://api.tavily.com/search";
16
16
  const DEFAULT_NUM_RESULTS = 5;
@@ -58,9 +58,13 @@ function getErrorMessage(value: unknown): string | null {
58
58
  return null;
59
59
  }
60
60
 
61
- /** Find Tavily API key from environment or agent.db credentials. */
62
- export async function findApiKey(): Promise<string | null> {
63
- return findCredential(getEnvApiKey("tavily"), "tavily");
61
+ /** Find Tavily API key through AuthStorage's unified refresh pipeline. */
62
+ export async function findApiKey(
63
+ authStorage: AuthStorage,
64
+ sessionId: string | undefined,
65
+ signal: AbortSignal | undefined,
66
+ ): Promise<string | null> {
67
+ return (await authStorage.getApiKey("tavily", sessionId, { signal })) ?? null;
64
68
  }
65
69
 
66
70
  /** Exported for testing. Builds the Tavily request body from unified params. */
@@ -116,16 +120,22 @@ async function callTavilySearch(apiKey: string, params: TavilySearchParams): Pro
116
120
  }
117
121
 
118
122
  /** Execute Tavily web search. */
119
- export async function searchTavily(params: TavilySearchParams): Promise<SearchResponse> {
120
- const apiKey = await findApiKey();
123
+ export async function searchTavily(params: SearchParams): Promise<SearchResponse> {
124
+ const tavilyParams: TavilySearchParams = {
125
+ query: params.query,
126
+ num_results: params.numSearchResults ?? params.limit,
127
+ recency: params.recency,
128
+ signal: params.signal,
129
+ };
130
+ const apiKey = await findApiKey(params.authStorage, params.sessionId, params.signal);
121
131
  if (!apiKey) {
122
132
  throw new Error(
123
- 'Tavily credentials not found. Set TAVILY_API_KEY or store an API key for provider "tavily" in agent.db.',
133
+ 'Tavily credentials not found. Set TAVILY_API_KEY or configure an API key for provider "tavily".',
124
134
  );
125
135
  }
126
136
 
127
- const numResults = clampNumResults(params.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
128
- const response = await callTavilySearch(apiKey, params);
137
+ const numResults = clampNumResults(tavilyParams.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
138
+ const response = await callTavilySearch(apiKey, tavilyParams);
129
139
  const sources: SearchSource[] = [];
130
140
 
131
141
  for (const result of response.results ?? []) {
@@ -153,16 +163,11 @@ export class TavilyProvider extends SearchProvider {
153
163
  readonly id = "tavily";
154
164
  readonly label = "Tavily";
155
165
 
156
- isAvailable(): Promise<boolean> {
157
- return isApiKeyAvailable(findApiKey);
166
+ isAvailable(authStorage: AuthStorage): boolean {
167
+ return authStorage.hasAuth("tavily") || !!getEnvApiKey("tavily");
158
168
  }
159
169
 
160
170
  search(params: SearchParams): Promise<SearchResponse> {
161
- return searchTavily({
162
- query: params.query,
163
- num_results: params.numSearchResults ?? params.limit,
164
- recency: params.recency,
165
- signal: params.signal,
166
- });
171
+ return searchTavily(params);
167
172
  }
168
173
  }
@@ -1,5 +1,4 @@
1
- import { getAgentDbPath } from "@oh-my-pi/pi-utils";
2
- import { AgentStorage } from "../../../session/agent-storage";
1
+ import type { AgentStorage } from "../../../session/agent-storage";
3
2
  import { SearchProviderError, type SearchProviderId, type SearchSource } from "../../../web/search/types";
4
3
  import { dateToAgeSeconds } from "../utils";
5
4
 
@@ -7,17 +6,24 @@ import { dateToAgeSeconds } from "../utils";
7
6
  * Search for an API credential by checking an env-derived key first,
8
7
  * then falling back to agent.db stored credentials for the given providers.
9
8
  *
9
+ * The caller MUST supply an open {@link AgentStorage} handle so the helper
10
+ * never reaches out to global filesystem state; both the unified web_search
11
+ * chain and one-shot CLI calls open storage exactly once and thread it
12
+ * through every provider.
13
+ *
14
+ * @param storage - Open agent storage handle
10
15
  * @param envKey - Pre-resolved environment variable value (or null)
11
16
  * @param storageProviders - Provider names to look up in AgentStorage
12
17
  */
13
- export async function findCredential(
18
+ export function findCredential(
19
+ storage: AgentStorage | null | undefined,
14
20
  envKey: string | null | undefined,
15
21
  ...storageProviders: string[]
16
- ): Promise<string | null> {
22
+ ): string | null {
17
23
  if (envKey) return envKey;
24
+ if (!storage) return null;
18
25
 
19
26
  try {
20
- const storage = await AgentStorage.open(getAgentDbPath());
21
27
  for (const provider of storageProviders) {
22
28
  const records = storage.listAuthCredentials(provider);
23
29
  for (const record of records) {
@@ -37,18 +43,6 @@ export async function findCredential(
37
43
  return null;
38
44
  }
39
45
 
40
- /**
41
- * Probe whether a provider's API key lookup resolves to a truthy value.
42
- * Swallows lookup errors and reports unavailability.
43
- */
44
- export async function isApiKeyAvailable(findApiKey: () => string | null | Promise<string | null>) {
45
- try {
46
- return !!(await findApiKey());
47
- } catch {
48
- return false;
49
- }
50
- }
51
-
52
46
  /**
53
47
  * Default hard ceiling for a single web-search round-trip. 60s tolerates
54
48
  * legitimate slow LLM-mediated responses (anthropic web_search_20250305,
@@ -4,14 +4,14 @@
4
4
  * Calls Z.AI's remote MCP server (`webSearchPrime`) and adapts results into
5
5
  * the unified SearchResponse shape used by the web search tool.
6
6
  */
7
- import { getEnvApiKey } from "@oh-my-pi/pi-ai";
7
+ import { type AuthStorage, getEnvApiKey } from "@oh-my-pi/pi-ai";
8
8
  import { asRecord, asString } from "../../../web/scrapers/utils";
9
9
  import type { SearchResponse, SearchSource } from "../../../web/search/types";
10
10
  import { SearchProviderError } from "../../../web/search/types";
11
11
  import { dateToAgeSeconds } from "../utils";
12
12
  import type { SearchParams } from "./base";
13
13
  import { SearchProvider } from "./base";
14
- import { classifyProviderHttpError, findCredential, isApiKeyAvailable, withHardTimeout } from "./utils";
14
+ import { classifyProviderHttpError, withHardTimeout } from "./utils";
15
15
 
16
16
  const ZAI_MCP_URL = "https://api.z.ai/api/mcp/web_search_prime/mcp";
17
17
  const ZAI_TOOL_NAME = "web_search_prime";
@@ -21,6 +21,8 @@ export interface ZaiSearchParams {
21
21
  query: string;
22
22
  num_results?: number;
23
23
  signal?: AbortSignal;
24
+ authStorage: AuthStorage;
25
+ sessionId?: string;
24
26
  }
25
27
 
26
28
  interface ZaiSearchResult {
@@ -51,9 +53,13 @@ interface JsonRpcPayload {
51
53
  error?: JsonRpcError;
52
54
  }
53
55
 
54
- /** Find Z.AI API credentials from environment or saved auth storage. */
55
- export async function findApiKey(): Promise<string | null> {
56
- return findCredential(getEnvApiKey("zai"), "zai");
56
+ /** Resolve Z.AI API credentials through the unified auth storage pipeline. */
57
+ export async function findApiKey(
58
+ authStorage: AuthStorage,
59
+ sessionId?: string,
60
+ signal?: AbortSignal,
61
+ ): Promise<string | null> {
62
+ return (await authStorage.getApiKey("zai", sessionId, { signal })) ?? null;
57
63
  }
58
64
 
59
65
  async function callZaiTool(apiKey: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<unknown> {
@@ -272,7 +278,7 @@ function toSources(results: ZaiSearchResult[]): SearchSource[] {
272
278
 
273
279
  /** Execute Z.AI web search via remote MCP endpoint. */
274
280
  export async function searchZai(params: ZaiSearchParams): Promise<SearchResponse> {
275
- const apiKey = await findApiKey();
281
+ const apiKey = await findApiKey(params.authStorage, params.sessionId, params.signal);
276
282
  if (!apiKey) {
277
283
  throw new Error("Z.AI credentials not found. Set ZAI_API_KEY or login with 'omp /login zai'.");
278
284
  }
@@ -298,8 +304,8 @@ export class ZaiProvider extends SearchProvider {
298
304
  readonly id = "zai";
299
305
  readonly label = "Z.AI";
300
306
 
301
- isAvailable(): Promise<boolean> {
302
- return isApiKeyAvailable(findApiKey);
307
+ isAvailable(authStorage: AuthStorage): Promise<boolean> | boolean {
308
+ return authStorage.hasAuth("zai") || !!getEnvApiKey("zai");
303
309
  }
304
310
 
305
311
  search(params: SearchParams): Promise<SearchResponse> {
@@ -307,6 +313,8 @@ export class ZaiProvider extends SearchProvider {
307
313
  query: params.query,
308
314
  num_results: params.numSearchResults ?? params.limit,
309
315
  signal: params.signal,
316
+ authStorage: params.authStorage,
317
+ sessionId: params.sessionId,
310
318
  });
311
319
  }
312
320
  }
@@ -1,7 +0,0 @@
1
- import type { HashlineCursor, HashlineEdit } from "./types";
2
- export declare function cloneCursor(cursor: HashlineCursor): HashlineCursor;
3
- export declare function parseHashline(diff: string): HashlineEdit[];
4
- export declare function parseHashlineWithWarnings(diff: string): {
5
- edits: HashlineEdit[];
6
- warnings: string[];
7
- };
@@ -1,7 +0,0 @@
1
- /**
2
- * Back-compat re-export layer.
3
- * All types and functions have moved to src/tool-discovery/tool-index.ts.
4
- * This file exists solely so existing imports continue to compile without changes.
5
- */
6
- export type { DiscoverableMCPSearchDocument, DiscoverableMCPSearchIndex, DiscoverableMCPSearchResult, DiscoverableMCPTool, DiscoverableMCPToolServerSummary, DiscoverableMCPToolSummary, } from "../tool-discovery/tool-index";
7
- export { buildDiscoverableMCPSearchIndex, collectDiscoverableMCPTools, formatDiscoverableMCPToolServerSummary, getDiscoverableMCPTool, isMCPToolName, searchDiscoverableMCPTools, selectDiscoverableMCPToolNamesByServer, summarizeDiscoverableMCPTools, } from "../tool-discovery/tool-index";