ubersearch 0.0.0-development

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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +374 -0
  3. package/package.json +76 -0
  4. package/src/app/index.ts +30 -0
  5. package/src/bootstrap/container.ts +157 -0
  6. package/src/cli.ts +380 -0
  7. package/src/config/defineConfig.ts +176 -0
  8. package/src/config/load.ts +368 -0
  9. package/src/config/types.ts +86 -0
  10. package/src/config/validation.ts +148 -0
  11. package/src/core/cache.ts +74 -0
  12. package/src/core/container.ts +268 -0
  13. package/src/core/credits/CreditManager.ts +158 -0
  14. package/src/core/credits/CreditStateProvider.ts +151 -0
  15. package/src/core/credits/FileCreditStateProvider.ts +137 -0
  16. package/src/core/credits/index.ts +3 -0
  17. package/src/core/docker/dockerComposeHelper.ts +177 -0
  18. package/src/core/docker/dockerLifecycleManager.ts +361 -0
  19. package/src/core/docker/index.ts +8 -0
  20. package/src/core/logger.ts +146 -0
  21. package/src/core/orchestrator.ts +103 -0
  22. package/src/core/paths.ts +157 -0
  23. package/src/core/provider/ILifecycleProvider.ts +120 -0
  24. package/src/core/provider/ProviderFactory.ts +120 -0
  25. package/src/core/provider.ts +61 -0
  26. package/src/core/serviceKeys.ts +45 -0
  27. package/src/core/strategy/AllProvidersStrategy.ts +245 -0
  28. package/src/core/strategy/FirstSuccessStrategy.ts +98 -0
  29. package/src/core/strategy/ISearchStrategy.ts +94 -0
  30. package/src/core/strategy/StrategyFactory.ts +204 -0
  31. package/src/core/strategy/index.ts +9 -0
  32. package/src/core/strategy/types.ts +56 -0
  33. package/src/core/types.ts +58 -0
  34. package/src/index.ts +1 -0
  35. package/src/plugin/PluginRegistry.ts +336 -0
  36. package/src/plugin/builtin.ts +130 -0
  37. package/src/plugin/index.ts +33 -0
  38. package/src/plugin/types.ts +212 -0
  39. package/src/providers/BaseProvider.ts +49 -0
  40. package/src/providers/brave.ts +66 -0
  41. package/src/providers/constants.ts +13 -0
  42. package/src/providers/helpers/index.ts +24 -0
  43. package/src/providers/helpers/lifecycleHelpers.ts +110 -0
  44. package/src/providers/helpers/resultMappers.ts +168 -0
  45. package/src/providers/index.ts +6 -0
  46. package/src/providers/linkup.ts +114 -0
  47. package/src/providers/retry.ts +95 -0
  48. package/src/providers/searchxng.ts +163 -0
  49. package/src/providers/tavily.ts +73 -0
  50. package/src/providers/types/index.ts +185 -0
  51. package/src/providers/utils.ts +182 -0
  52. package/src/tool/allSearchTool.ts +110 -0
  53. package/src/tool/interface.ts +71 -0
package/src/cli.ts ADDED
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Multi-Search CLI
4
+ *
5
+ * Unified search interface for multiple search providers
6
+ */
7
+
8
+ import { bootstrapContainer, isLifecycleProvider } from "./bootstrap/container";
9
+ import type { ProviderRegistry } from "./core/provider";
10
+ import { ServiceKeys } from "./core/serviceKeys";
11
+ import type { AllSearchOutput } from "./tool/interface";
12
+ import { getCreditStatus, multiSearch } from "./tool/multiSearchTool";
13
+
14
+ // Parse command line arguments
15
+ let args = process.argv.slice(2);
16
+
17
+ // Parse --config first and remove from args (global option that can appear anywhere)
18
+ const configIdx = args.indexOf("--config");
19
+ let configPath: string | undefined;
20
+ if (configIdx !== -1) {
21
+ configPath = args[configIdx + 1];
22
+ if (!configPath || configPath.startsWith("--")) {
23
+ console.error("Error: --config requires a file path");
24
+ process.exit(1);
25
+ }
26
+ args.splice(configIdx, 2);
27
+ }
28
+
29
+ // Show help
30
+ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
31
+ console.log(`
32
+ allsearch — Unified search across multiple providers
33
+
34
+ USAGE:
35
+ allsearch <query> [options]
36
+ allsearch credits
37
+
38
+ ARGUMENTS:
39
+ <query> Search query (required, unless using 'credits' command)
40
+ credits Show credit status for all engines
41
+ health Run health checks on all providers
42
+
43
+ OPTIONS:
44
+ --json Output results as JSON
45
+ --engines <engine,list> Use specific engines (comma-separated)
46
+ --strategy <strategy> Search strategy: 'all' or 'first-success' (default: all)
47
+ --limit <number> Maximum results per engine
48
+ --include-raw Include raw provider responses
49
+ --config <path> Path to configuration file
50
+ --help, -h Show this help message
51
+
52
+ EXAMPLES:
53
+ allsearch "best TypeScript ORM 2025"
54
+ allsearch "llm observability" --engines tavily,brave --json
55
+ allsearch "hawaii dev meetups" --strategy first-success
56
+ allsearch credits
57
+ allsearch health
58
+ allsearch --config /path/to/config.json credits
59
+ allsearch "query" --config /path/to/config.json
60
+
61
+ CONFIGURATION:
62
+ Config files are searched in order:
63
+ 1. ./allsearch.config.json
64
+ 2. $XDG_CONFIG_HOME/allsearch/config.json
65
+ 3. ~/.config/allsearch/config.json
66
+
67
+ ENVIRONMENT:
68
+ Set API keys in environment variables:
69
+ - TAVILY_API_KEY for Tavily
70
+ - BRAVE_API_KEY for Brave Search
71
+ `);
72
+ process.exit(0);
73
+ }
74
+
75
+ // Credits command
76
+ if (args[0] === "credits") {
77
+ await showCredits(configPath);
78
+ process.exit(0);
79
+ }
80
+
81
+ // Health check command
82
+ if (args[0] === "health") {
83
+ await runHealthChecks(configPath);
84
+ process.exit(0);
85
+ }
86
+
87
+ // Parse options
88
+ const options = {
89
+ json: args.includes("--json"),
90
+ includeRaw: args.includes("--include-raw"),
91
+ engines: undefined as string[] | undefined,
92
+ strategy: undefined as "all" | "first-success" | undefined,
93
+ limit: undefined as number | undefined,
94
+ };
95
+
96
+ // Parse --engines
97
+ const enginesIdx = args.indexOf("--engines");
98
+ if (enginesIdx !== -1) {
99
+ const enginesArg = args[enginesIdx + 1];
100
+ if (enginesArg !== undefined) {
101
+ options.engines = enginesArg.split(",").map((e) => e.trim());
102
+ }
103
+ }
104
+
105
+ // Parse --strategy
106
+ const strategyIdx = args.indexOf("--strategy");
107
+ if (strategyIdx !== -1 && args[strategyIdx + 1]) {
108
+ const strategy = args[strategyIdx + 1];
109
+ if (strategy === "all" || strategy === "first-success") {
110
+ options.strategy = strategy;
111
+ } else {
112
+ console.error(`Invalid strategy: ${strategy}. Must be 'all' or 'first-success'`);
113
+ process.exit(1);
114
+ }
115
+ }
116
+
117
+ // Parse --limit
118
+ const limitIdx = args.indexOf("--limit");
119
+ if (limitIdx !== -1) {
120
+ const limitArg = args[limitIdx + 1];
121
+ if (limitArg !== undefined) {
122
+ const limit = parseInt(limitArg, 10);
123
+ if (Number.isNaN(limit) || limit < 1) {
124
+ console.error(`Invalid limit: ${limitArg}. Must be a positive number`);
125
+ process.exit(1);
126
+ }
127
+ options.limit = limit;
128
+ }
129
+ }
130
+
131
+ // Extract query (non-option arguments)
132
+ // Filter out option flags (--*) and their values
133
+ const optionsWithValues = ["--engines", "--strategy", "--limit", "--config"];
134
+ const queryParts = args.filter((arg, idx) => {
135
+ // Skip arguments starting with --
136
+ if (arg.startsWith("--")) {
137
+ return false;
138
+ }
139
+ // Skip special commands
140
+ if (["credits", "health"].includes(arg)) {
141
+ return false;
142
+ }
143
+ // Skip values that follow option flags
144
+ const prevArg = args[idx - 1];
145
+ if (prevArg && optionsWithValues.includes(prevArg)) {
146
+ return false;
147
+ }
148
+ return true;
149
+ });
150
+
151
+ const query = queryParts.join(" ").trim();
152
+
153
+ if (!query) {
154
+ console.error("Error: Query is required");
155
+ console.error("Run with --help for usage information");
156
+ process.exit(1);
157
+ }
158
+
159
+ // Execute search
160
+ async function main() {
161
+ try {
162
+ // Bootstrap the DI container
163
+ const _container = await bootstrapContainer(configPath);
164
+
165
+ const result = await multiSearch(
166
+ {
167
+ query,
168
+ limit: options.limit,
169
+ engines: options.engines,
170
+ includeRaw: options.includeRaw,
171
+ strategy: options.strategy,
172
+ },
173
+ configPath,
174
+ );
175
+
176
+ if (options.json) {
177
+ console.log(JSON.stringify(result, null, 2));
178
+ } else {
179
+ printHumanReadable(result);
180
+ }
181
+ } catch (error) {
182
+ console.error("Search failed:", error instanceof Error ? error.message : String(error));
183
+ process.exit(1);
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Print results in human-readable format
189
+ */
190
+ function printHumanReadable(result: AllSearchOutput) {
191
+ console.log(`\nQuery: "${result.query}"`);
192
+ console.log(`Found ${result.items.length} results\n`);
193
+
194
+ if (result.items.length === 0) {
195
+ console.log("No results found.");
196
+ return;
197
+ }
198
+
199
+ // Group results by engine
200
+ const byEngine = new Map<string, typeof result.items>();
201
+ for (const item of result.items) {
202
+ if (!byEngine.has(item.sourceEngine)) {
203
+ byEngine.set(item.sourceEngine, []);
204
+ }
205
+ byEngine.get(item.sourceEngine)?.push(item);
206
+ }
207
+
208
+ // Print results
209
+ for (const [engineId, items] of byEngine) {
210
+ if (items.length > 0) {
211
+ console.log(`\n${"=".repeat(60)}`);
212
+ console.log(`${engineId} (${items.length} results)`);
213
+ console.log(`${"=".repeat(60)}`);
214
+
215
+ for (let i = 0; i < items.length && i < 5; i++) {
216
+ const item = items[i];
217
+ if (!item) {
218
+ continue;
219
+ }
220
+ console.log(`\n${i + 1}. ${item.title}`);
221
+ console.log(` ${item.url}`);
222
+ if (item.score) {
223
+ console.log(` Score: ${item.score}`);
224
+ }
225
+ if (item.snippet) {
226
+ console.log(
227
+ ` ${item.snippet.substring(0, 200)}${item.snippet.length > 200 ? "..." : ""}`,
228
+ );
229
+ }
230
+ }
231
+
232
+ if (items.length > 5) {
233
+ console.log(`\n ... and ${items.length - 5} more results`);
234
+ }
235
+ }
236
+ }
237
+
238
+ // Print engine status
239
+ console.log(`\n${"=".repeat(60)}`);
240
+ console.log("Engine Status");
241
+ console.log(`${"=".repeat(60)}`);
242
+ for (const attempt of result.enginesTried) {
243
+ const status = attempt.success
244
+ ? "✓ Success"
245
+ : `✗ Failed${attempt.reason ? ` (${attempt.reason})` : ""}`;
246
+ console.log(`${attempt.engineId.padEnd(15)} ${status}`);
247
+ }
248
+
249
+ // Print credit warnings
250
+ if (result.credits) {
251
+ const lowCredits = result.credits.filter((c) => c.remaining < c.quota * 0.2);
252
+ if (lowCredits.length > 0) {
253
+ console.log(`\n⚠️ Low credit warnings:`);
254
+ for (const credit of lowCredits) {
255
+ console.log(` ${credit.engineId}: ${credit.remaining} remaining of ${credit.quota}`);
256
+ }
257
+ }
258
+ }
259
+
260
+ console.log();
261
+ }
262
+
263
+ /**
264
+ * Run health checks on all providers
265
+ */
266
+ async function runHealthChecks(configPath?: string) {
267
+ try {
268
+ // Bootstrap the DI container
269
+ const container = await bootstrapContainer(configPath);
270
+ const registry = container.get<ProviderRegistry>(ServiceKeys.PROVIDER_REGISTRY);
271
+ const providers = registry.list();
272
+
273
+ console.log("\nProvider Health Checks");
274
+ console.log("=".repeat(60));
275
+
276
+ if (providers.length === 0) {
277
+ console.log("No providers are registered.");
278
+ return;
279
+ }
280
+
281
+ const results: Array<{
282
+ engineId: string;
283
+ status: "healthy" | "unhealthy" | "skipped";
284
+ message: string;
285
+ }> = [];
286
+
287
+ for (const provider of providers) {
288
+ const engineId = provider.id;
289
+
290
+ // Check if provider implements lifecycle methods
291
+ if (isLifecycleProvider(provider)) {
292
+ try {
293
+ await provider.healthcheck();
294
+ results.push({
295
+ engineId,
296
+ status: "healthy",
297
+ message: "Health check passed",
298
+ });
299
+ console.log(`✓ ${engineId.padEnd(15)} Healthy`);
300
+ } catch (error) {
301
+ const message = error instanceof Error ? error.message : String(error);
302
+ results.push({
303
+ engineId,
304
+ status: "unhealthy",
305
+ message,
306
+ });
307
+ console.log(`✗ ${engineId.padEnd(15)} Unhealthy - ${message}`);
308
+ }
309
+ } else {
310
+ results.push({
311
+ engineId,
312
+ status: "skipped",
313
+ message: "Health checks not supported",
314
+ });
315
+ console.log(`- ${engineId.padEnd(15)} Skipped (no health check support)`);
316
+ }
317
+ }
318
+
319
+ // Summary
320
+ const healthy = results.filter((r) => r.status === "healthy").length;
321
+ const unhealthy = results.filter((r) => r.status === "unhealthy").length;
322
+ const skipped = results.filter((r) => r.status === "skipped").length;
323
+
324
+ console.log(`\nSummary: ${healthy} healthy, ${unhealthy} unhealthy, ${skipped} skipped`);
325
+
326
+ if (unhealthy > 0) {
327
+ console.log("\n⚠️ Some providers are unhealthy. Check configuration and connectivity.");
328
+ process.exit(1);
329
+ } else {
330
+ console.log("\n✓ All providers are healthy");
331
+ }
332
+
333
+ console.log();
334
+ } catch (error) {
335
+ console.error("Health check failed:", error instanceof Error ? error.message : String(error));
336
+ process.exit(1);
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Show credit status
342
+ */
343
+ async function showCredits(configPath?: string) {
344
+ try {
345
+ const credits = await getCreditStatus(configPath);
346
+
347
+ if (!credits || credits.length === 0) {
348
+ console.log("No credits configured or no engines enabled.");
349
+ return;
350
+ }
351
+
352
+ console.log("\nCredit Status");
353
+ console.log("=".repeat(60));
354
+
355
+ for (const credit of credits) {
356
+ const usedPercent = ((credit.used / credit.quota) * 100).toFixed(1);
357
+ const status = credit.isExhausted
358
+ ? "⚠️ EXHAUSTED"
359
+ : credit.remaining < credit.quota * 0.2
360
+ ? "⚠️ Low"
361
+ : "✓ OK";
362
+
363
+ console.log(`\n${credit.engineId}`);
364
+ console.log(` Used: ${credit.used} / ${credit.quota} (${usedPercent}%)`);
365
+ console.log(` Remaining: ${credit.remaining}`);
366
+ console.log(` Status: ${status}`);
367
+ }
368
+
369
+ console.log();
370
+ } catch (error) {
371
+ console.error(
372
+ "Failed to load credits:",
373
+ error instanceof Error ? error.message : String(error),
374
+ );
375
+ process.exit(1);
376
+ }
377
+ }
378
+
379
+ // Run main
380
+ main();
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Configuration Helper Functions
3
+ *
4
+ * Provides type-safe helpers for defining allsearch configurations.
5
+ * Inspired by Vite's defineConfig pattern.
6
+ */
7
+
8
+ import type { SearchProvider } from "../core/provider";
9
+ import type { PluginDefinition } from "../plugin/types";
10
+ import type {
11
+ BraveConfig,
12
+ EngineConfig,
13
+ EngineConfigBase,
14
+ LinkupConfig,
15
+ AllSearchConfig,
16
+ SearchxngConfig,
17
+ TavilyConfig,
18
+ } from "./types";
19
+
20
+ /**
21
+ * Extended configuration that supports plugins
22
+ */
23
+ export interface ExtendedSearchConfig extends AllSearchConfig {
24
+ /**
25
+ * Custom plugins to register
26
+ * These will be registered before creating providers
27
+ */
28
+ plugins?: PluginDefinition[];
29
+ }
30
+
31
+ /**
32
+ * Configuration factory function type
33
+ * Allows async config creation for loading secrets, etc.
34
+ */
35
+ export type ConfigFactory = () => ExtendedSearchConfig | Promise<ExtendedSearchConfig>;
36
+
37
+ /**
38
+ * Define a allsearch configuration with full type safety
39
+ *
40
+ * @param config - Configuration object or factory function
41
+ * @returns The configuration (passthrough with type inference)
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * // allsearch.config.ts
46
+ * import { defineConfig } from 'allsearch';
47
+ *
48
+ * export default defineConfig({
49
+ * defaultEngineOrder: ['tavily', 'brave'],
50
+ * engines: [
51
+ * defineTavily({
52
+ * id: 'tavily',
53
+ * enabled: true,
54
+ * displayName: 'Tavily Search',
55
+ * apiKeyEnv: 'TAVILY_API_KEY',
56
+ * endpoint: 'https://api.tavily.com/search',
57
+ * searchDepth: 'basic',
58
+ * monthlyQuota: 1000,
59
+ * creditCostPerSearch: 1,
60
+ * lowCreditThresholdPercent: 80,
61
+ * }),
62
+ * ],
63
+ * });
64
+ * ```
65
+ */
66
+ export function defineConfig(config: ExtendedSearchConfig): ExtendedSearchConfig;
67
+ export function defineConfig(factory: ConfigFactory): ConfigFactory;
68
+ export function defineConfig(
69
+ configOrFactory: ExtendedSearchConfig | ConfigFactory,
70
+ ): ExtendedSearchConfig | ConfigFactory {
71
+ return configOrFactory;
72
+ }
73
+
74
+ /**
75
+ * Helper to define a Tavily engine configuration
76
+ */
77
+ export function defineTavily(
78
+ config: Omit<TavilyConfig, "type"> & Partial<Pick<TavilyConfig, "type">>,
79
+ ): TavilyConfig {
80
+ return {
81
+ type: "tavily",
82
+ ...config,
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Helper to define a Brave engine configuration
88
+ */
89
+ export function defineBrave(
90
+ config: Omit<BraveConfig, "type"> & Partial<Pick<BraveConfig, "type">>,
91
+ ): BraveConfig {
92
+ return {
93
+ type: "brave",
94
+ ...config,
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Helper to define a Linkup engine configuration
100
+ */
101
+ export function defineLinkup(
102
+ config: Omit<LinkupConfig, "type"> & Partial<Pick<LinkupConfig, "type">>,
103
+ ): LinkupConfig {
104
+ return {
105
+ type: "linkup",
106
+ ...config,
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Helper to define a SearXNG engine configuration
112
+ */
113
+ export function defineSearchxng(
114
+ config: Omit<SearchxngConfig, "type"> & Partial<Pick<SearchxngConfig, "type">>,
115
+ ): SearchxngConfig {
116
+ return {
117
+ type: "searchxng",
118
+ ...config,
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Generic helper for custom engine types
124
+ *
125
+ * @example
126
+ * ```typescript
127
+ * interface MyCustomConfig extends EngineConfigBase {
128
+ * type: 'my-custom';
129
+ * customOption: string;
130
+ * }
131
+ *
132
+ * const myEngine = defineEngine<MyCustomConfig>({
133
+ * type: 'my-custom',
134
+ * id: 'my-custom',
135
+ * // ...
136
+ * });
137
+ * ```
138
+ */
139
+ export function defineEngine<T extends EngineConfig>(config: T): T {
140
+ return config;
141
+ }
142
+
143
+ /**
144
+ * Helper to define a custom plugin
145
+ *
146
+ * @example
147
+ * ```typescript
148
+ * const myPlugin = definePlugin({
149
+ * type: 'my-search',
150
+ * displayName: 'My Search',
151
+ * hasLifecycle: false,
152
+ * factory: (config) => new MySearchProvider(config),
153
+ * });
154
+ * ```
155
+ */
156
+ export function definePlugin<TConfig extends EngineConfigBase, TProvider extends SearchProvider>(
157
+ plugin: PluginDefinition<TConfig, TProvider>,
158
+ ): PluginDefinition<TConfig, TProvider> {
159
+ return plugin;
160
+ }
161
+
162
+ /**
163
+ * Create a configuration with defaults
164
+ */
165
+ export function createConfig(
166
+ engines: EngineConfig[],
167
+ options: Partial<Omit<ExtendedSearchConfig, "engines">> = {},
168
+ ): ExtendedSearchConfig {
169
+ const enabledEngines = engines.filter((e) => e.enabled);
170
+ return {
171
+ defaultEngineOrder: options.defaultEngineOrder ?? enabledEngines.map((e) => e.id),
172
+ engines,
173
+ storage: options.storage,
174
+ plugins: options.plugins,
175
+ };
176
+ }