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.
- package/package.json +1 -1
- package/providers/searxng/.env.example +1 -1
- package/src/bootstrap/container.ts +3 -2
- package/src/cli.ts +6 -14
- package/src/config/load.ts +10 -13
- package/src/config/validation.ts +22 -1
- package/src/core/credits/FileCreditStateProvider.ts +1 -1
- package/src/core/docker/dockerComposeHelper.ts +15 -6
- package/src/core/docker/dockerLifecycleManager.ts +21 -9
- package/src/core/errorUtils.ts +6 -0
- package/src/core/orchestrator.ts +2 -28
- package/src/core/paths.ts +7 -8
- package/src/core/provider.ts +1 -14
- package/src/core/retry.ts +102 -0
- package/src/core/strategy/AllProvidersStrategy.ts +43 -27
- package/src/core/strategy/FirstSuccessStrategy.ts +3 -4
- package/src/core/strategy/ISearchStrategy.ts +2 -2
- package/src/core/types.ts +1 -0
- package/src/mcp-server.ts +110 -11
- package/src/plugin/PluginRegistry.ts +17 -6
- package/src/plugin/builtin.ts +8 -10
- package/src/plugin/types.ts +3 -4
- package/src/providers/brave.ts +2 -7
- package/src/providers/helpers/resultMappers.ts +2 -2
- package/src/providers/linkup.ts +15 -47
- package/src/providers/retry.ts +1 -102
- package/src/providers/searchxng.ts +24 -45
- package/src/providers/tavily.ts +10 -16
- package/src/tool/interface.ts +3 -1
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* the overall search.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
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?:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
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 {
|
|
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?:
|
|
61
|
+
reason?: SearchFailureReason;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/**
|
package/src/core/types.ts
CHANGED
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: ${
|
|
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
|
-
|
|
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
|
-
|
|
329
|
+
log.warn(`Error during onUnregister for plugin '${plugin.type}':`, error);
|
|
319
330
|
}
|
|
320
331
|
}
|
|
321
332
|
}
|
package/src/plugin/builtin.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
85
|
+
] as unknown as PluginDefinition[];
|
|
88
86
|
|
|
89
87
|
/**
|
|
90
88
|
* Register all built-in plugins with the registry
|
package/src/plugin/types.ts
CHANGED
|
@@ -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
|
|
199
|
-
typeof
|
|
200
|
-
typeof
|
|
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
|
|
package/src/providers/brave.ts
CHANGED
|
@@ -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.
|
|
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"],
|
package/src/providers/linkup.ts
CHANGED
|
@@ -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
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
}
|