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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ubersearch",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "module": "src/index.ts",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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.info(`Skipping provider ${engineConfig.id}: ${provider.getMissingConfigMessage()}`);
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.info(`Registered provider: ${engineConfig.id}`);
104
+ log.debug(`Registered provider: ${engineConfig.id}`);
105
105
  } catch (error) {
106
106
  const errorMsg = error instanceof Error ? error.message : String(error);
107
- log.warn(`Failed to register provider ${engineConfig.id}: ${errorMsg}`);
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 could be registered. Skipped/failed providers: ${allSkipped.join(", ")}. ` +
117
- "Check your configuration and environment variables.",
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.warn(`Some providers failed to initialize: ${failedProviders.join(", ")}`);
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 _container = await bootstrapContainer(configPath);
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
- configPath,
173
+ { containerOverride: container },
174
174
  );
175
175
 
176
176
  if (options.json) {
@@ -203,35 +203,87 @@ export interface LoadConfigOptions {
203
203
  */
204
204
  /**
205
205
  * Get default configuration when no config file is found
206
- * Uses SearXNG as a sensible default (no API key required)
206
+ * Prioritizes cloud providers if API keys are set, falls back to SearXNG
207
207
  */
208
208
  function getDefaultConfig(): ExtendedSearchConfig {
209
- const packageRoot = getPackageRoot();
210
- const composeFile = join(packageRoot, "providers", "searxng", "docker-compose.yml");
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
- return {
213
- defaultEngineOrder: ["searchxng"],
214
- engines: [
215
- {
216
- id: "searchxng",
217
- type: "searchxng",
218
- enabled: true,
219
- displayName: "SearXNG (Local)",
220
- apiKeyEnv: "SEARXNG_API_KEY",
221
- endpoint: "http://localhost:8888/search",
222
- composeFile,
223
- containerName: "searxng",
224
- healthEndpoint: "http://localhost:8888/healthz",
225
- defaultLimit: 10,
226
- monthlyQuota: 10000,
227
- creditCostPerSearch: 0,
228
- lowCreditThresholdPercent: 80,
229
- autoStart: true,
230
- autoStop: true,
231
- initTimeoutMs: 60000,
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
- log.error("Initialization failed:", error);
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.warn("Docker is not available. Cannot auto-start container.");
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.info("Container is already running.");
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.info("Starting Docker container...");
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.info("Container started successfully.");
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
- log.error("Failed to start container:", error);
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.info("Waiting for health check...");
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.info("Health check passed.");
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.info("Stopping Docker container...");
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.info("Container stopped.");
244
+ log.debug("Container stopped.");
243
245
  } catch (error) {
244
- log.error("Failed to stop container:", error);
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
- log.warn("Error checking if container is running:", error);
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 warning but continue with other providers
122
- log.warn(
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.warn(`Search failed for ${engineId}: Promise rejected`);
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.warn(`Search failed for ${engineId}: ${error.message}`);
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 warning and continue
89
- log.warn(
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
- console.log(`[SearXNG] Container not healthy, attempting auto-start...`);
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
- console.error(`[SearXNG] Failed to auto-start container:`, initError);
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?: unknown[];
132
+ infoboxes?: SearxngInfobox[];
121
133
  suggestions?: string[];
122
134
  answers?: string[];
123
135
  corrections?: string[];