ubersearch 1.7.0 → 1.7.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.
@@ -7,9 +7,10 @@
7
7
  * the overall search.
8
8
  */
9
9
 
10
- import { withRetry } from "../../providers/retry";
10
+ import { getErrorMessage } from "../errorUtils";
11
11
  import { createLogger } from "../logger";
12
12
  import type { SearchProvider } from "../provider";
13
+ import { withRetry } from "../retry";
13
14
  import type { EngineId, SearchResponse, SearchResultItem } from "../types";
14
15
  import { SearchError } from "../types";
15
16
  import type {
@@ -28,7 +29,7 @@ const log = createLogger("AllProviders");
28
29
  interface EngineSearchResult {
29
30
  engineId: EngineId;
30
31
  response?: SearchResponse;
31
- error?: Error;
32
+ error?: unknown;
32
33
  }
33
34
 
34
35
  /**
@@ -101,12 +102,7 @@ export class AllProvidersStrategy implements ISearchStrategy {
101
102
  // Record success
102
103
  attempts.push({ engineId, success: true });
103
104
 
104
- // Add results, applying limit if specified
105
- if (options.limit !== undefined) {
106
- results.push(...response.items.slice(0, options.limit - results.length));
107
- } else {
108
- results.push(...response.items);
109
- }
105
+ results.push(...response.items);
110
106
  } catch (error) {
111
107
  // Refund credits for failed search
112
108
  context.creditManager.refund(engineId);
@@ -119,18 +115,11 @@ export class AllProvidersStrategy implements ISearchStrategy {
119
115
  }
120
116
 
121
117
  // Log debug message but continue with other providers
122
- log.debug(
123
- `Search failed for ${engineId}: ${error instanceof Error ? error.message : String(error)}`,
124
- );
118
+ log.debug(`Search failed for ${engineId}: ${getErrorMessage(error)}`);
125
119
  }
126
120
  }
127
121
 
128
- // Apply final limit if specified
129
- if (options.limit !== undefined && results.length > options.limit) {
130
- results.splice(options.limit);
131
- }
132
-
133
- return { results, attempts };
122
+ return { results: this.finalizeResults(results, options), attempts };
134
123
  }
135
124
 
136
125
  /**
@@ -182,7 +171,7 @@ export class AllProvidersStrategy implements ISearchStrategy {
182
171
  );
183
172
  return { engineId, response };
184
173
  } catch (error) {
185
- return { engineId, error: error instanceof Error ? error : new Error(String(error)) };
174
+ return { engineId, error };
186
175
  }
187
176
  },
188
177
  );
@@ -204,27 +193,34 @@ export class AllProvidersStrategy implements ISearchStrategy {
204
193
 
205
194
  if (settledResult.status === "rejected") {
206
195
  // Promise itself was rejected (shouldn't happen with our try/catch, but handle it)
207
- attempts.push({ engineId, success: false, reason: "unknown" });
196
+ context.creditManager.refund(engineId);
197
+ const error = settledResult.reason;
198
+ if (error instanceof SearchError) {
199
+ attempts.push({ engineId, success: false, reason: error.reason });
200
+ } else {
201
+ attempts.push({ engineId, success: false, reason: "unknown" });
202
+ }
208
203
  log.debug(`Search failed for ${engineId}: Promise rejected`);
209
204
  continue;
210
205
  }
211
206
 
212
- const { response, error } = settledResult.value;
213
-
214
- if (error) {
207
+ if ("error" in settledResult.value) {
215
208
  // Refund credits for failed search
216
209
  context.creditManager.refund(engineId);
217
210
 
218
211
  // Search threw an error
212
+ const error = settledResult.value.error;
219
213
  if (error instanceof SearchError) {
220
214
  attempts.push({ engineId, success: false, reason: error.reason });
221
215
  } else {
222
216
  attempts.push({ engineId, success: false, reason: "unknown" });
223
217
  }
224
- log.debug(`Search failed for ${engineId}: ${error.message}`);
218
+ log.debug(`Search failed for ${engineId}: ${getErrorMessage(error)}`);
225
219
  continue;
226
220
  }
227
221
 
222
+ const { response } = settledResult.value;
223
+
228
224
  if (response) {
229
225
  // Record success
230
226
  attempts.push({ engineId, success: true });
@@ -243,11 +239,31 @@ export class AllProvidersStrategy implements ISearchStrategy {
243
239
  (a, b) => (engineOrder.get(a.engineId) ?? 0) - (engineOrder.get(b.engineId) ?? 0),
244
240
  );
245
241
 
246
- // Apply final limit if specified
247
- if (options.limit !== undefined && results.length > options.limit) {
248
- results.splice(options.limit);
242
+ return { results: this.finalizeResults(results, options), attempts };
243
+ }
244
+
245
+ /**
246
+ * Deduplicate results by URL, sort by score, and apply limit.
247
+ */
248
+ private finalizeResults(
249
+ results: SearchResultItem[],
250
+ options: UberSearchOptions,
251
+ ): SearchResultItem[] {
252
+ const seen = new Set<string>();
253
+ const deduped: SearchResultItem[] = [];
254
+ for (const item of results) {
255
+ if (!seen.has(item.url)) {
256
+ seen.add(item.url);
257
+ deduped.push(item);
258
+ }
259
+ }
260
+
261
+ deduped.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
262
+
263
+ if (options.limit !== undefined && deduped.length > options.limit) {
264
+ deduped.splice(options.limit);
249
265
  }
250
266
 
251
- return { results, attempts };
267
+ return deduped;
252
268
  }
253
269
  }
@@ -6,8 +6,9 @@
6
6
  * with insufficient credits and stops execution after the first success.
7
7
  */
8
8
 
9
- import { withRetry } from "../../providers/retry";
9
+ import { getErrorMessage } from "../errorUtils";
10
10
  import { createLogger } from "../logger";
11
+ import { withRetry } from "../retry";
11
12
  import type { EngineId } from "../types";
12
13
  import { SearchError } from "../types";
13
14
  import type {
@@ -87,9 +88,7 @@ export class FirstSuccessStrategy implements ISearchStrategy {
87
88
  }
88
89
 
89
90
  // Log debug message and continue
90
- log.debug(
91
- `Search failed for ${engineId}: ${error instanceof Error ? error.message : String(error)}`,
92
- );
91
+ log.debug(`Search failed for ${engineId}: ${getErrorMessage(error)}`);
93
92
  }
94
93
  }
95
94
 
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { CreditManager } from "../credits";
9
9
  import type { ProviderRegistry } from "../provider";
10
- import type { EngineId, SearchResultItem } from "../types";
10
+ import type { EngineId, SearchFailureReason, SearchResultItem } from "../types";
11
11
 
12
12
  /**
13
13
  * Context object providing dependencies to search strategies
@@ -58,7 +58,7 @@ export interface EngineAttempt {
58
58
  success: boolean;
59
59
 
60
60
  /** Reason for failure (if success=false) */
61
- reason?: string;
61
+ reason?: SearchFailureReason;
62
62
  }
63
63
 
64
64
  /**
package/src/core/types.ts CHANGED
@@ -33,6 +33,7 @@ export type SearchFailureReason =
33
33
  | "rate_limit"
34
34
  | "no_results"
35
35
  | "low_credit"
36
+ | "out_of_credit"
36
37
  | "config_error"
37
38
  | "no_provider"
38
39
  | "provider_unavailable"
package/src/mcp-server.ts CHANGED
@@ -7,9 +7,10 @@
7
7
  */
8
8
 
9
9
  import { bootstrapContainer, getCreditStatus, uberSearch } from "./app/index";
10
+ import { getErrorMessage } from "./core/errorUtils";
10
11
  import type { ProviderRegistry } from "./core/provider";
11
12
  import { ServiceKeys } from "./core/serviceKeys";
12
- import { isLifecycleProvider } from "./plugin/types.js";
13
+ import { isLifecycleProvider } from "./plugin/types";
13
14
 
14
15
  interface MCPRequest {
15
16
  jsonrpc: string;
@@ -26,10 +27,10 @@ interface MCPTool {
26
27
 
27
28
  interface MCPResponse {
28
29
  jsonrpc: string;
29
- id?: number | string;
30
+ id?: number | string | null;
30
31
  result?: unknown;
31
32
  error?: {
32
- code?: number;
33
+ code: number;
33
34
  message: string;
34
35
  };
35
36
  }
@@ -40,6 +41,9 @@ interface HealthResult {
40
41
  message?: string;
41
42
  }
42
43
 
44
+ // Module-level container reference for shutdown handlers
45
+ let globalContainer: Awaited<ReturnType<typeof bootstrapContainer>> | null = null;
46
+
43
47
  // Helper function with timeout
44
48
  async function withTimeout<T>(promise: Promise<T>, ms: number, operation: string): Promise<T> {
45
49
  const timeoutPromise = new Promise<never>((_, reject) => {
@@ -48,8 +52,37 @@ async function withTimeout<T>(promise: Promise<T>, ms: number, operation: string
48
52
  return Promise.race([promise, timeoutPromise]);
49
53
  }
50
54
 
55
+ function setupShutdownHandlers() {
56
+ const shutdown = async () => {
57
+ try {
58
+ const container = globalContainer;
59
+ if (container) {
60
+ const registry = container.get<ProviderRegistry>(ServiceKeys.PROVIDER_REGISTRY);
61
+ if (registry) {
62
+ for (const provider of registry.list()) {
63
+ if (isLifecycleProvider(provider) && typeof provider.shutdown === "function") {
64
+ try {
65
+ await provider.shutdown();
66
+ } catch {
67
+ // Best-effort shutdown
68
+ }
69
+ }
70
+ }
71
+ }
72
+ }
73
+ } catch {
74
+ // Best-effort cleanup
75
+ }
76
+ process.exit(0);
77
+ };
78
+
79
+ process.on("SIGTERM", shutdown);
80
+ process.on("SIGINT", shutdown);
81
+ }
82
+
51
83
  // MCP Server entry point for Claude Desktop
52
84
  export async function serve() {
85
+ setupShutdownHandlers();
53
86
  const tools: MCPTool[] = [
54
87
  {
55
88
  name: "uber_search",
@@ -113,6 +146,8 @@ Example: "it,science" for tech and academic results`,
113
146
  ];
114
147
 
115
148
  // Handle MCP tool calls via stdin/stdout
149
+ const validToolNames = new Set(tools.map((t) => t.name));
150
+
116
151
  const readline = (await import("node:readline")).createInterface({
117
152
  input: process.stdin,
118
153
  output: process.stdout,
@@ -124,6 +159,15 @@ Example: "it,science" for tech and academic results`,
124
159
  try {
125
160
  request = JSON.parse(line);
126
161
  } catch {
162
+ const errorResponse: MCPResponse = {
163
+ jsonrpc: "2.0",
164
+ id: null,
165
+ error: {
166
+ code: -32700,
167
+ message: "Parse error: invalid JSON",
168
+ },
169
+ };
170
+ process.stdout.write(`${JSON.stringify(errorResponse)}\n`);
127
171
  continue;
128
172
  }
129
173
 
@@ -143,7 +187,7 @@ Example: "it,science" for tech and academic results`,
143
187
  },
144
188
  },
145
189
  };
146
- console.log(JSON.stringify(response));
190
+ process.stdout.write(`${JSON.stringify(response)}\n`);
147
191
  continue;
148
192
  }
149
193
 
@@ -158,7 +202,7 @@ Example: "it,science" for tech and academic results`,
158
202
  id: request.id,
159
203
  result: { tools },
160
204
  };
161
- console.log(JSON.stringify(response));
205
+ process.stdout.write(`${JSON.stringify(response)}\n`);
162
206
  continue;
163
207
  }
164
208
 
@@ -170,6 +214,48 @@ Example: "it,science" for tech and academic results`,
170
214
  ? (params.arguments as Record<string, string>)
171
215
  : ({} as Record<string, string>);
172
216
 
217
+ // Validate tool name
218
+ if (!validToolNames.has(name)) {
219
+ const response: MCPResponse = {
220
+ jsonrpc: "2.0",
221
+ id: request.id,
222
+ error: {
223
+ code: -32602,
224
+ message: `Unknown tool: '${name}'`,
225
+ },
226
+ };
227
+ process.stdout.write(`${JSON.stringify(response)}\n`);
228
+ continue;
229
+ }
230
+
231
+ // Validate required query parameter for uber_search
232
+ if (name === "uber_search" && (!args.query || args.query.trim() === "")) {
233
+ const response: MCPResponse = {
234
+ jsonrpc: "2.0",
235
+ id: request.id,
236
+ error: {
237
+ code: -32602,
238
+ message: "Invalid params: 'query' is required and must be non-empty",
239
+ },
240
+ };
241
+ process.stdout.write(`${JSON.stringify(response)}\n`);
242
+ continue;
243
+ }
244
+
245
+ // Input sanitization - query length check
246
+ if (args.query && args.query.length > 2000) {
247
+ const response: MCPResponse = {
248
+ jsonrpc: "2.0",
249
+ id: request.id,
250
+ error: {
251
+ code: -32602,
252
+ message: "Invalid params: query exceeds maximum length of 2000 characters",
253
+ },
254
+ };
255
+ process.stdout.write(`${JSON.stringify(response)}\n`);
256
+ continue;
257
+ }
258
+
173
259
  try {
174
260
  let result: unknown;
175
261
  if (name === "uber_search") {
@@ -194,7 +280,10 @@ Example: "it,science" for tech and academic results`,
194
280
  } else if (name === "uber_search_credits") {
195
281
  result = await withTimeout(getCreditStatus(), 10000, "getCreditStatus");
196
282
  } else if (name === "uber_search_health") {
197
- const container = await withTimeout(bootstrapContainer(), 30000, "bootstrapContainer");
283
+ if (!globalContainer) {
284
+ globalContainer = await withTimeout(bootstrapContainer(), 30000, "bootstrapContainer");
285
+ }
286
+ const container = globalContainer;
198
287
  const registry = container.get<ProviderRegistry>(ServiceKeys.PROVIDER_REGISTRY);
199
288
  const providers = registry.list();
200
289
 
@@ -208,7 +297,7 @@ Example: "it,science" for tech and academic results`,
208
297
  results.push({
209
298
  engineId: provider.id,
210
299
  status: "unhealthy",
211
- message: error instanceof Error ? error.message : String(error),
300
+ message: getErrorMessage(error),
212
301
  });
213
302
  }
214
303
  } else {
@@ -216,8 +305,6 @@ Example: "it,science" for tech and academic results`,
216
305
  }
217
306
  }
218
307
  result = results;
219
- } else {
220
- throw new Error(`Unknown tool: ${name}`);
221
308
  }
222
309
 
223
310
  const response: MCPResponse = {
@@ -232,17 +319,29 @@ Example: "it,science" for tech and academic results`,
232
319
  ],
233
320
  },
234
321
  };
235
- console.log(JSON.stringify(response));
322
+ process.stdout.write(`${JSON.stringify(response)}\n`);
236
323
  } catch (error) {
237
324
  const errorResponse: MCPResponse = {
238
325
  jsonrpc: "2.0",
239
326
  id: request.id,
240
327
  error: {
328
+ code: -32603,
241
329
  message: error instanceof Error ? error.message : String(error),
242
330
  },
243
331
  };
244
- console.log(JSON.stringify(errorResponse));
332
+ process.stdout.write(`${JSON.stringify(errorResponse)}\n`);
245
333
  }
334
+ } else if (request.id !== undefined) {
335
+ // Unknown method - send error response (only for requests with id, not notifications)
336
+ const response: MCPResponse = {
337
+ jsonrpc: "2.0",
338
+ id: request.id,
339
+ error: {
340
+ code: -32601,
341
+ message: `Method not found: ${request.method}`,
342
+ },
343
+ };
344
+ process.stdout.write(`${JSON.stringify(response)}\n`);
246
345
  }
247
346
  }
248
347
  }
@@ -6,6 +6,8 @@
6
6
  */
7
7
 
8
8
  import type { EngineConfigBase } from "../config/types";
9
+ import { getErrorMessage } from "../core/errorUtils";
10
+ import { createLogger } from "../core/logger";
9
11
  import type {
10
12
  CreateProviderOptions,
11
13
  ManagedProvider,
@@ -15,6 +17,8 @@ import type {
15
17
  PluginRegistrationResult,
16
18
  } from "./types";
17
19
 
20
+ const log = createLogger("PluginRegistry");
21
+
18
22
  /**
19
23
  * Central registry for search provider plugins
20
24
  *
@@ -108,7 +112,7 @@ export class PluginRegistry {
108
112
  await existing.onUnregister();
109
113
  } catch (error) {
110
114
  // Log but don't fail - we still want to register the new plugin
111
- console.warn(`Error during onUnregister for plugin '${plugin.type}':`, error);
115
+ log.warn(`Error during onUnregister for plugin '${plugin.type}':`, error);
112
116
  }
113
117
  }
114
118
  }
@@ -126,7 +130,7 @@ export class PluginRegistry {
126
130
  return {
127
131
  success: false,
128
132
  type: plugin.type,
129
- message: `Plugin onRegister failed: ${error instanceof Error ? error.message : String(error)}`,
133
+ message: `Plugin onRegister failed: ${getErrorMessage(error)}`,
130
134
  };
131
135
  }
132
136
  }
@@ -285,14 +289,21 @@ export class PluginRegistry {
285
289
  validatedConfig = plugin.configSchema.validate(config) as T & { type: string };
286
290
  } catch (error) {
287
291
  throw new Error(
288
- `Config validation failed for plugin '${config.type}': ` +
289
- `${error instanceof Error ? error.message : String(error)}`,
292
+ `Config validation failed for plugin '${config.type}': ` + `${getErrorMessage(error)}`,
290
293
  );
291
294
  }
292
295
  }
293
296
 
294
297
  // Create provider using factory
295
- return plugin.factory(validatedConfig, container) as ManagedProvider;
298
+ try {
299
+ const provider = plugin.factory(validatedConfig, container);
300
+ return provider as ManagedProvider;
301
+ } catch (error) {
302
+ const message = getErrorMessage(error);
303
+ throw new Error(`Failed to create provider for plugin '${config.type}': ${message}`, {
304
+ cause: error,
305
+ });
306
+ }
296
307
  }
297
308
 
298
309
  /**
@@ -315,7 +326,7 @@ export class PluginRegistry {
315
326
  try {
316
327
  await plugin.onUnregister();
317
328
  } catch (error) {
318
- console.warn(`Error during onUnregister for plugin '${plugin.type}':`, error);
329
+ log.warn(`Error during onUnregister for plugin '${plugin.type}':`, error);
319
330
  }
320
331
  }
321
332
  }
@@ -8,14 +8,7 @@
8
8
  * - SearchXNG
9
9
  */
10
10
 
11
- import type {
12
- BraveConfig,
13
- EngineConfigBase,
14
- LinkupConfig,
15
- SearchxngConfig,
16
- TavilyConfig,
17
- } from "../config/types";
18
- import type { SearchProvider } from "../core/provider";
11
+ import type { BraveConfig, LinkupConfig, SearchxngConfig, TavilyConfig } from "../config/types";
19
12
  import { BraveProvider } from "../providers/brave";
20
13
  import { LinkupProvider } from "../providers/linkup";
21
14
  import { SearchxngProvider } from "../providers/searchxng";
@@ -77,14 +70,19 @@ export const searchxngPlugin: PluginDefinition<SearchxngConfig, SearchxngProvide
77
70
 
78
71
  /**
79
72
  * All built-in plugins
80
- * Using type assertion since specific configs/providers extend base types
73
+ *
74
+ * Each plugin has a specific config/provider type (e.g. TavilyConfig/TavilyProvider),
75
+ * but consumers only need the base PluginDefinition interface. The factory parameter
76
+ * is contravariant in TConfig, so TypeScript disallows a direct assignment; however,
77
+ * the cast is safe because each specific config extends EngineConfigBase and each
78
+ * provider extends SearchProvider.
81
79
  */
82
80
  export const builtInPlugins = [
83
81
  tavilyPlugin,
84
82
  bravePlugin,
85
83
  linkupPlugin,
86
84
  searchxngPlugin,
87
- ] as unknown as PluginDefinition<EngineConfigBase, SearchProvider>[];
85
+ ] as unknown as PluginDefinition[];
88
86
 
89
87
  /**
90
88
  * Register all built-in plugins with the registry
@@ -190,14 +190,13 @@ export function isLifecycleProvider(
190
190
  if (provider == null || typeof provider !== "object") {
191
191
  return false;
192
192
  }
193
- const obj = provider as unknown as Record<string, unknown>;
194
193
  return (
195
194
  "init" in provider &&
196
195
  "healthcheck" in provider &&
197
196
  "shutdown" in provider &&
198
- typeof obj.init === "function" &&
199
- typeof obj.healthcheck === "function" &&
200
- typeof obj.shutdown === "function"
197
+ typeof (provider as Record<string, unknown>).init === "function" &&
198
+ typeof (provider as Record<string, unknown>).healthcheck === "function" &&
199
+ typeof (provider as Record<string, unknown>).shutdown === "function"
201
200
  );
202
201
  }
203
202
 
@@ -6,6 +6,7 @@ import type { BraveConfig } from "../config/types";
6
6
  import type { SearchQuery, SearchResponse } from "../core/types";
7
7
  import { BaseProvider } from "./BaseProvider";
8
8
  import { PROVIDER_DEFAULTS } from "./constants";
9
+ import { mapSearchResults, PROVIDER_MAPPINGS } from "./helpers";
9
10
  import type { BraveApiResponse, BraveWebResult } from "./types";
10
11
  import { buildUrl, fetchWithErrorHandling } from "./utils";
11
12
 
@@ -48,13 +49,7 @@ export class BraveProvider extends BaseProvider<BraveConfig> {
48
49
  this.validateResults(webResults, "Brave");
49
50
 
50
51
  // Map to normalized format
51
- const items = webResults.map((r: BraveWebResult) => ({
52
- title: r.title ?? r.url,
53
- url: r.url,
54
- snippet: r.description ?? r.snippet ?? r.abstract ?? "",
55
- score: r.rank ?? r.score,
56
- sourceEngine: this.id,
57
- }));
52
+ const items = mapSearchResults(webResults, this.id, PROVIDER_MAPPINGS.brave);
58
53
 
59
54
  return {
60
55
  engineId: this.id,
@@ -149,8 +149,8 @@ export const PROVIDER_MAPPINGS = {
149
149
  brave: {
150
150
  title: ["title", "url"],
151
151
  url: ["url"],
152
- snippet: ["description", "snippet"],
153
- score: ["score"],
152
+ snippet: ["description", "snippet", "abstract"],
153
+ score: ["rank", "score"],
154
154
  },
155
155
  linkup: {
156
156
  title: ["name", "title", "url"],
@@ -4,32 +4,25 @@
4
4
  */
5
5
 
6
6
  import type { LinkupConfig } from "../config/types";
7
- import { DockerLifecycleManager } from "../core/docker/dockerLifecycleManager";
8
7
  import type { ILifecycleProvider } from "../core/provider";
9
8
  import type { SearchQuery, SearchResponse } from "../core/types";
10
9
  import { BaseProvider } from "./BaseProvider";
11
10
  import { PROVIDER_DEFAULTS } from "./constants";
11
+ import {
12
+ addLifecycleMethods,
13
+ createDockerLifecycle,
14
+ mapSearchResults,
15
+ PROVIDER_MAPPINGS,
16
+ } from "./helpers";
12
17
  import type { LinkupApiResponse, LinkupSearchResult } from "./types";
13
18
  import { fetchWithErrorHandling } from "./utils";
14
19
 
15
20
  export class LinkupProvider extends BaseProvider<LinkupConfig> implements ILifecycleProvider {
16
- private lifecycleManager: DockerLifecycleManager;
17
-
18
21
  constructor(config: LinkupConfig) {
19
22
  super(config);
20
23
 
21
- const autoStart = config.autoStart ?? false;
22
- const autoStop = config.autoStop ?? false;
23
- const initTimeoutMs = config.initTimeoutMs ?? 30000;
24
-
25
- this.lifecycleManager = new DockerLifecycleManager({
26
- containerName: config.containerName,
27
- composeFile: config.composeFile,
28
- healthEndpoint: config.healthEndpoint,
29
- autoStart,
30
- autoStop,
31
- initTimeoutMs,
32
- });
24
+ const manager = createDockerLifecycle(config);
25
+ addLifecycleMethods(this, manager);
33
26
  }
34
27
 
35
28
  protected getDocsUrl(): string {
@@ -70,14 +63,7 @@ export class LinkupProvider extends BaseProvider<LinkupConfig> implements ILifec
70
63
  this.validateResults(results, "Linkup");
71
64
 
72
65
  // Map to normalized format
73
- // Linkup results have url, name/title, content/snippet
74
- const items = results.map((r: LinkupSearchResult) => ({
75
- title: r.name ?? r.title ?? r.url,
76
- url: r.url,
77
- snippet: r.content ?? r.snippet ?? r.description ?? "",
78
- score: r.score ?? r.relevance,
79
- sourceEngine: this.id,
80
- }));
66
+ const items = mapSearchResults(results, this.id, PROVIDER_MAPPINGS.linkup);
81
67
 
82
68
  return {
83
69
  engineId: this.id,
@@ -87,28 +73,10 @@ export class LinkupProvider extends BaseProvider<LinkupConfig> implements ILifec
87
73
  };
88
74
  }
89
75
 
90
- // ILifecycleProvider implementation
91
- async init(): Promise<void> {
92
- await this.lifecycleManager.init();
93
- }
94
-
95
- async healthcheck(): Promise<boolean> {
96
- return await this.lifecycleManager.healthcheck();
97
- }
98
-
99
- async shutdown(): Promise<void> {
100
- await this.lifecycleManager.shutdown();
101
- }
102
-
103
- async validateConfig(): Promise<{
104
- valid: boolean;
105
- errors: string[];
106
- warnings: string[];
107
- }> {
108
- return await this.lifecycleManager.validateDockerConfig();
109
- }
110
-
111
- isLifecycleManaged(): boolean {
112
- return true; // This provider manages lifecycle
113
- }
76
+ // Lifecycle methods are added via addLifecycleMethods() in constructor
77
+ declare init: () => Promise<void>;
78
+ declare healthcheck: () => Promise<boolean>;
79
+ declare shutdown: () => Promise<void>;
80
+ declare validateConfig: () => Promise<{ valid: boolean; errors: string[]; warnings: string[] }>;
81
+ declare isLifecycleManaged: () => boolean;
114
82
  }