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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ubersearch",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "module": "src/index.ts",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,7 +2,7 @@
2
2
  # Copy this file to .env and update values
3
3
 
4
4
  # Secret key for SearXNG (generate with: openssl rand -hex 32)
5
- SEARXNG_SECRET_KEY=your-secret-key-here
5
+ SEARXNG_SECRET=your-secret-key-here
6
6
 
7
7
  # API key for ubersearch integration
8
8
  # Generate with: openssl rand -hex 16
@@ -9,6 +9,7 @@ import type { EngineConfig, UberSearchConfig } from "../config/types";
9
9
  import { type Container, container } from "../core/container";
10
10
  import { CreditManager } from "../core/credits";
11
11
  import { FileCreditStateProvider } from "../core/credits/FileCreditStateProvider";
12
+ import { getErrorMessage } from "../core/errorUtils";
12
13
  import { createLogger } from "../core/logger";
13
14
  import { UberSearchOrchestrator } from "../core/orchestrator";
14
15
  import type { SearchProvider } from "../core/provider";
@@ -103,8 +104,8 @@ export async function bootstrapContainer(
103
104
  registry.register(provider);
104
105
  log.debug(`Registered provider: ${engineConfig.id}`);
105
106
  } catch (error) {
106
- const errorMsg = error instanceof Error ? error.message : String(error);
107
- log.debug(`Failed to register provider ${engineConfig.id}: ${errorMsg}`);
107
+ const errorMsg = getErrorMessage(error);
108
+ log.warn(`Failed to register provider ${engineConfig.id}: ${errorMsg}`);
108
109
  failedProviders.push(engineConfig.id);
109
110
  }
110
111
  }
package/src/cli.ts CHANGED
@@ -6,10 +6,11 @@
6
6
  */
7
7
 
8
8
  import { bootstrapContainer } from "./bootstrap/container";
9
+ import { getErrorMessage } from "./core/errorUtils";
9
10
  import type { ProviderRegistry } from "./core/provider";
10
11
  import { ServiceKeys } from "./core/serviceKeys";
11
12
  import { serve as serveMcp } from "./mcp-server";
12
- import { isLifecycleProvider } from "./plugin/types.js";
13
+ import { isLifecycleProvider } from "./plugin/types";
13
14
  import type { UberSearchOutput } from "./tool/interface";
14
15
  import { getCreditStatus, uberSearch } from "./tool/uberSearchTool";
15
16
 
@@ -102,12 +103,6 @@ if (args[0] === "health") {
102
103
  process.exit(0);
103
104
  }
104
105
 
105
- // MCP server command
106
- if (args[0] === "mcp") {
107
- await serveMcp();
108
- process.exit(0);
109
- }
110
-
111
106
  // Parse options
112
107
  const options = {
113
108
  json: args.includes("--json"),
@@ -214,7 +209,7 @@ async function main() {
214
209
  printHumanReadable(result);
215
210
  }
216
211
  } catch (error) {
217
- console.error("Search failed:", error instanceof Error ? error.message : String(error));
212
+ console.error("Search failed:", getErrorMessage(error));
218
213
  process.exit(1);
219
214
  }
220
215
  }
@@ -333,7 +328,7 @@ async function runHealthChecks(configPath?: string) {
333
328
  });
334
329
  console.log(`✓ ${engineId.padEnd(15)} Healthy`);
335
330
  } catch (error) {
336
- const message = error instanceof Error ? error.message : String(error);
331
+ const message = getErrorMessage(error);
337
332
  results.push({
338
333
  engineId,
339
334
  status: "unhealthy",
@@ -367,7 +362,7 @@ async function runHealthChecks(configPath?: string) {
367
362
 
368
363
  console.log();
369
364
  } catch (error) {
370
- console.error("Health check failed:", error instanceof Error ? error.message : String(error));
365
+ console.error("Health check failed:", getErrorMessage(error));
371
366
  process.exit(1);
372
367
  }
373
368
  }
@@ -403,10 +398,7 @@ async function showCredits(configPath?: string) {
403
398
 
404
399
  console.log();
405
400
  } catch (error) {
406
- console.error(
407
- "Failed to load credits:",
408
- error instanceof Error ? error.message : String(error),
409
- );
401
+ console.error("Failed to load credits:", getErrorMessage(error));
410
402
  process.exit(1);
411
403
  }
412
404
  }
@@ -14,6 +14,7 @@
14
14
  import { existsSync, readFileSync } from "node:fs";
15
15
  import { homedir } from "node:os";
16
16
  import { dirname, isAbsolute, join } from "node:path";
17
+ import { getErrorMessage } from "../core/errorUtils";
17
18
  import { getBundledSearxngComposePath } from "../core/paths";
18
19
  import { PluginRegistry, registerBuiltInPlugins } from "../plugin";
19
20
  import type { ConfigFactory, ExtendedSearchConfig } from "./defineConfig";
@@ -89,7 +90,10 @@ async function loadTypeScriptConfig(path: string): Promise<ExtendedSearchConfig>
89
90
  const configOrFactory = module.default ?? module.config;
90
91
 
91
92
  if (!configOrFactory) {
92
- throw new Error(`Config file must export a default configuration or named 'config' export`);
93
+ throw new Error(
94
+ `Config file at ${path} must export a config object as 'default' or 'config'. ` +
95
+ `Found exports: ${Object.keys(module).join(", ") || "none"}`,
96
+ );
93
97
  }
94
98
 
95
99
  // Handle factory functions (async config)
@@ -99,9 +103,7 @@ async function loadTypeScriptConfig(path: string): Promise<ExtendedSearchConfig>
99
103
 
100
104
  return configOrFactory as ExtendedSearchConfig;
101
105
  } catch (error) {
102
- throw new Error(
103
- `Failed to load TypeScript config from ${path}: ${error instanceof Error ? error.message : String(error)}`,
104
- );
106
+ throw new Error(`Failed to load TypeScript config from ${path}: ${getErrorMessage(error)}`);
105
107
  }
106
108
  }
107
109
 
@@ -201,12 +203,11 @@ function getDefaultConfig(): ExtendedSearchConfig {
201
203
  type: "searchxng",
202
204
  enabled: true,
203
205
  displayName: "SearXNG (Local)",
204
- apiKeyEnv: "SEARXNG_API_KEY",
205
206
  endpoint: "http://localhost:8888/search",
206
207
  composeFile,
207
208
  containerName: "searxng",
208
209
  healthEndpoint: "http://localhost:8888/healthz",
209
- defaultLimit: 15,
210
+ defaultLimit: 10,
210
211
  monthlyQuota: 10000,
211
212
  creditCostPerSearch: 0,
212
213
  lowCreditThresholdPercent: 80,
@@ -225,7 +226,7 @@ function getDefaultConfig(): ExtendedSearchConfig {
225
226
  displayName: "Brave Search",
226
227
  apiKeyEnv: "BRAVE_API_KEY",
227
228
  endpoint: "https://api.search.brave.com/res/v1/web/search",
228
- defaultLimit: 15,
229
+ defaultLimit: 10,
229
230
  monthlyQuota: 2000,
230
231
  creditCostPerSearch: 1,
231
232
  lowCreditThresholdPercent: 80,
@@ -284,9 +285,7 @@ export async function loadConfig(
284
285
  rawConfig = loadJsonConfig(path);
285
286
  }
286
287
  } catch (error) {
287
- throw new Error(
288
- `Failed to load config file at ${path}: ${error instanceof Error ? error.message : String(error)}`,
289
- );
288
+ throw new Error(`Failed to load config file at ${path}: ${getErrorMessage(error)}`);
290
289
  }
291
290
 
292
291
  // Resolve relative paths (like composeFile) to absolute paths
@@ -359,9 +358,7 @@ export function loadConfigSync(
359
358
  try {
360
359
  rawConfig = loadJsonConfig(path);
361
360
  } catch (error) {
362
- throw new Error(
363
- `Failed to parse config file at ${path}: ${error instanceof Error ? error.message : String(error)}`,
364
- );
361
+ throw new Error(`Failed to parse config file at ${path}: ${getErrorMessage(error)}`);
365
362
  }
366
363
 
367
364
  // Resolve relative paths (like composeFile) to absolute paths
@@ -73,7 +73,28 @@ export const UberSearchConfigSchema = z
73
73
  })
74
74
  .optional(),
75
75
  })
76
- .passthrough();
76
+ .passthrough()
77
+ .refine(
78
+ (config) => {
79
+ const ids = config.engines.map((e: { id: string }) => e.id);
80
+ return new Set(ids).size === ids.length;
81
+ },
82
+ {
83
+ message: "Duplicate engine IDs found in engines array",
84
+ },
85
+ )
86
+ .refine(
87
+ (config) => {
88
+ if (!config.defaultEngineOrder) {
89
+ return true;
90
+ }
91
+ const engineIds = new Set(config.engines.map((e: { id: string }) => e.id));
92
+ return config.defaultEngineOrder.every((id: string) => engineIds.has(id));
93
+ },
94
+ {
95
+ message: "defaultEngineOrder contains engine IDs not defined in engines array",
96
+ },
97
+ );
77
98
 
78
99
  // CLI input schema
79
100
  export const CliInputSchema = z.object({
@@ -112,7 +112,7 @@ export class FileCreditStateProvider implements CreditStateProvider {
112
112
  // Write file using Bun's async API
113
113
  await Bun.write(this.statePath, JSON.stringify(state, null, 2));
114
114
  } catch (error) {
115
- log.warn(`Failed to save credit state to ${this.statePath}:`, error);
115
+ log.error(`Failed to save credit state to ${this.statePath}:`, error);
116
116
  }
117
117
  }
118
118
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { execFile } from "node:child_process";
8
8
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
9
- import { join } from "node:path";
9
+ import { dirname, join } from "node:path";
10
10
  import { promisify } from "node:util";
11
11
  import { getSearxngPaths } from "../paths";
12
12
 
@@ -30,7 +30,8 @@ function getSearxngSecret(configDir: string): string {
30
30
  }
31
31
 
32
32
  // Generate a new secret using crypto
33
- const secret = [...Array(64)].map(() => Math.floor(Math.random() * 16).toString(16)).join("");
33
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
34
+ const secret = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
34
35
  try {
35
36
  writeFileSync(secretFile, secret, { mode: 0o600 });
36
37
  } catch {
@@ -69,7 +70,9 @@ export class DockerComposeHelper {
69
70
  cwd,
70
71
  timeout,
71
72
  env: {
72
- ...process.env,
73
+ PATH: process.env.PATH,
74
+ HOME: process.env.HOME,
75
+ DOCKER_HOST: process.env.DOCKER_HOST,
73
76
  SEARXNG_CONFIG: configDir,
74
77
  SEARXNG_DATA: dataDir,
75
78
  SEARXNG_SECRET: getSearxngSecret(configDir),
@@ -105,7 +108,7 @@ export class DockerComposeHelper {
105
108
  * Get directory containing compose file
106
109
  */
107
110
  private getComposeDir(): string {
108
- return require("node:path").dirname(this.composeFile);
111
+ return dirname(this.composeFile);
109
112
  }
110
113
 
111
114
  /**
@@ -173,13 +176,19 @@ export class DockerComposeHelper {
173
176
  async isRunning(service?: string, options?: DockerComposeOptions): Promise<boolean> {
174
177
  try {
175
178
  const output = await this.ps(options);
179
+ const lines = output.split("\n").filter((l) => l.trim());
176
180
 
177
181
  if (service) {
178
- return output.includes(service) && !output.includes("Exit");
182
+ return lines.some((line) => {
183
+ const lower = line.toLowerCase();
184
+ return (
185
+ lower.includes(service.toLowerCase()) && /\bup\b/i.test(line) && !/exit/i.test(line)
186
+ );
187
+ });
179
188
  }
180
189
 
181
190
  // Check if any services are running
182
- return output.includes("Up");
191
+ return lines.some((line) => /\bup\b/i.test(line));
183
192
  } catch (_error) {
184
193
  return false;
185
194
  }
@@ -20,6 +20,7 @@
20
20
  * ```
21
21
  */
22
22
 
23
+ import { getErrorMessage } from "../errorUtils";
23
24
  import { createLogger } from "../logger";
24
25
  import { bootstrapSearxngConfig } from "../paths";
25
26
  import { DockerComposeHelper } from "./dockerComposeHelper";
@@ -71,7 +72,7 @@ export class DockerLifecycleManager {
71
72
  }
72
73
 
73
74
  this.initPromise = this.performInit().catch((error) => {
74
- const message = error instanceof Error ? error.message : String(error);
75
+ const message = getErrorMessage(error);
75
76
  log.debug("Initialization failed:", message);
76
77
  this.initialized = false;
77
78
  throw error;
@@ -88,11 +89,19 @@ export class DockerLifecycleManager {
88
89
  timeoutMs: number,
89
90
  operation: string,
90
91
  ): Promise<T> {
92
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
91
93
  const timeoutPromise = new Promise<never>((_, reject) => {
92
- setTimeout(() => reject(new Error(`${operation} timed out after ${timeoutMs}ms`)), timeoutMs);
94
+ timeoutId = setTimeout(
95
+ () => reject(new Error(`${operation} timed out after ${timeoutMs}ms`)),
96
+ timeoutMs,
97
+ );
93
98
  });
94
99
 
95
- return Promise.race([promise, timeoutPromise]);
100
+ try {
101
+ return await Promise.race([promise, timeoutPromise]);
102
+ } finally {
103
+ clearTimeout(timeoutId);
104
+ }
96
105
  }
97
106
 
98
107
  private async performInit(): Promise<void> {
@@ -141,7 +150,7 @@ export class DockerLifecycleManager {
141
150
 
142
151
  this.initialized = true;
143
152
  } catch (error) {
144
- const message = error instanceof Error ? error.message : String(error);
153
+ const message = getErrorMessage(error);
145
154
  log.debug("Failed to start container:", message);
146
155
  throw error;
147
156
  } finally {
@@ -171,8 +180,11 @@ export class DockerLifecycleManager {
171
180
  if (response.ok) {
172
181
  return true;
173
182
  }
174
- } catch {
175
- // Health endpoint failed, fall through to Docker check
183
+ log.debug(`Health endpoint returned non-ok status: ${response.status}`);
184
+ } catch (error) {
185
+ const msg = getErrorMessage(error);
186
+ log.debug(`Health endpoint check failed: ${msg}`);
187
+ // Fall through to Docker status check
176
188
  } finally {
177
189
  clearTimeout(timeoutId);
178
190
  }
@@ -243,7 +255,7 @@ export class DockerLifecycleManager {
243
255
  );
244
256
  log.debug("Container stopped.");
245
257
  } catch (error) {
246
- const message = error instanceof Error ? error.message : String(error);
258
+ const message = getErrorMessage(error);
247
259
  log.debug("Failed to stop container:", message);
248
260
  // Don't throw on shutdown errors
249
261
  }
@@ -306,7 +318,7 @@ export class DockerLifecycleManager {
306
318
  const projectRoot = this.config.projectRoot || process.cwd();
307
319
  await this.dockerHelper.ps({ cwd: projectRoot });
308
320
  } catch (error: unknown) {
309
- const message = error instanceof Error ? error.message : String(error);
321
+ const message = getErrorMessage(error);
310
322
  if (message.includes("config")) {
311
323
  errors.push(`Invalid compose file: ${message}`);
312
324
  }
@@ -357,7 +369,7 @@ export class DockerLifecycleManager {
357
369
  cwd: projectRoot,
358
370
  });
359
371
  } catch (error) {
360
- const message = error instanceof Error ? error.message : String(error);
372
+ const message = getErrorMessage(error);
361
373
  log.debug("Error checking if container is running:", message);
362
374
  return false;
363
375
  }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Safely extracts an error message from an unknown error value.
3
+ */
4
+ export function getErrorMessage(error: unknown): string {
5
+ return error instanceof Error ? error.message : String(error);
6
+ }
@@ -7,32 +7,11 @@
7
7
  import type { UberSearchConfig } from "../config/types";
8
8
  import type { CreditManager, CreditSnapshot } from "./credits";
9
9
  import type { ProviderRegistry } from "./provider";
10
- import type { StrategyContext } from "./strategy/ISearchStrategy";
10
+ import type { EngineAttempt, StrategyContext, UberSearchOptions } from "./strategy/ISearchStrategy";
11
11
  import { StrategyFactory } from "./strategy/StrategyFactory";
12
12
  import type { EngineId, SearchResultItem } from "./types";
13
13
 
14
- export interface UberSearchOptions {
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
- /** SearXNG categories to search (e.g., ["general", "it", "science"]) */
28
- categories?: string[];
29
- }
30
-
31
- export interface EngineAttempt {
32
- engineId: EngineId;
33
- success: boolean;
34
- reason?: string;
35
- }
14
+ export type { UberSearchOptions, EngineAttempt };
36
15
 
37
16
  export interface OrchestratorResult {
38
17
  /** Original query */
@@ -89,11 +68,6 @@ export class UberSearchOrchestrator {
89
68
  // Execute strategy
90
69
  const { results, attempts } = await strategy.execute(query, order, options, context);
91
70
 
92
- // For 'all' strategy, sort results by score (descending)
93
- if (strategyName === "all") {
94
- results.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
95
- }
96
-
97
71
  // Get credit snapshots
98
72
  const credits = this.credits.listSnapshots();
99
73
 
package/src/core/paths.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
6
6
  */
7
7
 
8
- import { copyFileSync, existsSync, mkdirSync } from "node:fs";
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
9
  import { homedir } from "node:os";
10
10
  import { dirname, join, resolve } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
@@ -170,11 +170,6 @@ export function bootstrapSearxngConfig(): boolean {
170
170
  const { configDir } = getSearxngPaths();
171
171
  const targetSettings = join(configDir, "settings.yml");
172
172
 
173
- // If settings already exist, don't overwrite
174
- if (existsSync(targetSettings)) {
175
- return false;
176
- }
177
-
178
173
  const defaultSettings = getDefaultSettingsPath();
179
174
 
180
175
  // If default settings don't exist, we can't bootstrap
@@ -186,12 +181,16 @@ export function bootstrapSearxngConfig(): boolean {
186
181
  return false;
187
182
  }
188
183
 
189
- // Copy default settings to XDG config dir
184
+ // Use exclusive flag to prevent TOCTOU race and avoid overwriting
190
185
  try {
191
- copyFileSync(defaultSettings, targetSettings);
186
+ const content = readFileSync(defaultSettings);
187
+ writeFileSync(targetSettings, content, { flag: "wx" });
192
188
  console.log(`[SearXNG] Bootstrapped default config to ${targetSettings}`);
193
189
  return true;
194
190
  } catch (error) {
191
+ if ((error as NodeJS.ErrnoException)?.code === "EEXIST") {
192
+ return false; // File already exists, no action needed
193
+ }
195
194
  console.error(`[SearXNG] Failed to bootstrap config:`, error);
196
195
  return false;
197
196
  }
@@ -23,20 +23,7 @@ export interface SearchProvider {
23
23
  getMissingConfigMessage(): string;
24
24
  }
25
25
 
26
- /**
27
- * Interface for providers that manage lifecycle (init, healthcheck, shutdown)
28
- */
29
- export interface ILifecycleProvider {
30
- init(): Promise<void>;
31
- healthcheck(): Promise<boolean>;
32
- shutdown(): Promise<void>;
33
- validateConfig(): Promise<{
34
- valid: boolean;
35
- errors: string[];
36
- warnings: string[];
37
- }>;
38
- isLifecycleManaged(): boolean;
39
- }
26
+ export type { ILifecycleProvider } from "./provider/ILifecycleProvider";
40
27
 
41
28
  /**
42
29
  * Registry to manage all available providers
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Retry Logic for Provider Operations
3
+ *
4
+ * Provides exponential backoff retry functionality for resilient
5
+ * provider operations
6
+ */
7
+
8
+ import type { EngineId } from "./types";
9
+ import { SearchError } from "./types";
10
+
11
+ /**
12
+ * Retry configuration
13
+ */
14
+ export interface RetryConfig {
15
+ /** Maximum number of retry attempts */
16
+ maxAttempts?: number;
17
+
18
+ /** Initial delay between retries in milliseconds */
19
+ initialDelayMs?: number;
20
+
21
+ /** Multiplier for exponential backoff */
22
+ backoffMultiplier?: number;
23
+
24
+ /** Maximum delay between retries in milliseconds */
25
+ maxDelayMs?: number;
26
+
27
+ /** Whether to retry on specific error types */
28
+ retryableErrors?: Array<"network_error" | "api_error" | "rate_limit" | "no_results" | "timeout">;
29
+ }
30
+
31
+ /**
32
+ * Default retry configuration
33
+ */
34
+ export const DEFAULT_RETRY_CONFIG: Required<RetryConfig> = {
35
+ maxAttempts: 3,
36
+ initialDelayMs: 1000,
37
+ backoffMultiplier: 2,
38
+ maxDelayMs: 10000,
39
+ retryableErrors: ["network_error", "api_error", "rate_limit", "no_results"],
40
+ };
41
+
42
+ /**
43
+ * Execute a function with retry logic and exponential backoff
44
+ *
45
+ * @param engineId - Engine ID for error messages
46
+ * @param fn - Function to execute with retry logic
47
+ * @param config - Retry configuration
48
+ * @returns Result of the function
49
+ * @throws SearchError if all retry attempts fail
50
+ */
51
+ export async function withRetry<T>(
52
+ engineId: EngineId,
53
+ fn: () => Promise<T>,
54
+ config: RetryConfig = {},
55
+ ): Promise<T> {
56
+ // Skip retries in test environment to avoid timeouts
57
+ if (process.env.DISABLE_RETRY === "true") {
58
+ return fn();
59
+ }
60
+
61
+ const {
62
+ maxAttempts = DEFAULT_RETRY_CONFIG.maxAttempts,
63
+ initialDelayMs = DEFAULT_RETRY_CONFIG.initialDelayMs,
64
+ backoffMultiplier = DEFAULT_RETRY_CONFIG.backoffMultiplier,
65
+ maxDelayMs = DEFAULT_RETRY_CONFIG.maxDelayMs,
66
+ retryableErrors = DEFAULT_RETRY_CONFIG.retryableErrors,
67
+ } = config;
68
+
69
+ let lastError: unknown;
70
+ let delay = initialDelayMs;
71
+
72
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
73
+ try {
74
+ return await fn();
75
+ } catch (error) {
76
+ lastError = error;
77
+
78
+ if (!(error instanceof SearchError)) {
79
+ throw error;
80
+ }
81
+
82
+ const shouldRetry = retryableErrors.includes(
83
+ error.reason as "network_error" | "api_error" | "rate_limit" | "no_results",
84
+ );
85
+
86
+ if (!shouldRetry || attempt >= maxAttempts) {
87
+ throw error;
88
+ }
89
+
90
+ const nextDelay = Math.min(delay * backoffMultiplier, maxDelayMs);
91
+
92
+ console.warn(
93
+ `[${engineId}] Attempt ${attempt}/${maxAttempts} failed: ${error.message}. Retrying in ${nextDelay}ms...`,
94
+ );
95
+
96
+ await new Promise((resolve) => setTimeout(resolve, nextDelay));
97
+ delay = nextDelay;
98
+ }
99
+ }
100
+
101
+ throw lastError;
102
+ }