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
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Multi-Search Orchestrator
3
+ *
4
+ * Coordinates multiple search providers and implements different search strategies
5
+ */
6
+
7
+ import type { AllSearchConfig } from "../config/types";
8
+ import type { CreditManager, CreditSnapshot } from "./credits";
9
+ import type { ProviderRegistry } from "./provider";
10
+ import type { StrategyContext } from "./strategy/ISearchStrategy";
11
+ import { StrategyFactory } from "./strategy/StrategyFactory";
12
+ import type { EngineId, SearchResultItem } from "./types";
13
+
14
+ export interface AllSearchOptions {
15
+ /** Override the default engine order */
16
+ engineOrderOverride?: EngineId[];
17
+
18
+ /** Maximum results per provider */
19
+ limit?: number;
20
+
21
+ /** Include raw provider responses */
22
+ includeRaw?: boolean;
23
+
24
+ /** Search strategy */
25
+ strategy?: "all" | "first-success";
26
+ }
27
+
28
+ export interface EngineAttempt {
29
+ engineId: EngineId;
30
+ success: boolean;
31
+ reason?: string;
32
+ }
33
+
34
+ export interface OrchestratorResult {
35
+ /** Original query */
36
+ query: string;
37
+
38
+ /** Combined results from all successful providers */
39
+ results: SearchResultItem[];
40
+
41
+ /** Metadata about engine attempts */
42
+ engineAttempts: EngineAttempt[];
43
+
44
+ /** Credit snapshots after the search */
45
+ credits: CreditSnapshot[];
46
+ }
47
+
48
+ export class AllSearchOrchestrator {
49
+ constructor(
50
+ private config: AllSearchConfig,
51
+ private credits: CreditManager,
52
+ private registry: ProviderRegistry,
53
+ ) {}
54
+
55
+ /**
56
+ * Determine the engine order to use for this search
57
+ */
58
+ private getEngineOrder(override?: EngineId[]): EngineId[] {
59
+ if (override?.length) {
60
+ return override;
61
+ }
62
+ return this.config.defaultEngineOrder;
63
+ }
64
+
65
+ /**
66
+ * Run a allsearch with the specified options
67
+ */
68
+ async run(query: string, options: AllSearchOptions = {}): Promise<OrchestratorResult> {
69
+ const order = this.getEngineOrder(options.engineOrderOverride);
70
+ const strategyName = options.strategy ?? "all";
71
+
72
+ if (order.length === 0) {
73
+ throw new Error("No engines configured or selected");
74
+ }
75
+
76
+ // Create strategy using factory
77
+ const strategy = StrategyFactory.createStrategy(strategyName);
78
+
79
+ // Create strategy context
80
+ const context: StrategyContext = {
81
+ creditManager: this.credits,
82
+ providerRegistry: this.registry,
83
+ };
84
+
85
+ // Execute strategy
86
+ const { results, attempts } = await strategy.execute(query, order, options, context);
87
+
88
+ // For 'all' strategy, sort results by score (descending)
89
+ if (strategyName === "all") {
90
+ results.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
91
+ }
92
+
93
+ // Get credit snapshots
94
+ const credits = this.credits.listSnapshots();
95
+
96
+ return {
97
+ query,
98
+ results,
99
+ engineAttempts: attempts,
100
+ credits,
101
+ };
102
+ }
103
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * XDG Base Directory utilities
3
+ *
4
+ * Provides standard paths following XDG Base Directory Specification
5
+ * https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
6
+ */
7
+
8
+ import { copyFileSync, existsSync, mkdirSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { dirname, join } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const APP_NAME = "allsearch";
14
+
15
+ /**
16
+ * Get XDG config home directory
17
+ * Default: ~/.config
18
+ */
19
+ export function getXdgConfigHome(): string {
20
+ return process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
21
+ }
22
+
23
+ /**
24
+ * Get XDG data home directory
25
+ * Default: ~/.local/share
26
+ */
27
+ export function getXdgDataHome(): string {
28
+ return process.env.XDG_DATA_HOME || join(homedir(), ".local", "share");
29
+ }
30
+
31
+ /**
32
+ * Get XDG state home directory
33
+ * Default: ~/.local/state
34
+ */
35
+ export function getXdgStateHome(): string {
36
+ return process.env.XDG_STATE_HOME || join(homedir(), ".local", "state");
37
+ }
38
+
39
+ /**
40
+ * Get app config directory
41
+ * ~/.config/allsearch
42
+ */
43
+ export function getAppConfigDir(): string {
44
+ return join(getXdgConfigHome(), APP_NAME);
45
+ }
46
+
47
+ /**
48
+ * Get app data directory
49
+ * ~/.local/share/allsearch
50
+ */
51
+ export function getAppDataDir(): string {
52
+ return join(getXdgDataHome(), APP_NAME);
53
+ }
54
+
55
+ /**
56
+ * Get SearXNG config directory
57
+ * ~/.config/allsearch/searxng/config
58
+ */
59
+ export function getSearxngConfigDir(): string {
60
+ return join(getAppConfigDir(), "searxng", "config");
61
+ }
62
+
63
+ /**
64
+ * Get SearXNG data directory
65
+ * ~/.local/share/allsearch/searxng/data
66
+ */
67
+ export function getSearxngDataDir(): string {
68
+ return join(getAppDataDir(), "searxng", "data");
69
+ }
70
+
71
+ /**
72
+ * Ensure a directory exists, creating it if necessary
73
+ */
74
+ export function ensureDir(dir: string): void {
75
+ if (!existsSync(dir)) {
76
+ mkdirSync(dir, { recursive: true });
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Get all SearXNG paths and ensure directories exist
82
+ */
83
+ export function getSearxngPaths(): { configDir: string; dataDir: string } {
84
+ const configDir = getSearxngConfigDir();
85
+ const dataDir = getSearxngDataDir();
86
+
87
+ ensureDir(configDir);
88
+ ensureDir(dataDir);
89
+
90
+ return { configDir, dataDir };
91
+ }
92
+
93
+ /**
94
+ * Get package root directory
95
+ * Handles both development (src/) and bundled (dist/) environments
96
+ */
97
+ export function getPackageRoot(): string {
98
+ const currentFile = fileURLToPath(import.meta.url);
99
+ const currentDir = dirname(currentFile);
100
+
101
+ // Check if we're in dist/ or src/
102
+ if (currentDir.includes("/dist")) {
103
+ // Bundled: dist/core/paths.js or dist/cli.js -> find dist root
104
+ const distIndex = currentDir.indexOf("/dist");
105
+ return currentDir.substring(0, distIndex + 5); // Include /dist
106
+ }
107
+ // Development: src/core/paths.ts -> go up 2 levels to package root
108
+ return dirname(dirname(currentDir));
109
+ }
110
+
111
+ /**
112
+ * Get the bundled default settings.yml path
113
+ */
114
+ export function getDefaultSettingsPath(): string {
115
+ const packageRoot = getPackageRoot();
116
+ // In bundled mode, look in dist/providers/searxng/
117
+ // In dev mode, look in providers/searxng/
118
+ if (packageRoot.endsWith("/dist")) {
119
+ return join(packageRoot, "providers", "searxng", "config", "settings.yml");
120
+ }
121
+ return join(packageRoot, "providers", "searxng", "config", "settings.yml");
122
+ }
123
+
124
+ /**
125
+ * Bootstrap SearXNG config by copying default settings if not present
126
+ * @returns true if config was bootstrapped, false if already exists
127
+ */
128
+ export function bootstrapSearxngConfig(): boolean {
129
+ const { configDir } = getSearxngPaths();
130
+ const targetSettings = join(configDir, "settings.yml");
131
+
132
+ // If settings already exist, don't overwrite
133
+ if (existsSync(targetSettings)) {
134
+ return false;
135
+ }
136
+
137
+ const defaultSettings = getDefaultSettingsPath();
138
+
139
+ // If default settings don't exist, we can't bootstrap
140
+ if (!existsSync(defaultSettings)) {
141
+ console.warn(
142
+ `[SearXNG] Default settings not found at ${defaultSettings}. ` +
143
+ `Please create ${targetSettings} manually.`,
144
+ );
145
+ return false;
146
+ }
147
+
148
+ // Copy default settings to XDG config dir
149
+ try {
150
+ copyFileSync(defaultSettings, targetSettings);
151
+ console.log(`[SearXNG] Bootstrapped default config to ${targetSettings}`);
152
+ return true;
153
+ } catch (error) {
154
+ console.error(`[SearXNG] Failed to bootstrap config:`, error);
155
+ return false;
156
+ }
157
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Lifecycle Provider Interface
3
+ *
4
+ * Separates lifecycle concerns (init, healthcheck, shutdown, validation) from search provider contract.
5
+ * Implemented by providers that manage their own infrastructure (Docker containers, local services).
6
+ * NOT implemented by external API providers (Tavily, Brave) that don't require lifecycle management.
7
+ *
8
+ * This interface follows the Interface Segregation Principle (ISP) by providing
9
+ * a focused contract for lifecycle-aware providers, separate from the core SearchProvider interface.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * class DockerSearchProvider implements SearchProvider, ILifecycleProvider {
14
+ * // SearchProvider methods
15
+ * search(query: SearchQuery): Promise<SearchResponse> { ... }
16
+ *
17
+ * // ILifecycleProvider methods
18
+ * async init(): Promise<void> { ... }
19
+ * async healthcheck(): Promise<boolean> { ... }
20
+ * async shutdown(): Promise<void> { ... }
21
+ * async validateConfig(): Promise<ValidationResult> { ... }
22
+ * isLifecycleManaged(): boolean { return true; }
23
+ * }
24
+ * ```
25
+ */
26
+
27
+ /**
28
+ * Validation result for provider configuration
29
+ */
30
+ export interface ValidationResult {
31
+ /** Whether the configuration is valid */
32
+ valid: boolean;
33
+ /** List of validation errors (empty if valid) */
34
+ errors: string[];
35
+ /** List of validation warnings (informational) */
36
+ warnings: string[];
37
+ }
38
+
39
+ /**
40
+ * Interface for providers that manage their own lifecycle (Docker containers, local services)
41
+ *
42
+ * Separates lifecycle management from search functionality, allowing:
43
+ * - Clean separation of concerns (Single Responsibility Principle)
44
+ * - LSP compliance - SearchProvider contract remains pure
45
+ * - Flexibility - can use providers with or without lifecycle management
46
+ * - Better testability - lifecycle can be mocked/stubbed separately
47
+ */
48
+ export interface ILifecycleProvider {
49
+ /**
50
+ * Initialize the provider
51
+ *
52
+ * Performs initialization tasks such as:
53
+ * - Starting Docker containers (if autoStart is enabled)
54
+ * - Establishing connections to external services
55
+ * - Performing health checks
56
+ * - Setting up required infrastructure
57
+ *
58
+ * Should be idempotent - safe to call multiple times.
59
+ * Should handle concurrent calls gracefully.
60
+ *
61
+ * @throws Error if initialization fails critically
62
+ */
63
+ init(): Promise<void>;
64
+
65
+ /**
66
+ * Check if the provider is healthy and ready to serve requests
67
+ *
68
+ * Performs health checks such as:
69
+ * - Container running status (for Docker providers)
70
+ * - Health endpoint responsiveness
71
+ * - Service availability checks
72
+ * - Resource availability validation
73
+ *
74
+ * @returns true if provider is healthy and ready, false otherwise
75
+ */
76
+ healthcheck(): Promise<boolean>;
77
+
78
+ /**
79
+ * Shutdown the provider cleanly
80
+ *
81
+ * Performs cleanup tasks such as:
82
+ * - Stopping Docker containers (if autoStop is enabled)
83
+ * - Closing connections
84
+ * - Releasing resources
85
+ * - Cleanup of temporary files/state
86
+ *
87
+ * Should be graceful - avoid throwing errors unless absolutely necessary.
88
+ * Should handle cases where provider is not running.
89
+ *
90
+ * @throws Error only for critical shutdown failures that need attention
91
+ */
92
+ shutdown(): Promise<void>;
93
+
94
+ /**
95
+ * Validate provider configuration
96
+ *
97
+ * Checks configuration for issues such as:
98
+ * - Docker availability and permissions
99
+ * - Compose file validity and existence
100
+ * - Container name validation
101
+ * - Health endpoint URL validity
102
+ * - Required environment variables
103
+ * - Port availability and conflicts
104
+ *
105
+ * @returns Validation result with errors and warnings
106
+ */
107
+ validateConfig(): Promise<ValidationResult>;
108
+
109
+ /**
110
+ * Check if this provider requires lifecycle management
111
+ *
112
+ * Returns true for providers that manage infrastructure (Docker containers, local services).
113
+ * Returns false for external API providers that don't require lifecycle management.
114
+ *
115
+ * This method allows consumers to determine whether lifecycle methods should be called.
116
+ *
117
+ * @returns true if provider manages its own lifecycle, false for external APIs
118
+ */
119
+ isLifecycleManaged(): boolean;
120
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Provider Factory
3
+ *
4
+ * Factory for creating search provider instances based on configuration.
5
+ * Delegates to PluginRegistry for provider creation, enabling new providers
6
+ * to be added without modifying this code.
7
+ */
8
+
9
+ import type { EngineConfig } from "../../config/types";
10
+ import { areBuiltInPluginsRegistered, PluginRegistry, registerBuiltInPlugins } from "../../plugin";
11
+ import type { Container } from "../container";
12
+ import type { SearchProvider } from "../provider";
13
+
14
+ /**
15
+ * Factory for creating provider instances based on configuration
16
+ *
17
+ * Uses the PluginRegistry to create providers, allowing new provider types
18
+ * to be added by registering plugins rather than modifying factory code.
19
+ *
20
+ * @class ProviderFactory
21
+ * @example
22
+ * ```typescript
23
+ * // Ensure plugins are registered first
24
+ * await ProviderFactory.ensurePluginsRegistered();
25
+ *
26
+ * // Create a provider
27
+ * const provider = ProviderFactory.createProvider(config, container);
28
+ * ```
29
+ */
30
+ let initialized = false;
31
+
32
+ /**
33
+ * Ensure built-in plugins are registered
34
+ * Call this before creating providers if not loading via loadConfig()
35
+ */
36
+ async function ensurePluginsRegistered(): Promise<void> {
37
+ if (!initialized) {
38
+ const registry = PluginRegistry.getInstance();
39
+ if (!areBuiltInPluginsRegistered(registry)) {
40
+ await registerBuiltInPlugins(registry);
41
+ }
42
+ initialized = true;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Synchronously ensure built-in plugins are registered
48
+ * Uses sync registration (no lifecycle hooks called)
49
+ */
50
+ function ensurePluginsRegisteredSync(): void {
51
+ if (!initialized) {
52
+ const registry = PluginRegistry.getInstance();
53
+ if (!areBuiltInPluginsRegistered(registry)) {
54
+ registerBuiltInPlugins(registry);
55
+ }
56
+ initialized = true;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Creates a search provider instance based on the provided configuration
62
+ *
63
+ * @param {EngineConfig} config - The engine configuration (must include 'type' field)
64
+ * @param {Container} container - DI container for dependency injection
65
+ * @returns {SearchProvider} An instance of the requested provider
66
+ * @throws {Error} If the provider type is not registered
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * const provider = ProviderFactory.createProvider(config, container);
71
+ * ```
72
+ */
73
+ function createProvider(config: EngineConfig, container: Container): SearchProvider {
74
+ // Ensure built-in plugins are registered (sync)
75
+ ensurePluginsRegisteredSync();
76
+
77
+ const registry = PluginRegistry.getInstance();
78
+
79
+ return registry.createProvider(config, { container });
80
+ }
81
+
82
+ /**
83
+ * Create multiple providers from an array of configs
84
+ */
85
+ function createProviders(configs: EngineConfig[], container: Container): SearchProvider[] {
86
+ return configs.map((config) => createProvider(config, container));
87
+ }
88
+
89
+ /**
90
+ * Check if a provider type is supported
91
+ */
92
+ function isTypeSupported(type: string): boolean {
93
+ ensurePluginsRegisteredSync();
94
+ return PluginRegistry.getInstance().has(type);
95
+ }
96
+
97
+ /**
98
+ * Get all supported provider types
99
+ */
100
+ function getSupportedTypes(): string[] {
101
+ ensurePluginsRegisteredSync();
102
+ return PluginRegistry.getInstance().getTypes();
103
+ }
104
+
105
+ /**
106
+ * Reset initialization state (for testing)
107
+ */
108
+ function reset(): void {
109
+ initialized = false;
110
+ }
111
+
112
+ export const ProviderFactory = {
113
+ ensurePluginsRegistered,
114
+ ensurePluginsRegisteredSync,
115
+ createProvider,
116
+ createProviders,
117
+ isTypeSupported,
118
+ getSupportedTypes,
119
+ reset,
120
+ };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Provider abstraction layer
3
+ */
4
+
5
+ import type { EngineId, SearchQuery, SearchResponse } from "./types";
6
+
7
+ export interface ProviderMetadata {
8
+ id: EngineId;
9
+ displayName: string;
10
+ docsUrl?: string;
11
+ }
12
+
13
+ /**
14
+ * Abstract interface that all search providers must implement
15
+ */
16
+ export interface SearchProvider {
17
+ readonly id: EngineId;
18
+ getMetadata(): ProviderMetadata;
19
+ search(query: SearchQuery): Promise<SearchResponse>;
20
+ }
21
+
22
+ /**
23
+ * Interface for providers that manage lifecycle (init, healthcheck, shutdown)
24
+ */
25
+ export interface ILifecycleProvider {
26
+ init(): Promise<void>;
27
+ healthcheck(): Promise<boolean>;
28
+ shutdown(): Promise<void>;
29
+ validateConfig(): Promise<{
30
+ valid: boolean;
31
+ errors: string[];
32
+ warnings: string[];
33
+ }>;
34
+ isLifecycleManaged(): boolean;
35
+ }
36
+
37
+ /**
38
+ * Registry to manage all available providers
39
+ */
40
+ export class ProviderRegistry {
41
+ private providers = new Map<EngineId, SearchProvider>();
42
+
43
+ register(provider: SearchProvider): void {
44
+ if (this.providers.has(provider.id)) {
45
+ throw new Error(`Provider already registered: ${provider.id}`);
46
+ }
47
+ this.providers.set(provider.id, provider);
48
+ }
49
+
50
+ get(id: EngineId): SearchProvider | undefined {
51
+ return this.providers.get(id);
52
+ }
53
+
54
+ list(): SearchProvider[] {
55
+ return Array.from(this.providers.values());
56
+ }
57
+
58
+ has(id: EngineId): boolean {
59
+ return this.providers.has(id);
60
+ }
61
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Service Keys for Dependency Injection Container
3
+ *
4
+ * Centralized constants for all service identifiers used with the DI container.
5
+ * Using constants instead of magic strings prevents typos and enables IDE support.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { ServiceKeys } from '../core/serviceKeys';
10
+ *
11
+ * // Register service
12
+ * container.singleton(ServiceKeys.CONFIG, () => config);
13
+ *
14
+ * // Resolve service
15
+ * const config = container.get<AllSearchConfig>(ServiceKeys.CONFIG);
16
+ * ```
17
+ */
18
+
19
+ /**
20
+ * All service keys used in the DI container
21
+ */
22
+ export const ServiceKeys = {
23
+ /** Application configuration */
24
+ CONFIG: "config",
25
+
26
+ /** Credit state persistence provider */
27
+ CREDIT_STATE_PROVIDER: "creditStateProvider",
28
+
29
+ /** Credit manager for tracking usage */
30
+ CREDIT_MANAGER: "creditManager",
31
+
32
+ /** Registry of all search providers */
33
+ PROVIDER_REGISTRY: "providerRegistry",
34
+
35
+ /** Factory for creating search strategies */
36
+ STRATEGY_FACTORY: "strategyFactory",
37
+
38
+ /** Main search orchestrator */
39
+ ORCHESTRATOR: "orchestrator",
40
+ } as const;
41
+
42
+ /**
43
+ * Type representing any valid service key
44
+ */
45
+ export type ServiceKey = (typeof ServiceKeys)[keyof typeof ServiceKeys];