opencode-search-plugin 2.1.1 → 3.0.0

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.
@@ -122,8 +122,12 @@ type SearchResult = ReturnType<typeof toResult>
122
122
  //
123
123
  // QUERIT_CACHE_TTL ms to reuse a response for the same query (0 disables)
124
124
  // QUERIT_COUNT results per search; Querit's own limit applies
125
- // QUERIT_API_KEY required; from https://querit.ai
125
+ // QUERIT_TIMEOUT ms to wait before failing (0 removes the deadline)
126
+ // OSP_QUERIT_API_KEY required; from https://querit.ai
126
127
  const CACHE_TTL = envInt("QUERIT_CACHE_TTL", 600_000)
128
+ // OpenCode's own providers each bounded themselves at 25s, and the host adds
129
+ // no deadline of its own around `execute`, so this is the only one that exists.
130
+ const SEARCH_TIMEOUT = envInt("QUERIT_TIMEOUT", 25_000)
127
131
  const SEARCH_SIZE = envInt("QUERIT_COUNT", 8)
128
132
 
129
133
  const CACHE_LIMIT = 200
@@ -148,6 +152,14 @@ function writeCache(queryKey: string, results: SearchResult[]): void {
148
152
  }
149
153
  }
150
154
 
155
+ // A hung upstream must not hang the agent: the deadline bounds it, and the
156
+ // thunk's signal carries host cancellation into the socket. `timeout <= 0`
157
+ // removes the deadline but never the cancellation, because
158
+ // `AbortSignal.timeout(0)` aborts instantly rather than "never".
159
+ function timedSignal(signal: AbortSignal, timeout: number): AbortSignal {
160
+ return timeout > 0 ? AbortSignal.any([signal, AbortSignal.timeout(timeout)]) : signal
161
+ }
162
+
151
163
  export const effect = (ctx: Plugin.Context) =>
152
164
  Effect.gen(function* () {
153
165
  // Make "Querit" show up in /connect with an API-key method.
@@ -161,7 +173,7 @@ export const effect = (ctx: Plugin.Context) =>
161
173
  })
162
174
  editor.method.update({
163
175
  integrationID: ID,
164
- method: { type: "env", names: ["QUERIT_API_KEY"] },
176
+ method: { type: "env", names: ["OSP_QUERIT_API_KEY"] },
165
177
  })
166
178
  })
167
179
 
@@ -181,7 +193,7 @@ export const effect = (ctx: Plugin.Context) =>
181
193
  const credential = connection
182
194
  ? yield* soften(ctx.integration.connection.resolve(connection))
183
195
  : undefined
184
- return credential?.type === "key" ? credential.key : process.env.QUERIT_API_KEY
196
+ return credential?.type === "key" ? credential.key : process.env.OSP_QUERIT_API_KEY
185
197
  })
186
198
 
187
199
  // Querit is bound to websearch only while a key exists. Deleting the key
@@ -205,12 +217,12 @@ export const effect = (ctx: Plugin.Context) =>
205
217
  const key = yield* resolveKey()
206
218
  if (!key) {
207
219
  return yield* Effect.fail(
208
- new Error("Connect Querit in /connect, or set QUERIT_API_KEY"),
220
+ new Error("Connect Querit in /connect, or set OSP_QUERIT_API_KEY"),
209
221
  )
210
222
  }
211
223
 
212
224
  const response = yield* Effect.tryPromise({
213
- try: () =>
225
+ try: (signal) =>
214
226
  fetch("https://api.querit.ai/v1/search", {
215
227
  method: "POST",
216
228
  headers: {
@@ -219,8 +231,14 @@ export const effect = (ctx: Plugin.Context) =>
219
231
  Authorization: "Bearer " + key,
220
232
  },
221
233
  body: JSON.stringify({ query: queryKey, count: SEARCH_SIZE }),
234
+ signal: timedSignal(signal, SEARCH_TIMEOUT),
222
235
  }),
223
- catch: (error) => new Error("Querit request failed: " + String(error)),
236
+ // A timeout is its own failure: naming it stops the user hunting
237
+ // for a key problem that was never there.
238
+ catch: (error) =>
239
+ error instanceof Error && error.name === "TimeoutError"
240
+ ? new Error("Querit search timed out after " + SEARCH_TIMEOUT + "ms")
241
+ : new Error("Querit request failed: " + String(error)),
224
242
  })
225
243
 
226
244
  if (!response.ok) {
@@ -111,11 +111,15 @@ type SearchResult = ReturnType<typeof toResult>
111
111
  // SERPAPI_GL country code for results, e.g. "us", "cn"
112
112
  // SERPAPI_HL language code for results, e.g. "en", "zh-cn"
113
113
  // SERPAPI_TBS advanced filters, e.g. "qdr:d" (past day), "qdr:y"
114
- // SERPAPI_API_KEY required; from https://serpapi.com
114
+ // SERPAPI_TIMEOUT ms to wait before failing (0 removes the deadline)
115
+ // OSP_SERPAPI_API_KEY required; from https://serpapi.com
115
116
  //
116
117
  // There is deliberately no result-count knob: Google withdrew the `num`
117
118
  // parameter, so SerpApi cannot honour one and a count would be a lie.
118
119
  const CACHE_TTL = envInt("SERPAPI_CACHE_TTL", 600_000)
120
+ // OpenCode's own providers each bounded themselves at 25s, and the host adds
121
+ // no deadline of its own around `execute`, so this is the only one that exists.
122
+ const SEARCH_TIMEOUT = envInt("SERPAPI_TIMEOUT", 25_000)
119
123
  const GL = envStr("SERPAPI_GL")
120
124
  const HL = envStr("SERPAPI_HL")
121
125
  const TBS = envStr("SERPAPI_TBS")
@@ -142,6 +146,14 @@ function writeCache(queryKey: string, results: SearchResult[]): void {
142
146
  }
143
147
  }
144
148
 
149
+ // A hung upstream must not hang the agent: the deadline bounds it, and the
150
+ // thunk's signal carries host cancellation into the socket. `timeout <= 0`
151
+ // removes the deadline but never the cancellation, because
152
+ // `AbortSignal.timeout(0)` aborts instantly rather than "never".
153
+ function timedSignal(signal: AbortSignal, timeout: number): AbortSignal {
154
+ return timeout > 0 ? AbortSignal.any([signal, AbortSignal.timeout(timeout)]) : signal
155
+ }
156
+
145
157
  export const effect = (ctx: Plugin.Context) =>
146
158
  Effect.gen(function* () {
147
159
  // Make "SerpApi" show up in /connect with an API-key method.
@@ -155,7 +167,7 @@ export const effect = (ctx: Plugin.Context) =>
155
167
  })
156
168
  editor.method.update({
157
169
  integrationID: ID,
158
- method: { type: "env", names: ["SERPAPI_API_KEY"] },
170
+ method: { type: "env", names: ["OSP_SERPAPI_API_KEY"] },
159
171
  })
160
172
  })
161
173
 
@@ -175,7 +187,7 @@ export const effect = (ctx: Plugin.Context) =>
175
187
  const credential = connection
176
188
  ? yield* soften(ctx.integration.connection.resolve(connection))
177
189
  : undefined
178
- return credential?.type === "key" ? credential.key : process.env.SERPAPI_API_KEY
190
+ return credential?.type === "key" ? credential.key : process.env.OSP_SERPAPI_API_KEY
179
191
  })
180
192
 
181
193
  // SerpApi is bound to websearch only while a key exists. Deleting the key
@@ -199,7 +211,7 @@ export const effect = (ctx: Plugin.Context) =>
199
211
  const key = yield* resolveKey()
200
212
  if (!key) {
201
213
  return yield* Effect.fail(
202
- new Error("Connect SerpApi in /connect, or set SERPAPI_API_KEY"),
214
+ new Error("Connect SerpApi in /connect, or set OSP_SERPAPI_API_KEY"),
203
215
  )
204
216
  }
205
217
 
@@ -212,13 +224,19 @@ export const effect = (ctx: Plugin.Context) =>
212
224
  if (TBS) url.searchParams.set("tbs", TBS)
213
225
 
214
226
  const response = yield* Effect.tryPromise({
215
- try: () =>
227
+ try: (signal) =>
216
228
  fetch(url, {
217
229
  headers: {
218
230
  Accept: "application/json",
219
231
  },
232
+ signal: timedSignal(signal, SEARCH_TIMEOUT),
220
233
  }),
221
- catch: (error) => new Error("SerpApi request failed: " + String(error)),
234
+ // A timeout is its own failure: naming it stops the user hunting
235
+ // for a key problem that was never there.
236
+ catch: (error) =>
237
+ error instanceof Error && error.name === "TimeoutError"
238
+ ? new Error("SerpApi search timed out after " + SEARCH_TIMEOUT + "ms")
239
+ : new Error("SerpApi request failed: " + String(error)),
222
240
  })
223
241
 
224
242
  if (!response.ok) {
@@ -112,8 +112,12 @@ type SearchResult = ReturnType<typeof toResult>
112
112
  // SERPER_HL language code for results, e.g. "en", "zh-cn"
113
113
  // SERPER_TBS time filter: "qdr:h" (hour) "qdr:d" (day) "qdr:w" (week)
114
114
  // "qdr:m" (month) "qdr:y" (year)
115
- // SERPER_API_KEY required; from https://serper.dev
115
+ // SERPER_TIMEOUT ms to wait before failing (0 removes the deadline)
116
+ // OSP_SERPER_API_KEY required; from https://serper.dev
116
117
  const CACHE_TTL = envInt("SERPER_CACHE_TTL", 600_000)
118
+ // OpenCode's own providers each bounded themselves at 25s, and the host adds
119
+ // no deadline of its own around `execute`, so this is the only one that exists.
120
+ const SEARCH_TIMEOUT = envInt("SERPER_TIMEOUT", 25_000)
117
121
  const SEARCH_SIZE = Math.min(Math.max(envInt("SERPER_NUM", 10), 1), 100)
118
122
  const GL = envStr("SERPER_GL")
119
123
  const HL = envStr("SERPER_HL")
@@ -141,6 +145,14 @@ function writeCache(queryKey: string, results: SearchResult[]): void {
141
145
  }
142
146
  }
143
147
 
148
+ // A hung upstream must not hang the agent: the deadline bounds it, and the
149
+ // thunk's signal carries host cancellation into the socket. `timeout <= 0`
150
+ // removes the deadline but never the cancellation, because
151
+ // `AbortSignal.timeout(0)` aborts instantly rather than "never".
152
+ function timedSignal(signal: AbortSignal, timeout: number): AbortSignal {
153
+ return timeout > 0 ? AbortSignal.any([signal, AbortSignal.timeout(timeout)]) : signal
154
+ }
155
+
144
156
  export const effect = (ctx: Plugin.Context) =>
145
157
  Effect.gen(function* () {
146
158
  // Make "Serper" show up in /connect with an API-key method.
@@ -154,7 +166,7 @@ export const effect = (ctx: Plugin.Context) =>
154
166
  })
155
167
  editor.method.update({
156
168
  integrationID: ID,
157
- method: { type: "env", names: ["SERPER_API_KEY"] },
169
+ method: { type: "env", names: ["OSP_SERPER_API_KEY"] },
158
170
  })
159
171
  })
160
172
 
@@ -174,7 +186,7 @@ export const effect = (ctx: Plugin.Context) =>
174
186
  const credential = connection
175
187
  ? yield* soften(ctx.integration.connection.resolve(connection))
176
188
  : undefined
177
- return credential?.type === "key" ? credential.key : process.env.SERPER_API_KEY
189
+ return credential?.type === "key" ? credential.key : process.env.OSP_SERPER_API_KEY
178
190
  })
179
191
 
180
192
  // Serper is bound to websearch only while a key exists. Deleting the key
@@ -198,12 +210,12 @@ export const effect = (ctx: Plugin.Context) =>
198
210
  const key = yield* resolveKey()
199
211
  if (!key) {
200
212
  return yield* Effect.fail(
201
- new Error("Connect Serper in /connect, or set SERPER_API_KEY"),
213
+ new Error("Connect Serper in /connect, or set OSP_SERPER_API_KEY"),
202
214
  )
203
215
  }
204
216
 
205
217
  const response = yield* Effect.tryPromise({
206
- try: () =>
218
+ try: (signal) =>
207
219
  fetch("https://google.serper.dev/search", {
208
220
  method: "POST",
209
221
  headers: {
@@ -218,8 +230,14 @@ export const effect = (ctx: Plugin.Context) =>
218
230
  ...(HL ? { hl: HL } : {}),
219
231
  ...(TBS ? { tbs: TBS } : {}),
220
232
  }),
233
+ signal: timedSignal(signal, SEARCH_TIMEOUT),
221
234
  }),
222
- catch: (error) => new Error("Serper request failed: " + String(error)),
235
+ // A timeout is its own failure: naming it stops the user hunting
236
+ // for a key problem that was never there.
237
+ catch: (error) =>
238
+ error instanceof Error && error.name === "TimeoutError"
239
+ ? new Error("Serper search timed out after " + SEARCH_TIMEOUT + "ms")
240
+ : new Error("Serper request failed: " + String(error)),
223
241
  })
224
242
 
225
243
  if (!response.ok) {
@@ -140,8 +140,12 @@ type SearchResult = ReturnType<typeof toResult>
140
140
  // YOUCOM_LANGUAGE result language, e.g. "EN", "ZH-HANS"
141
141
  // YOUCOM_COUNTRY geographic focus, e.g. "US", "CN"
142
142
  // YOUCOM_FRESHNESS "day" | "week" | "month" | "year" or "YYYY-MM-DDtoYYYY-MM-DD"
143
- // YDC_API_KEY required; You.com's canonical env var, shared with its SDKs
143
+ // YOUCOM_TIMEOUT ms to wait before failing (0 removes the deadline)
144
+ // OSP_YDC_API_KEY required; You.com's canonical env var, shared with its SDKs
144
145
  const CACHE_TTL = envInt("YOUCOM_CACHE_TTL", 600_000)
146
+ // OpenCode's own providers each bounded themselves at 25s, and the host adds
147
+ // no deadline of its own around `execute`, so this is the only one that exists.
148
+ const SEARCH_TIMEOUT = envInt("YOUCOM_TIMEOUT", 25_000)
145
149
  const SEARCH_SIZE = Math.min(Math.max(envInt("YOUCOM_COUNT", 10), 1), 100)
146
150
  const EXTRACTION = ["none", "highlights", "full_page"].includes(envStr("YOUCOM_EXTRACTION"))
147
151
  ? envStr("YOUCOM_EXTRACTION")
@@ -172,6 +176,14 @@ function writeCache(queryKey: string, results: SearchResult[]): void {
172
176
  }
173
177
  }
174
178
 
179
+ // A hung upstream must not hang the agent: the deadline bounds it, and the
180
+ // thunk's signal carries host cancellation into the socket. `timeout <= 0`
181
+ // removes the deadline but never the cancellation, because
182
+ // `AbortSignal.timeout(0)` aborts instantly rather than "never".
183
+ function timedSignal(signal: AbortSignal, timeout: number): AbortSignal {
184
+ return timeout > 0 ? AbortSignal.any([signal, AbortSignal.timeout(timeout)]) : signal
185
+ }
186
+
175
187
  export const effect = (ctx: Plugin.Context) =>
176
188
  Effect.gen(function* () {
177
189
  // Make "You.com" show up in /connect with an API-key method.
@@ -185,7 +197,7 @@ export const effect = (ctx: Plugin.Context) =>
185
197
  })
186
198
  editor.method.update({
187
199
  integrationID: ID,
188
- method: { type: "env", names: ["YDC_API_KEY"] },
200
+ method: { type: "env", names: ["OSP_YDC_API_KEY"] },
189
201
  })
190
202
  })
191
203
 
@@ -205,7 +217,7 @@ export const effect = (ctx: Plugin.Context) =>
205
217
  const credential = connection
206
218
  ? yield* soften(ctx.integration.connection.resolve(connection))
207
219
  : undefined
208
- return credential?.type === "key" ? credential.key : process.env.YDC_API_KEY
220
+ return credential?.type === "key" ? credential.key : process.env.OSP_YDC_API_KEY
209
221
  })
210
222
 
211
223
  // You.com is bound to websearch only while a key exists. Deleting the key
@@ -229,7 +241,7 @@ export const effect = (ctx: Plugin.Context) =>
229
241
  const key = yield* resolveKey()
230
242
  if (!key) {
231
243
  return yield* Effect.fail(
232
- new Error("Connect You.com in /connect, or set YDC_API_KEY"),
244
+ new Error("Connect You.com in /connect, or set OSP_YDC_API_KEY"),
233
245
  )
234
246
  }
235
247
 
@@ -244,7 +256,7 @@ export const effect = (ctx: Plugin.Context) =>
244
256
  : undefined
245
257
 
246
258
  const response = yield* Effect.tryPromise({
247
- try: () =>
259
+ try: (signal) =>
248
260
  fetch("https://ydc-index.io/v1/search", {
249
261
  method: "POST",
250
262
  headers: {
@@ -260,8 +272,14 @@ export const effect = (ctx: Plugin.Context) =>
260
272
  ...(FRESHNESS ? { freshness: FRESHNESS } : {}),
261
273
  ...(extraction ? { extraction } : {}),
262
274
  }),
275
+ signal: timedSignal(signal, SEARCH_TIMEOUT),
263
276
  }),
264
- catch: (error) => new Error("You.com request failed: " + String(error)),
277
+ // A timeout is its own failure: naming it stops the user hunting
278
+ // for a key problem that was never there.
279
+ catch: (error) =>
280
+ error instanceof Error && error.name === "TimeoutError"
281
+ ? new Error("You.com search timed out after " + SEARCH_TIMEOUT + "ms")
282
+ : new Error("You.com request failed: " + String(error)),
265
283
  })
266
284
 
267
285
  if (!response.ok) {