opencode-websearch-deepseek 0.3.1 → 0.3.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.
package/dist/index.d.ts CHANGED
@@ -131,7 +131,7 @@ export interface WebsearchContext {
131
131
  * variables, then the credential OpenCode stores for the `deepseek` provider
132
132
  * (configured via `opencode auth login`).
133
133
  */
134
- export declare function resolveApiKey(ctx: WebsearchContext, options?: WebsearchOptions): Promise<string | undefined>;
134
+ export declare function resolveApiKey(ctx: WebsearchContext, options?: WebsearchOptions, signal?: AbortSignal): Promise<string | undefined>;
135
135
  /**
136
136
  * Resolve the `max_uses` value for the search tool declaration.
137
137
  * Falls back to {@link DEFAULT_MAX_USES} for missing or invalid input.
package/dist/index.js CHANGED
@@ -56,7 +56,7 @@ function readString(value) {
56
56
  * variables, then the credential OpenCode stores for the `deepseek` provider
57
57
  * (configured via `opencode auth login`).
58
58
  */
59
- export async function resolveApiKey(ctx, options) {
59
+ export async function resolveApiKey(ctx, options, signal) {
60
60
  const fromOptions = readString(options?.apiKey);
61
61
  if (fromOptions)
62
62
  return fromOptions;
@@ -67,13 +67,15 @@ export async function resolveApiKey(ctx, options) {
67
67
  if (!integration)
68
68
  return undefined;
69
69
  try {
70
- const connection = await integration.connection.active("deepseek");
70
+ const connection = await withAbort(integration.connection.active("deepseek"), signal);
71
71
  if (!connection)
72
72
  return undefined;
73
- const credential = await integration.connection.resolve(connection);
73
+ const credential = await withAbort(integration.connection.resolve(connection), signal);
74
74
  return readString(credential?.key);
75
75
  }
76
76
  catch (error) {
77
+ if (signal?.aborted)
78
+ throwCancellation(signal, error);
77
79
  // A genuine integration failure is not "no key": surface it with context.
78
80
  throw new Error("Failed to read the DeepSeek credential from OpenCode", { cause: error });
79
81
  }
@@ -93,21 +95,52 @@ function throwCancellation(signal, fallback) {
93
95
  if (reason instanceof Error)
94
96
  throw reason;
95
97
  if (typeof reason === "object" && reason !== null) {
96
- // Preserve AbortError-like objects (DOMException has a message); wrap the rest.
97
- const candidate = reason;
98
- if (candidate.name === "AbortError" && typeof candidate.message === "string")
99
- throw reason;
98
+ // Preserve AbortError-like objects (DOMException has a message); wrap the
99
+ // rest. Guard the property reads: a hostile reason may throw from a getter.
100
+ try {
101
+ const candidate = reason;
102
+ if (candidate.name === "AbortError" && typeof candidate.message === "string" && candidate.message) {
103
+ throw reason;
104
+ }
105
+ }
106
+ catch (error) {
107
+ if (error === reason)
108
+ throw error;
109
+ }
100
110
  throw new Error("The web search was aborted", { cause: reason });
101
111
  }
112
+ if (typeof reason === "string" && reason.trim())
113
+ throw new Error(reason.trim());
102
114
  if (reason !== undefined && reason !== null) {
103
- const text = String(reason).trim();
104
- if (text)
105
- throw new Error(text);
115
+ // Non-string primitives (e.g. 0) are not useful messages; keep them as cause.
116
+ throw new Error("The web search was aborted", { cause: reason });
106
117
  }
107
118
  if (fallback instanceof Error)
108
119
  throw fallback;
109
120
  throw new Error("The web search was aborted");
110
121
  }
122
+ /** Reject with the signal's reason if `signal` aborts before `promise` settles. */
123
+ function withAbort(promise, signal) {
124
+ if (!signal)
125
+ return promise;
126
+ if (signal.aborted) {
127
+ // The passed promise is already created; swallow its later rejection so it
128
+ // does not surface as an unhandled rejection.
129
+ promise.catch(() => { });
130
+ return Promise.reject(signal.reason ?? new Error("The web search was aborted"));
131
+ }
132
+ return new Promise((resolve, reject) => {
133
+ const onAbort = () => reject(signal.reason ?? new Error("The web search was aborted"));
134
+ signal.addEventListener("abort", onAbort, { once: true });
135
+ promise.then((value) => {
136
+ signal.removeEventListener("abort", onAbort);
137
+ resolve(value);
138
+ }, (error) => {
139
+ signal.removeEventListener("abort", onAbort);
140
+ reject(error);
141
+ });
142
+ });
143
+ }
111
144
  /**
112
145
  * Validate a `max_uses` value: a positive safe integer, given as a number or a
113
146
  * plain-integer string. Returns `undefined` when unset or invalid.
@@ -243,6 +276,8 @@ export function toResults(answer, sources = []) {
243
276
  const valid = (Array.isArray(sources) ? sources : []).filter((source) => typeof source?.url === "string" && source.url.length > 0);
244
277
  const results = [];
245
278
  if (answer) {
279
+ // The synthesized answer reuses the first source's URL so it links to a
280
+ // real page; the same source is also listed below (intentional).
246
281
  results.push({
247
282
  url: valid[0]?.url ?? FALLBACK_URL,
248
283
  title: "DeepSeek answer",
@@ -280,7 +315,7 @@ export const plugin = {
280
315
  execute: async ({ query }, context) => {
281
316
  const signal = context?.signal;
282
317
  try {
283
- const apiKey = await resolveApiKey(ctx, options);
318
+ const apiKey = await resolveApiKey(ctx, options, signal);
284
319
  if (!apiKey) {
285
320
  throw new Error("No DeepSeek API key: set the DEEPSEEK_API_KEY env var, pass the apiKey plugin option, or sign in to the deepseek provider in OpenCode");
286
321
  }
@@ -320,6 +355,9 @@ export const plugin = {
320
355
  throw new Error(`DeepSeek API error ${response.status}: ${text.slice(0, 300)}`);
321
356
  }
322
357
  const data = (await response.json());
358
+ // A cancellation may arrive after the response completed.
359
+ if (signal?.aborted)
360
+ throwCancellation(signal, undefined);
323
361
  const { answer, sources } = extractAnswerAndSources(data);
324
362
  return toResults(answer, sources);
325
363
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "opencode-websearch-deepseek",
4
- "version": "0.3.1",
4
+ "version": "0.3.2",
5
5
  "description": "DeepSeek-powered web search provider for OpenCode's built-in websearch tool.",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -44,7 +44,7 @@
44
44
  "build": "tsc -p tsconfig.json",
45
45
  "typecheck": "tsc -p tsconfig.json --noEmit",
46
46
  "test": "npm run build && node --test",
47
- "prepare": "node -e \"try{require.resolve('typescript')}catch{process.exit(0)};require('node:child_process').execSync('npm run build',{stdio:'inherit'})\"",
47
+ "prepare": "node -e \"const fs=require('node:fs');if(fs.existsSync('dist/index.js'))process.exit(0);try{require.resolve('typescript')}catch{process.exit(0)};require('node:child_process').execSync('npm run build',{stdio:'inherit'})\"",
48
48
  "prepack": "npm run build",
49
49
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
50
50
  },