ubersearch 1.1.0 → 1.1.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/package.json +1 -1
- package/src/bootstrap/container.ts +11 -6
- package/src/cli.ts +2 -2
- package/src/config/load.ts +79 -32
- package/src/core/docker/dockerLifecycleManager.ts +16 -12
- package/src/core/strategy/AllProvidersStrategy.ts +4 -4
- package/src/core/strategy/FirstSuccessStrategy.ts +2 -2
- package/src/providers/searchxng.ts +28 -6
- package/src/providers/types/index.ts +13 -1
package/package.json
CHANGED
|
@@ -95,16 +95,16 @@ export async function bootstrapContainer(
|
|
|
95
95
|
|
|
96
96
|
// Skip providers that aren't configured (e.g., missing API key)
|
|
97
97
|
if (!provider.isConfigured()) {
|
|
98
|
-
log.
|
|
98
|
+
log.debug(`Skipping provider ${engineConfig.id}: ${provider.getMissingConfigMessage()}`);
|
|
99
99
|
skippedProviders.push(engineConfig.id);
|
|
100
100
|
continue;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
registry.register(provider);
|
|
104
|
-
log.
|
|
104
|
+
log.debug(`Registered provider: ${engineConfig.id}`);
|
|
105
105
|
} catch (error) {
|
|
106
106
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
107
|
-
log.
|
|
107
|
+
log.debug(`Failed to register provider ${engineConfig.id}: ${errorMsg}`);
|
|
108
108
|
failedProviders.push(engineConfig.id);
|
|
109
109
|
}
|
|
110
110
|
}
|
|
@@ -112,14 +112,19 @@ export async function bootstrapContainer(
|
|
|
112
112
|
const availableProviders = registry.list();
|
|
113
113
|
if (availableProviders.length === 0) {
|
|
114
114
|
const allSkipped = [...failedProviders, ...skippedProviders];
|
|
115
|
+
const skipDetails = allSkipped.length > 0 ? `Skipped: ${allSkipped.join(", ")}. ` : "";
|
|
115
116
|
throw new Error(
|
|
116
|
-
`No providers
|
|
117
|
-
"
|
|
117
|
+
`No search providers available. ${skipDetails}\n\n` +
|
|
118
|
+
"To fix this, either:\n" +
|
|
119
|
+
" 1. Set an API key: export TAVILY_API_KEY=your-key (or BRAVE_API_KEY, LINKUP_API_KEY)\n" +
|
|
120
|
+
" 2. Start the SearXNG Docker container: docker compose up -d\n" +
|
|
121
|
+
" 3. Create a config file: ubersearch.config.json\n\n" +
|
|
122
|
+
"Get API keys at: https://tavily.com, https://brave.com/search/api, https://linkup.so",
|
|
118
123
|
);
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
if (failedProviders.length > 0) {
|
|
122
|
-
log.
|
|
127
|
+
log.debug(`Some providers failed to initialize: ${failedProviders.join(", ")}`);
|
|
123
128
|
}
|
|
124
129
|
|
|
125
130
|
return registry;
|
package/src/cli.ts
CHANGED
|
@@ -160,7 +160,7 @@ if (!query) {
|
|
|
160
160
|
async function main() {
|
|
161
161
|
try {
|
|
162
162
|
// Bootstrap the DI container
|
|
163
|
-
const
|
|
163
|
+
const container = await bootstrapContainer(configPath);
|
|
164
164
|
|
|
165
165
|
const result = await uberSearch(
|
|
166
166
|
{
|
|
@@ -170,7 +170,7 @@ async function main() {
|
|
|
170
170
|
includeRaw: options.includeRaw,
|
|
171
171
|
strategy: options.strategy,
|
|
172
172
|
},
|
|
173
|
-
|
|
173
|
+
{ containerOverride: container },
|
|
174
174
|
);
|
|
175
175
|
|
|
176
176
|
if (options.json) {
|
package/src/config/load.ts
CHANGED
|
@@ -203,35 +203,87 @@ export interface LoadConfigOptions {
|
|
|
203
203
|
*/
|
|
204
204
|
/**
|
|
205
205
|
* Get default configuration when no config file is found
|
|
206
|
-
*
|
|
206
|
+
* Prioritizes cloud providers if API keys are set, falls back to SearXNG
|
|
207
207
|
*/
|
|
208
208
|
function getDefaultConfig(): ExtendedSearchConfig {
|
|
209
|
-
const
|
|
210
|
-
const
|
|
209
|
+
const engines: ExtendedSearchConfig["engines"] = [];
|
|
210
|
+
const defaultEngineOrder: string[] = [];
|
|
211
|
+
|
|
212
|
+
// Add cloud providers if API keys are available (preferred for bunx usage)
|
|
213
|
+
if (process.env.TAVILY_API_KEY) {
|
|
214
|
+
defaultEngineOrder.push("tavily");
|
|
215
|
+
engines.push({
|
|
216
|
+
id: "tavily",
|
|
217
|
+
type: "tavily",
|
|
218
|
+
enabled: true,
|
|
219
|
+
displayName: "Tavily Search",
|
|
220
|
+
apiKeyEnv: "TAVILY_API_KEY",
|
|
221
|
+
endpoint: "https://api.tavily.com/search",
|
|
222
|
+
searchDepth: "basic",
|
|
223
|
+
monthlyQuota: 1000,
|
|
224
|
+
creditCostPerSearch: 1,
|
|
225
|
+
lowCreditThresholdPercent: 80,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
211
228
|
|
|
212
|
-
|
|
213
|
-
defaultEngineOrder
|
|
214
|
-
engines
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
229
|
+
if (process.env.BRAVE_API_KEY) {
|
|
230
|
+
defaultEngineOrder.push("brave");
|
|
231
|
+
engines.push({
|
|
232
|
+
id: "brave",
|
|
233
|
+
type: "brave",
|
|
234
|
+
enabled: true,
|
|
235
|
+
displayName: "Brave Search",
|
|
236
|
+
apiKeyEnv: "BRAVE_API_KEY",
|
|
237
|
+
endpoint: "https://api.search.brave.com/res/v1/web/search",
|
|
238
|
+
defaultLimit: 10,
|
|
239
|
+
monthlyQuota: 1000,
|
|
240
|
+
creditCostPerSearch: 1,
|
|
241
|
+
lowCreditThresholdPercent: 80,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (process.env.LINKUP_API_KEY) {
|
|
246
|
+
defaultEngineOrder.push("linkup");
|
|
247
|
+
engines.push({
|
|
248
|
+
id: "linkup",
|
|
249
|
+
type: "linkup",
|
|
250
|
+
enabled: true,
|
|
251
|
+
displayName: "Linkup Search",
|
|
252
|
+
apiKeyEnv: "LINKUP_API_KEY",
|
|
253
|
+
endpoint: "https://api.linkup.so/v1/search",
|
|
254
|
+
monthlyQuota: 1000,
|
|
255
|
+
creditCostPerSearch: 1,
|
|
256
|
+
lowCreditThresholdPercent: 80,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// If no cloud providers configured, fall back to SearXNG (requires Docker)
|
|
261
|
+
if (engines.length === 0) {
|
|
262
|
+
const packageRoot = getPackageRoot();
|
|
263
|
+
const composeFile = join(packageRoot, "providers", "searxng", "docker-compose.yml");
|
|
264
|
+
|
|
265
|
+
defaultEngineOrder.push("searchxng");
|
|
266
|
+
engines.push({
|
|
267
|
+
id: "searchxng",
|
|
268
|
+
type: "searchxng",
|
|
269
|
+
enabled: true,
|
|
270
|
+
displayName: "SearXNG (Local)",
|
|
271
|
+
apiKeyEnv: "SEARXNG_API_KEY",
|
|
272
|
+
endpoint: "http://localhost:8888/search",
|
|
273
|
+
composeFile,
|
|
274
|
+
containerName: "searxng",
|
|
275
|
+
healthEndpoint: "http://localhost:8888/healthz",
|
|
276
|
+
defaultLimit: 10,
|
|
277
|
+
monthlyQuota: 10000,
|
|
278
|
+
creditCostPerSearch: 0,
|
|
279
|
+
lowCreditThresholdPercent: 80,
|
|
280
|
+
autoStart: true,
|
|
281
|
+
autoStop: true,
|
|
282
|
+
initTimeoutMs: 60000,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return { defaultEngineOrder, engines };
|
|
235
287
|
}
|
|
236
288
|
|
|
237
289
|
export async function loadConfig(
|
|
@@ -293,10 +345,7 @@ export async function loadConfig(
|
|
|
293
345
|
}
|
|
294
346
|
}
|
|
295
347
|
|
|
296
|
-
// No config file found - use default configuration
|
|
297
|
-
console.warn("No config file found. Using default configuration (SearXNG).");
|
|
298
|
-
console.warn("Create a config file for custom providers: ubersearch.config.json");
|
|
299
|
-
|
|
348
|
+
// No config file found - use default configuration (silent)
|
|
300
349
|
const defaultConfig = getDefaultConfig();
|
|
301
350
|
|
|
302
351
|
// Register plugins for default config
|
|
@@ -354,8 +403,6 @@ export function loadConfigSync(
|
|
|
354
403
|
}
|
|
355
404
|
}
|
|
356
405
|
|
|
357
|
-
console.warn("No config file found. Using default configuration (SearXNG).");
|
|
358
|
-
console.warn("Create a config file for custom providers: ubersearch.config.json");
|
|
359
406
|
return getDefaultConfig();
|
|
360
407
|
}
|
|
361
408
|
|
|
@@ -73,7 +73,8 @@ export class DockerLifecycleManager {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
this.initPromise = this.performInit().catch((error) => {
|
|
76
|
-
|
|
76
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
77
|
+
log.error("Initialization failed:", message);
|
|
77
78
|
this.initialized = false;
|
|
78
79
|
throw error;
|
|
79
80
|
});
|
|
@@ -110,7 +111,7 @@ export class DockerLifecycleManager {
|
|
|
110
111
|
"Docker availability check",
|
|
111
112
|
);
|
|
112
113
|
if (!dockerAvailable) {
|
|
113
|
-
log.
|
|
114
|
+
log.debug("Docker is not available. Cannot auto-start container.");
|
|
114
115
|
this.initialized = true;
|
|
115
116
|
return;
|
|
116
117
|
}
|
|
@@ -118,13 +119,13 @@ export class DockerLifecycleManager {
|
|
|
118
119
|
// Check if container is already running (with timeout)
|
|
119
120
|
const isRunning = await this.withTimeout(this.healthcheck(), 5000, "Initial health check");
|
|
120
121
|
if (isRunning) {
|
|
121
|
-
log.
|
|
122
|
+
log.debug("Container is already running.");
|
|
122
123
|
this.initialized = true;
|
|
123
124
|
return;
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
// Auto-start container
|
|
127
|
-
log.
|
|
128
|
+
log.debug("Starting Docker container...");
|
|
128
129
|
try {
|
|
129
130
|
// Run from project root to ensure correct path resolution
|
|
130
131
|
const projectRoot = this.config.projectRoot || process.cwd();
|
|
@@ -132,7 +133,7 @@ export class DockerLifecycleManager {
|
|
|
132
133
|
this.config.containerName ? [this.config.containerName] : undefined,
|
|
133
134
|
{ cwd: projectRoot },
|
|
134
135
|
);
|
|
135
|
-
log.
|
|
136
|
+
log.debug("Container started successfully.");
|
|
136
137
|
|
|
137
138
|
// Wait for health check if endpoint is configured
|
|
138
139
|
if (this.config.healthEndpoint) {
|
|
@@ -141,7 +142,8 @@ export class DockerLifecycleManager {
|
|
|
141
142
|
|
|
142
143
|
this.initialized = true;
|
|
143
144
|
} catch (error) {
|
|
144
|
-
|
|
145
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
146
|
+
log.error("Failed to start container:", message);
|
|
145
147
|
throw error;
|
|
146
148
|
} finally {
|
|
147
149
|
this.initPromise = null;
|
|
@@ -196,12 +198,12 @@ export class DockerLifecycleManager {
|
|
|
196
198
|
return;
|
|
197
199
|
}
|
|
198
200
|
|
|
199
|
-
log.
|
|
201
|
+
log.debug("Waiting for health check...");
|
|
200
202
|
|
|
201
203
|
const startTime = Date.now();
|
|
202
204
|
while (Date.now() - startTime < timeoutMs) {
|
|
203
205
|
if (await this.healthcheck()) {
|
|
204
|
-
log.
|
|
206
|
+
log.debug("Health check passed.");
|
|
205
207
|
return;
|
|
206
208
|
}
|
|
207
209
|
|
|
@@ -233,15 +235,16 @@ export class DockerLifecycleManager {
|
|
|
233
235
|
return;
|
|
234
236
|
}
|
|
235
237
|
|
|
236
|
-
log.
|
|
238
|
+
log.debug("Stopping Docker container...");
|
|
237
239
|
try {
|
|
238
240
|
await this.dockerHelper.stop(
|
|
239
241
|
this.config.containerName ? [this.config.containerName] : undefined,
|
|
240
242
|
{ cwd: projectRoot },
|
|
241
243
|
);
|
|
242
|
-
log.
|
|
244
|
+
log.debug("Container stopped.");
|
|
243
245
|
} catch (error) {
|
|
244
|
-
|
|
246
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
247
|
+
log.error("Failed to stop container:", message);
|
|
245
248
|
// Don't throw on shutdown errors
|
|
246
249
|
}
|
|
247
250
|
}
|
|
@@ -345,7 +348,8 @@ export class DockerLifecycleManager {
|
|
|
345
348
|
cwd: projectRoot,
|
|
346
349
|
});
|
|
347
350
|
} catch (error) {
|
|
348
|
-
|
|
351
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
352
|
+
log.debug("Error checking if container is running:", message);
|
|
349
353
|
return false;
|
|
350
354
|
}
|
|
351
355
|
}
|
|
@@ -118,8 +118,8 @@ export class AllProvidersStrategy implements ISearchStrategy {
|
|
|
118
118
|
attempts.push({ engineId, success: false, reason: "unknown" });
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
// Log
|
|
122
|
-
log.
|
|
121
|
+
// Log debug message but continue with other providers
|
|
122
|
+
log.debug(
|
|
123
123
|
`Search failed for ${engineId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
124
124
|
);
|
|
125
125
|
}
|
|
@@ -194,7 +194,7 @@ export class AllProvidersStrategy implements ISearchStrategy {
|
|
|
194
194
|
if (settledResult.status === "rejected") {
|
|
195
195
|
// Promise itself was rejected (shouldn't happen with our try/catch, but handle it)
|
|
196
196
|
attempts.push({ engineId, success: false, reason: "unknown" });
|
|
197
|
-
log.
|
|
197
|
+
log.debug(`Search failed for ${engineId}: Promise rejected`);
|
|
198
198
|
continue;
|
|
199
199
|
}
|
|
200
200
|
|
|
@@ -207,7 +207,7 @@ export class AllProvidersStrategy implements ISearchStrategy {
|
|
|
207
207
|
} else {
|
|
208
208
|
attempts.push({ engineId, success: false, reason: "unknown" });
|
|
209
209
|
}
|
|
210
|
-
log.
|
|
210
|
+
log.debug(`Search failed for ${engineId}: ${error.message}`);
|
|
211
211
|
continue;
|
|
212
212
|
}
|
|
213
213
|
|
|
@@ -85,8 +85,8 @@ export class FirstSuccessStrategy implements ISearchStrategy {
|
|
|
85
85
|
attempts.push({ engineId, success: false, reason: "unknown" });
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
// Log
|
|
89
|
-
log.
|
|
88
|
+
// Log debug message and continue
|
|
89
|
+
log.debug(
|
|
90
90
|
`Search failed for ${engineId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
91
91
|
);
|
|
92
92
|
}
|
|
@@ -7,14 +7,17 @@
|
|
|
7
7
|
import { dirname } from "node:path";
|
|
8
8
|
import type { SearchxngConfig as BaseSearchxngConfig } from "../config/types";
|
|
9
9
|
import { DockerLifecycleManager } from "../core/docker/dockerLifecycleManager";
|
|
10
|
+
import { createLogger } from "../core/logger";
|
|
10
11
|
import type { ILifecycleProvider, ProviderMetadata } from "../core/provider";
|
|
11
12
|
import type { SearchQuery, SearchResponse, SearchResultItem } from "../core/types";
|
|
12
13
|
import { SearchError } from "../core/types";
|
|
13
14
|
import { BaseProvider } from "./BaseProvider";
|
|
14
15
|
import { PROVIDER_DEFAULTS } from "./constants";
|
|
15
|
-
import type { SearxngApiResponse, SearxngSearchResult } from "./types";
|
|
16
|
+
import type { SearxngApiResponse, SearxngInfobox, SearxngSearchResult } from "./types";
|
|
16
17
|
import { buildUrl, fetchWithErrorHandling } from "./utils";
|
|
17
18
|
|
|
19
|
+
const log = createLogger("SearXNG");
|
|
20
|
+
|
|
18
21
|
export class SearchxngProvider
|
|
19
22
|
extends BaseProvider<BaseSearchxngConfig>
|
|
20
23
|
implements ILifecycleProvider
|
|
@@ -79,12 +82,13 @@ export class SearchxngProvider
|
|
|
79
82
|
const isInitializing = !!(await (this.lifecycleManager as any).initPromise);
|
|
80
83
|
if (!isInitializing) {
|
|
81
84
|
try {
|
|
82
|
-
|
|
85
|
+
log.debug("Container not healthy, attempting auto-start...");
|
|
83
86
|
await this.init();
|
|
84
87
|
// Re-check health after init attempt
|
|
85
88
|
isHealthy = await this.healthcheck();
|
|
86
89
|
} catch (initError) {
|
|
87
|
-
|
|
90
|
+
const message = initError instanceof Error ? initError.message : String(initError);
|
|
91
|
+
log.debug(`Failed to auto-start container: ${message}`);
|
|
88
92
|
}
|
|
89
93
|
}
|
|
90
94
|
}
|
|
@@ -121,10 +125,10 @@ export class SearchxngProvider
|
|
|
121
125
|
"SearXNG",
|
|
122
126
|
);
|
|
123
127
|
|
|
124
|
-
const results: SearxngSearchResult[] = json.results
|
|
125
|
-
|
|
126
|
-
this.validateResults(results, "SearXNG");
|
|
128
|
+
const results: SearxngSearchResult[] = Array.isArray(json.results) ? json.results : [];
|
|
129
|
+
const infoboxes: SearxngInfobox[] = Array.isArray(json.infoboxes) ? json.infoboxes : [];
|
|
127
130
|
|
|
131
|
+
// Convert regular results
|
|
128
132
|
const items: SearchResultItem[] = results.map((r: SearxngSearchResult) => ({
|
|
129
133
|
title: r.title ?? r.url ?? "#",
|
|
130
134
|
url: r.url ?? "#",
|
|
@@ -133,6 +137,24 @@ export class SearchxngProvider
|
|
|
133
137
|
sourceEngine: r.engine ?? this.id,
|
|
134
138
|
}));
|
|
135
139
|
|
|
140
|
+
// Add infoboxes as results (Wikipedia, etc.)
|
|
141
|
+
for (const box of infoboxes) {
|
|
142
|
+
const boxUrl = box.id ?? box.urls?.[0]?.url;
|
|
143
|
+
if (boxUrl) {
|
|
144
|
+
items.push({
|
|
145
|
+
title: box.infobox ?? "Info",
|
|
146
|
+
url: boxUrl,
|
|
147
|
+
snippet: box.content ?? "",
|
|
148
|
+
sourceEngine: box.engine ?? "wikipedia",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Only validate if we have no results at all
|
|
154
|
+
if (items.length === 0) {
|
|
155
|
+
this.validateResults(results, "SearXNG");
|
|
156
|
+
}
|
|
157
|
+
|
|
136
158
|
const limitedItems = items.slice(0, limit);
|
|
137
159
|
|
|
138
160
|
return {
|
|
@@ -110,6 +110,18 @@ export interface SearxngSearchResult {
|
|
|
110
110
|
category?: string;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Infobox from SearXNG (Wikipedia, etc.)
|
|
115
|
+
*/
|
|
116
|
+
export interface SearxngInfobox {
|
|
117
|
+
infobox?: string;
|
|
118
|
+
id?: string;
|
|
119
|
+
content?: string;
|
|
120
|
+
img_src?: string;
|
|
121
|
+
urls?: Array<{ title?: string; url?: string }>;
|
|
122
|
+
engine?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
113
125
|
/**
|
|
114
126
|
* SearXNG search API response
|
|
115
127
|
*/
|
|
@@ -117,7 +129,7 @@ export interface SearxngApiResponse {
|
|
|
117
129
|
query?: string;
|
|
118
130
|
results: SearxngSearchResult[];
|
|
119
131
|
number_of_results?: number;
|
|
120
|
-
infoboxes?:
|
|
132
|
+
infoboxes?: SearxngInfobox[];
|
|
121
133
|
suggestions?: string[];
|
|
122
134
|
answers?: string[];
|
|
123
135
|
corrections?: string[];
|