omnirush 0.4.1 → 0.4.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.
@@ -0,0 +1,144 @@
1
+ // Omnirush retry layer — shared by every Omnirush API call path (model
2
+ // stream via the sota tap, /collect uploads, /device and /device/me).
3
+ //
4
+ // Goal: users ride out a backend deploy or a rate-limit window without
5
+ // seeing a single raw error. Retryable = 429 / 5xx / network failures.
6
+ // 4xx are PROTOCOL responses (401 auth, 428 pending, 400 expired) and are
7
+ // never retried here — their handling lives at the call sites.
8
+ //
9
+ // Plain ESM JavaScript on node builtins only (same constraint as
10
+ // auth.js): loaded directly by node:test and as a sibling import from
11
+ // the TypeScript extensions through jiti.
12
+ //
13
+ // Env knobs (tests + advanced users):
14
+ // OMNIRUSH_RETRY_ATTEMPTS total attempts (default 5, min 1)
15
+ // OMNIRUSH_RETRY_BASE_MS first backoff (default 800)
16
+ // OMNIRUSH_RETRY_MAX_MS per-wait cap before jitter (default 10000)
17
+ //
18
+ // SECURITY: nothing here ever logs a token; error strings carry only
19
+ // statuses and server `detail` fragments sanitized by the callers.
20
+
21
+ /** 429 and 5xx are transient; every 4xx is a protocol answer. */
22
+ export function isRetryableStatus(status) {
23
+ return status === 429 || (status >= 500 && status <= 599);
24
+ }
25
+
26
+ /** Network failures (fetch throws) are transient; aborts are not. */
27
+ export function isRetryableError(error) {
28
+ if (!error) return false;
29
+ if (error.name === "AbortError" || error?.code === "ABORT_ERR") return false;
30
+ if (error.name === "TimeoutError") return true; // AbortSignal.timeout
31
+ return true; // fetch TypeError/DNS/ECONNRESET/... — all transient
32
+ }
33
+
34
+ function intFromEnv(name, fallback, min, max) {
35
+ const raw = Number(process.env[name]);
36
+ if (!Number.isFinite(raw)) return fallback;
37
+ return Math.min(Math.max(Math.round(raw), min), max);
38
+ }
39
+
40
+ export function retryAttempts() {
41
+ return intFromEnv("OMNIRUSH_RETRY_ATTEMPTS", 5, 1, 10);
42
+ }
43
+
44
+ export function retryBaseMs() {
45
+ return intFromEnv("OMNIRUSH_RETRY_BASE_MS", 800, 0, 60_000);
46
+ }
47
+
48
+ export function retryMaxMs() {
49
+ return intFromEnv("OMNIRUSH_RETRY_MAX_MS", 10_000, 0, 120_000);
50
+ }
51
+
52
+ /**
53
+ * Exponential backoff with full jitter: wait = random(0, min(cap,
54
+ * base * 2^(attempt-1))). `retryAfterSec` (Retry-After header) wins when
55
+ * present, capped at maxMs. attempt is 1-based (the attempt that just
56
+ * failed); attempt 1 failure waits random(0, base).
57
+ */
58
+ export function computeBackoffMs(attempt, { baseMs, maxMs, retryAfterSec } = {}) {
59
+ const base = baseMs ?? retryBaseMs();
60
+ const cap = maxMs ?? retryMaxMs();
61
+ if (cap <= 0 || base <= 0) return 0;
62
+ if (Number.isFinite(retryAfterSec) && retryAfterSec > 0) {
63
+ return Math.min(Math.round(retryAfterSec * 1000), Math.max(cap, base));
64
+ }
65
+ const exponential = Math.min(cap, base * 2 ** Math.max(0, attempt - 1));
66
+ return Math.round(Math.random() * exponential);
67
+ }
68
+
69
+ /** Local correlation id for a failure sequence: omr-<8 hex>. */
70
+ export function newCorrelationId(now = Date.now()) {
71
+ const seed = `${now}:${Math.random()}:${process.pid}`;
72
+ let hash = 0x811c9dc5;
73
+ for (let i = 0; i < seed.length; i++) {
74
+ hash ^= seed.charCodeAt(i);
75
+ hash = Math.imul(hash, 0x01000193);
76
+ }
77
+ const tail = (hash >>> 0).toString(16).padStart(8, "0");
78
+ return `omr-${tail.slice(0, 8)}`;
79
+ }
80
+
81
+ /** The final human summary shown after every attempt failed. */
82
+ export function formatUnavailableSummary({ ref, attempts, waitedMs, status } = {}) {
83
+ const waited = Number.isFinite(waitedMs) && waitedMs > 0
84
+ ? ` (waited ${Math.round(waitedMs / 1000)}s)`
85
+ : "";
86
+ const cause = status === 429
87
+ ? "Omnirush is at capacity right now"
88
+ : "Omnirush is briefly unavailable";
89
+ return (
90
+ `${cause} and could not complete this request — tried ${attempts} time${attempts === 1 ? "" : "s"}${waited}. ` +
91
+ `Please retry in a moment; your grant is not affected. [ref ${ref ?? "omr-?????"}]`
92
+ );
93
+ }
94
+
95
+ /**
96
+ * Run `tryFn` with retries. `tryFn(attempt)` returns whatever the caller
97
+ * considers a result; classification is up to the caller via:
98
+ * isRetryable(result, error) — decide whether attempt failed transiently
99
+ * `tryFn` may return { retryAfterSec } alongside its result to honor the
100
+ * header. onRetry({attempt, delayMs, result, error, ref}) observes each
101
+ * waiting round (progress lines). Resolves with the first non-retryable
102
+ * result; when attempts are exhausted it resolves with the LAST result
103
+ * if `throwOnExhausted` is false (default) — callers turn that into
104
+ * their sanitized error — otherwise throws the last error.
105
+ * `sleep` is injectable for tests (default real timer).
106
+ */
107
+ export async function withRetries(tryFn, {
108
+ attempts = retryAttempts(),
109
+ isRetryable,
110
+ onRetry,
111
+ sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
112
+ } = {}) {
113
+ const ref = newCorrelationId();
114
+ const startedAt = Date.now();
115
+ let lastResult;
116
+ let lastError;
117
+ for (let attempt = 1; attempt <= attempts; attempt++) {
118
+ lastResult = undefined;
119
+ lastError = undefined;
120
+ try {
121
+ lastResult = await tryFn(attempt, ref);
122
+ } catch (error) {
123
+ lastError = error;
124
+ }
125
+ const transient = lastError
126
+ ? isRetryableError(lastError)
127
+ : isRetryable?.(lastResult) ?? false;
128
+ if (!transient || attempt === attempts) {
129
+ return {
130
+ ok: !lastError && !transient,
131
+ result: lastResult,
132
+ error: lastError,
133
+ ref,
134
+ attempts: attempt,
135
+ waitedMs: Date.now() - startedAt,
136
+ };
137
+ }
138
+ const delayMs = computeBackoffMs(attempt, { retryAfterSec: lastResult?.retryAfterSec });
139
+ onRetry?.({ attempt, nextAttempt: attempt + 1, attempts, delayMs, ref, result: lastResult, error: lastError });
140
+ if (delayMs > 0) await sleep(delayMs);
141
+ }
142
+ // Unreachable (loop returns), kept for shape completeness.
143
+ return { ok: false, result: lastResult, error: lastError, ref, attempts, waitedMs: Date.now() - startedAt };
144
+ }
@@ -60,3 +60,67 @@ export function formatSotaWarning(expected: string, served: string): string {
60
60
  export function isSotaViolation(expected: string, served: string): boolean {
61
61
  return served !== expected;
62
62
  }
63
+
64
+ // --- gateway error sanitization --------------------------------------------
65
+
66
+ /**
67
+ * Upstream providers leak support URLs and request ids through the
68
+ * gateway ("You have hit your usage limit... help.openai.com...
69
+ * request ID: abc"). That text must never surface to omnirush users —
70
+ * it is wrong (our grants, not the user's OpenAI account), confusing,
71
+ * and leaks our upstream. Every non-OK gateway body is replaced with an
72
+ * omnirush message; the raw body stays available only behind
73
+ * OMNIRUSH_DEBUG=1 (stderr) and is traceable via the local ref.
74
+ */
75
+
76
+ /** Heuristic: does this body look like leaked upstream/provider text? */
77
+ export function looksLikeUpstreamLeak(bodyText: string): boolean {
78
+ if (!bodyText) return false;
79
+ return /help\.openai\.com|platform\.openai\.com|openai\.com|anthropic\.com|request[ _-]?id|please visit|rate[ _-]?limit exceeded|billing/i.test(
80
+ bodyText,
81
+ );
82
+ }
83
+
84
+ /** Friendly per-status message shown when the gateway fails. */
85
+ export function friendlyGatewayMessage(status: number, ref: string, attempts: number, waitedMs: number): string {
86
+ const tried = ` (tried ${attempts} time${attempts === 1 ? "" : "s"}${waitedMs > 0 ? `, waited ${Math.round(waitedMs / 1000)}s` : ""})`;
87
+ if (status === 429) {
88
+ return `Omnirush is at capacity right now${tried} — please retry in a moment; your grant is not affected. [ref ${ref}]`;
89
+ }
90
+ if (status === 401 || status === 403) {
91
+ return `Omnirush could not authenticate this request${tried} — run \`omnirush login\` again. [ref ${ref}]`;
92
+ }
93
+ if (status === 408 || status === 504) {
94
+ return `Omnirush timed out serving this request${tried} — please retry. [ref ${ref}]`;
95
+ }
96
+ return `Omnirush is briefly unavailable and could not complete this request${tried} — please retry in a moment. [ref ${ref}]`;
97
+ }
98
+
99
+ /**
100
+ * Build the SANITIZED error body the agent layer gets to see. It is
101
+ * PLAIN TEXT (not JSON) on purpose: the OpenAI SDK surfaces non-JSON
102
+ * bodies verbatim as the error message, so pi shows exactly our
103
+ * sentence — a JSON body would be re-stringified into the display by
104
+ * the provider error formatter. `debug` (OMNIRUSH_DEBUG=1) passes the
105
+ * original body through instead — full detail for support, never for
106
+ * users.
107
+ */
108
+ export function sanitizeGatewayErrorBody({
109
+ status,
110
+ bodyText,
111
+ ref,
112
+ attempts,
113
+ waitedMs,
114
+ debug,
115
+ }: {
116
+ status: number;
117
+ bodyText: string;
118
+ ref: string;
119
+ attempts: number;
120
+ waitedMs: number;
121
+ debug: boolean;
122
+ }): { bodyText: string; sanitized: boolean } {
123
+ if (debug) return { bodyText, sanitized: false };
124
+ const message = friendlyGatewayMessage(status, ref, attempts, waitedMs);
125
+ return { bodyText: message, sanitized: true };
126
+ }
@@ -11,6 +11,14 @@
11
11
  // - on a 401 from the gateway, runs the single-flight device-token
12
12
  // refresh (rotate both tokens, persisted 0600) and retries the
13
13
  // request exactly once — the GUI gateway-broker pattern.
14
+ // - on 429 / 5xx / network failures, retries with exponential backoff
15
+ // + full jitter (shared retry layer, "attempt 2/5" progress on
16
+ // stderr) so users ride out backend deploys.
17
+ // - SANITIZES every non-OK response body: raw upstream provider text
18
+ // ("help.openai.com… request ID…") never reaches the user — the
19
+ // agent layer gets an omnirush message with a local correlation id
20
+ // ([ref omr-…]); the raw body passes through only with
21
+ // OMNIRUSH_DEBUG=1.
14
22
  //
15
23
  // Registered during the extension factory, so the provider is queued and
16
24
  // becomes the composition base at runner init: every omnirush request is
@@ -18,13 +26,20 @@
18
26
 
19
27
  import fs from "node:fs";
20
28
  import { openAIResponsesApi } from "@earendil-works/pi-ai";
29
+ import { recordGatewayUsage } from "./usage";
21
30
  import { sharedRefresher } from "./refresh";
22
31
  import {
23
32
  createSseDataScanner,
24
33
  formatSotaWarning,
34
+ sanitizeGatewayErrorBody,
25
35
  servedModelFromSseData,
26
36
  } from "./sota-lib";
27
- import { recordGatewayUsage } from "./usage";
37
+ import {
38
+ formatUnavailableSummary,
39
+ isRetryableStatus,
40
+ retryAttempts,
41
+ withRetries,
42
+ } from "./retry";
28
43
 
29
44
  const PROVIDER_ID = "omnirush";
30
45
  const GATEWAY_DEFAULT = "https://omnirush.ai/omnirush/v1";
@@ -121,13 +136,44 @@ function withBearer(init: any, token: string): any {
121
136
  return { ...(init ?? {}), headers };
122
137
  }
123
138
 
139
+ /** Read a non-OK body for sanitization/debug (bounded: first 64 KiB). */
140
+ async function readErrorBody(response: any): Promise<string> {
141
+ try {
142
+ const reader = response?.body?.getReader?.();
143
+ if (!reader) return "";
144
+ const chunks: any[] = [];
145
+ let total = 0;
146
+ for (;;) {
147
+ const { done, value } = await reader.read();
148
+ if (done) break;
149
+ chunks.push(value);
150
+ total += value?.byteLength ?? 0;
151
+ if (total >= 64 * 1024) {
152
+ reader.cancel().catch(() => undefined);
153
+ break;
154
+ }
155
+ }
156
+ return new TextDecoder().decode(Buffer.concat(chunks));
157
+ } catch {
158
+ return "";
159
+ }
160
+ }
161
+
124
162
  /**
125
163
  * Wrap a fetch implementation so that:
126
164
  * - 401 responses trigger one single-flight device-token refresh and a
127
- * single retry with the rotated token (gateway-broker pattern), and
165
+ * single retry with the rotated token (gateway-broker pattern),
166
+ * - 429 / 5xx / network failures retry with exponential backoff + full
167
+ * jitter (progress lines on stderr), and
128
168
  * - SSE response bodies stream through a pass-through tap that observes
129
- * the served model. Bytes are forwarded unmodified and non-SSE
130
- * responses (errors, JSON) are returned as-is.
169
+ * the served model; non-OK response bodies are SANITIZED (upstream
170
+ * provider text never reaches the user; OMNIRUSH_DEBUG=1 passes it
171
+ * through with a stderr copy).
172
+ *
173
+ * Retries only cover the request up to (and including) the response
174
+ * headers — once an OK response body starts streaming it is forwarded
175
+ * untouched; a mid-stream failure surfaces as the provider's own error.
176
+ * Client aborts (Esc) are never retried.
131
177
  */
132
178
  function tapFetch(
133
179
  baseFetch: any,
@@ -135,33 +181,118 @@ function tapFetch(
135
181
  onViolation: (expected: string, served: string) => void,
136
182
  ): any {
137
183
  return async (input: any, init: any) => {
138
- let response = await baseFetch(input, init);
139
- debug(`provider fetch ${typeof input === "string" ? input : input?.url} -> ${response?.status}`);
140
- // Capture the gateway's grant/usage headers (x-omnirush-* plus the
141
- // x-ratelimit-*-tokens grant pair) straight from the raw response —
142
- // guaranteed availability here, independent of pi's event plumbing.
143
- try {
144
- recordGatewayUsage(response?.headers);
145
- } catch {
146
- /* usage capture must never break the request */
147
- }
148
- if (response?.status === 401) {
149
- const tokenUsed = bearerFromInit(init) || bearerFromInit(input);
150
- debug(`401 seen; bearer present: ${Boolean(tokenUsed)}`);
151
- if (tokenUsed) {
152
- const refresher = sharedRefresher();
153
- const refreshed = await refresher.refresh(tokenUsed).catch((error) => {
154
- debug(`refresh threw: ${error?.message ?? error}`);
155
- return false;
156
- });
157
- const next = refresher.auth?.accessToken;
158
- debug(`refresh ok: ${refreshed}, next token present: ${Boolean(next)}`);
159
- if (refreshed && next) {
160
- response = await baseFetch(input, withBearer(init, next));
161
- debug(`retry status: ${response?.status}`);
184
+ const requestUrl = typeof input === "string" ? input : input?.url;
185
+ const doFetch = (token?: string) =>
186
+ baseFetch(input, token ? withBearer(init, token) : init);
187
+
188
+ const outcome = await withRetries(
189
+ async (attempt, ref) => {
190
+ let response = await doFetch();
191
+ debug(`provider fetch ${requestUrl} -> ${response?.status} (attempt ${attempt})`);
192
+ // Capture the gateway's grant/usage headers (x-omnirush-* plus
193
+ // the x-ratelimit-*-tokens grant pair) straight from the raw
194
+ // response — guaranteed availability here, independent of pi's
195
+ // event plumbing.
196
+ try {
197
+ recordGatewayUsage(response?.headers);
198
+ } catch {
199
+ /* usage capture must never break the request */
162
200
  }
201
+ if (response?.status === 401) {
202
+ const tokenUsed = bearerFromInit(init) || bearerFromInit(input);
203
+ debug(`401 seen; bearer present: ${Boolean(tokenUsed)}`);
204
+ if (tokenUsed) {
205
+ const refresher = sharedRefresher();
206
+ const refreshed = await refresher.refresh(tokenUsed).catch((error: any) => {
207
+ debug(`refresh threw: ${error?.message ?? error}`);
208
+ return false;
209
+ });
210
+ const next = refresher.auth?.accessToken;
211
+ debug(`refresh ok: ${refreshed}, next token present: ${Boolean(next)}`);
212
+ if (refreshed && next) {
213
+ response = await doFetch(next);
214
+ debug(`retry-after-refresh status: ${response?.status}`);
215
+ try {
216
+ recordGatewayUsage(response?.headers);
217
+ } catch {
218
+ /* as above */
219
+ }
220
+ }
221
+ }
222
+ }
223
+ const retryAfterSec = Number(response?.headers?.get?.("retry-after")) || undefined;
224
+ return { response, ref, retryAfterSec };
225
+ },
226
+ {
227
+ attempts: retryAttempts(),
228
+ isRetryable: ({ response }: any) => {
229
+ if (init?.signal?.aborted) return false;
230
+ return isRetryableStatus(response?.status);
231
+ },
232
+ onRetry: ({ attempt, attempts, delayMs, error }) => {
233
+ const cause = error
234
+ ? "network error"
235
+ : "gateway busy";
236
+ warnStderr(
237
+ `Omnirush is briefly unavailable — ${cause}; retrying (attempt ${attempt + 1}/${attempts})…`,
238
+ );
239
+ debug(`retry ${attempt + 1}/${attempts} in ${delayMs}ms`);
240
+ },
241
+ },
242
+ );
243
+
244
+ let { response, ref, attempts, waitedMs } = {
245
+ response: outcome.result?.response,
246
+ ref: outcome.ref,
247
+ attempts: outcome.attempts,
248
+ waitedMs: outcome.waitedMs,
249
+ };
250
+
251
+ if (!response) {
252
+ // Client aborts (Esc) must surface as aborts, never as errors.
253
+ if (outcome.error && (outcome.error.name === "AbortError" || outcome.error?.code === "ABORT_ERR")) {
254
+ throw outcome.error;
255
+ }
256
+ // Every attempt threw (network dead). Synthesize a sanitized
257
+ // response so the provider layer shows our message, not a stack.
258
+ const message = formatUnavailableSummary({ ref, attempts, waitedMs });
259
+ warnStderr(`Omnirush: ${message}`);
260
+ return new Response(
261
+ JSON.stringify({ error: { message, type: "omnirush_error", code: "network" } }),
262
+ { status: 503, headers: { "content-type": "application/json" } },
263
+ );
264
+ }
265
+
266
+ if (!response.ok && response.status >= 400) {
267
+ // Error body: sanitize (raw upstream provider text must never
268
+ // surface). OMNIRUSH_DEBUG=1 passes the raw body through and
269
+ // logs it to stderr with the correlation id.
270
+ const raw = await readErrorBody(response);
271
+ const debugRaw = process.env.OMNIRUSH_DEBUG === "1";
272
+ if (debugRaw) {
273
+ warnStderr(`omnirush debug: gateway error ${response.status} [ref ${ref}] body: ${raw.slice(0, 2048)}`);
163
274
  }
275
+ const { bodyText } = sanitizeGatewayErrorBody({
276
+ status: response.status,
277
+ bodyText: raw,
278
+ ref,
279
+ attempts,
280
+ waitedMs,
281
+ debug: debugRaw,
282
+ });
283
+ response = new Response(bodyText, {
284
+ status: response.status,
285
+ statusText: response.statusText,
286
+ headers: new Headers({
287
+ "content-type": "text/plain; charset=utf-8",
288
+ ...(response.headers?.get?.("retry-after")
289
+ ? { "retry-after": response.headers.get("retry-after")! }
290
+ : {}),
291
+ }),
292
+ });
293
+ return response;
164
294
  }
295
+
165
296
  try {
166
297
  const contentType = String(response?.headers?.get?.("content-type") ?? "");
167
298
  if (!response?.body || !contentType.includes("text/event-stream")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnirush",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Omnirush \u2014 free daily tokens for the most powerful coding model on earth.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -145,11 +145,15 @@ function findFile(rootDir, fileName) {
145
145
  return null;
146
146
  }
147
147
 
148
- async function fetchBun(plat) {
149
- const existing = path.join(RUNTIME, exe("bun"));
148
+ export function managedBunPath() {
149
+ return path.join(RUNTIME, exe("bun"));
150
+ }
151
+
152
+ async function fetchBun(plat, log = console.log) {
153
+ const existing = managedBunPath();
150
154
  if (fs.existsSync(existing)) return;
151
155
  const zipUrl = `https://github.com/oven-sh/bun/releases/download/${BUN_VERSION}/bun-${bunAssetPlat(plat)}.zip`;
152
- process.stdout.write(`omnirush: downloading agent runtime (bun for ${plat})...\n`);
156
+ log(`omnirush: downloading agent runtime (bun for ${plat})...`);
153
157
  const buf = await get(zipUrl);
154
158
  fs.mkdirSync(RUNTIME, { recursive: true });
155
159
  extractZip(buf, ROOT);
@@ -157,7 +161,7 @@ async function fetchBun(plat) {
157
161
  fs.copyFileSync(src, existing);
158
162
  if (process.platform !== "win32") fs.chmodSync(existing, 0o755);
159
163
  fs.rmSync(path.join(ROOT, `bun-${bunAssetPlat(plat)}`), { recursive: true, force: true });
160
- console.log("omnirush: runtime ready");
164
+ log("omnirush: runtime ready");
161
165
  }
162
166
 
163
167
  // fd/rg release archives both nest the binary under a versioned directory
@@ -196,9 +200,14 @@ function rgAsset(plat) {
196
200
  return `ripgrep-${RG_VERSION}-${arch}-unknown-linux-musl`;
197
201
  }
198
202
 
199
- async function main() {
203
+ /**
204
+ * Fetch the agent runtime when it is missing (idempotent: existing
205
+ * binaries are kept). Used by postinstall and by `omnirush doctor`
206
+ * self-heal. `log` defaults to console.log.
207
+ */
208
+ export async function ensureRuntime({ log = console.log } = {}) {
200
209
  const plat = platform();
201
- await fetchBun(plat);
210
+ await fetchBun(plat, log);
202
211
  for (const [name, version, repo, archive] of [
203
212
  ["fd", FD_VERSION, "sharkdp/fd", fdAsset(plat)],
204
213
  ["rg", RG_VERSION, "BurntSushi/ripgrep", rgAsset(plat)],
@@ -208,9 +217,17 @@ async function main() {
208
217
  } catch (error) {
209
218
  // Optional tools: warn and continue (the launcher only installs what
210
219
  // exists; the agent degrades to PATH lookup / built-in search).
211
- console.warn(`omnirush: ${name} download failed (search may be degraded):`, error.message);
220
+ log(`omnirush: ${name} download failed (search may be degraded): ${error.message}`);
212
221
  }
213
222
  }
214
223
  }
215
224
 
216
- main().catch((e) => { console.error("omnirush postinstall:", e.message); process.exit(1); });
225
+ async function main() {
226
+ await ensureRuntime();
227
+ }
228
+
229
+ // CLI entry (postinstall). Imported as a module by the launcher — never
230
+ // exit there.
231
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
232
+ main().catch((e) => { console.error("omnirush postinstall:", e.message); process.exit(1); });
233
+ }