bloby-bot 0.29.2 → 0.30.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": "bloby-bot",
3
- "version": "0.29.2",
3
+ "version": "0.30.1",
4
4
  "releaseNotes": [
5
5
  "1. # voice note (PTT bubble)",
6
6
  "2. # audio file + caption",
@@ -1,23 +1,23 @@
1
1
  // Read on-chain USDC balances for one or more Tempo addresses.
2
2
  //
3
- // Fill in the CONFIG block below, then run from repo root:
4
- // cp scripts/tempo-balance.ts /tmp/tempo-transfer/ && \
5
- // (cd /tmp/tempo-transfer && ./node_modules/.bin/tsx tempo-balance.ts)
6
- //
7
- // (The script must be executed from inside the isolated /tmp/tempo-transfer/
8
- // install — the repo's own node_modules has a @noble/hashes version conflict
9
- // that breaks viem's ESM import resolution. To rebuild the isolated dir if
10
- // missing: mkdir -p /tmp/tempo-transfer && cd /tmp/tempo-transfer \
11
- // && npm init -y && npm install viem@2.47.6 tsx)
3
+ // RUN (one-liner, from repo root): cp scripts/tempo-balance.ts /tmp/tempo-transfer/ && (cd /tmp/tempo-transfer && ./node_modules/.bin/tsx tempo-balance.ts)
12
4
  //
5
+ // Edit the ACCOUNTS list in the CONFIG block below to add/remove wallets.
13
6
  // Each entry prints: USDC balance, nonce (0 = wallet has never sent a tx),
14
7
  // and whether the address is an EOA or a contract.
8
+ //
9
+ // Why the copy-into-/tmp dance: the repo's own node_modules has a
10
+ // @noble/hashes version conflict that breaks viem's ESM import resolution,
11
+ // so the script must run from the isolated /tmp/tempo-transfer/ install.
12
+ // To rebuild that dir if it's missing:
13
+ // mkdir -p /tmp/tempo-transfer && cd /tmp/tempo-transfer && npm init -y && npm install viem@2.47.6 tsx
15
14
 
16
15
  // ─── CONFIG ──────────────────────────────────────────────────────────────────
17
16
 
18
17
  const ACCOUNTS = [
19
- { label: 'Bot wallet', wallet: '0xc65d8a0dB80f637337B87e42b8BEb5D86D46f942' },
18
+ { label: 'Bloby', wallet: '0xc65d8a0dB80f637337B87e42b8BEb5D86D46f942' },
20
19
  { label: 'Treasury', wallet: '0x084A80e395FaE8Aaf58F591f47F63DA1EeF06bbC' },
20
+ { label: 'Serge', wallet: '0x21AC79F74da90c9F441699Ee6215dA32c0CE9F5a' },
21
21
  ];
22
22
 
23
23
  // ─────────────────────────────────────────────────────────────────────────────
@@ -1,9 +1,22 @@
1
1
  /**
2
2
  * Codex OAuth PKCE flow for ChatGPT Plus/Pro subscription authentication.
3
- * Adapted from CodeDeck's CodexOAuthService for server-side Node.js.
4
3
  *
5
4
  * Spins up a temporary HTTP server on port 1455 to capture the OAuth callback.
6
- * Credentials are stored in ~/.codex/codedeck-auth.json.
5
+ * Credentials are stored in ~/.codex/auth.json in the same shape Codex CLI
6
+ * itself writes, so a spawned `codex app-server` process can use them directly.
7
+ *
8
+ * Shape on disk (chatgpt mode):
9
+ * {
10
+ * "OPENAI_API_KEY": null,
11
+ * "auth_mode": "chatgpt",
12
+ * "tokens": {
13
+ * "id_token": "<raw jwt>",
14
+ * "access_token": "<jwt>",
15
+ * "refresh_token": "...",
16
+ * "account_id": "<chatgpt_account_id from id_token claims>"
17
+ * },
18
+ * "last_refresh": "2026-05-03T12:34:56.789Z"
19
+ * }
7
20
  */
8
21
 
9
22
  import crypto from 'crypto';
@@ -23,50 +36,124 @@ const OAUTH_CONFIG = {
23
36
  };
24
37
 
25
38
  const AUTH_DIR = path.join(os.homedir(), '.codex');
26
- const AUTH_FILE = path.join(AUTH_DIR, 'codedeck-auth.json');
39
+ const AUTH_FILE = path.join(AUTH_DIR, 'auth.json');
40
+ const LEGACY_AUTH_FILE = path.join(AUTH_DIR, 'codedeck-auth.json');
41
+
42
+ /** Refresh access tokens this many ms before they actually expire. */
43
+ const REFRESH_LEEWAY_MS = 5 * 60 * 1000;
27
44
 
28
45
  let codeVerifier: string | null = null;
29
46
  let oauthState: string | null = null;
30
- let callbackServer: http.Server | null = null;
47
+ let callbackServers: http.Server[] = [];
31
48
 
32
- /* ── Helpers ── */
49
+ interface AuthDotJson {
50
+ OPENAI_API_KEY: string | null;
51
+ auth_mode?: 'apikey' | 'chatgpt' | 'chatgptAuthTokens' | 'agentIdentity';
52
+ tokens?: {
53
+ id_token: string;
54
+ access_token: string;
55
+ refresh_token: string;
56
+ account_id?: string | null;
57
+ };
58
+ last_refresh?: string;
59
+ [key: string]: unknown;
60
+ }
33
61
 
34
- function stopCallbackServer(): void {
35
- if (callbackServer) {
36
- try { callbackServer.close(); } catch {}
37
- callbackServer = null;
62
+ /* ── File I/O ── */
63
+
64
+ function readAuthFile(): AuthDotJson | null {
65
+ try {
66
+ if (!fs.existsSync(AUTH_FILE)) return null;
67
+ return JSON.parse(fs.readFileSync(AUTH_FILE, 'utf-8'));
68
+ } catch {
69
+ return null;
38
70
  }
39
71
  }
40
72
 
41
- function storeCredentials(tokens: any): void {
42
- if (!fs.existsSync(AUTH_DIR)) {
43
- fs.mkdirSync(AUTH_DIR, { recursive: true });
73
+ function writeAuthFile(auth: AuthDotJson): void {
74
+ fs.mkdirSync(AUTH_DIR, { recursive: true });
75
+ fs.writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2), 'utf-8');
76
+ try { fs.chmodSync(AUTH_FILE, 0o600); } catch {}
77
+ }
78
+
79
+ /** Decode a JWT and return its parsed payload, or null on failure. */
80
+ function decodeJwt(token: string): Record<string, any> | null {
81
+ try {
82
+ const parts = token.split('.');
83
+ if (parts.length < 2) return null;
84
+ return JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
85
+ } catch {
86
+ return null;
44
87
  }
88
+ }
45
89
 
46
- const credentials: Record<string, any> = {
47
- access_token: tokens.access_token,
48
- token_type: tokens.token_type || 'Bearer',
49
- };
90
+ /** Read the JWT `exp` claim as a Unix epoch (seconds). Null if missing/invalid. */
91
+ function jwtExpiryMs(token: string): number | null {
92
+ const payload = decodeJwt(token);
93
+ if (!payload || typeof payload.exp !== 'number') return null;
94
+ return payload.exp * 1000;
95
+ }
96
+
97
+ /** Pull `chatgpt_account_id` out of the id_token JWT claims, if present. */
98
+ function extractAccountId(idToken: string): string | undefined {
99
+ const payload = decodeJwt(idToken);
100
+ const authClaims = payload?.['https://api.openai.com/auth'] || {};
101
+ return authClaims.chatgpt_account_id || undefined;
102
+ }
50
103
 
51
- if (tokens.refresh_token) credentials.refresh_token = tokens.refresh_token;
52
- if (tokens.id_token) credentials.id_token = tokens.id_token;
53
- if (tokens.expires_in) {
54
- credentials.expires_at = Date.now() + (tokens.expires_in - 300) * 1000;
104
+ /** One-shot migration from the old `codedeck-auth.json` layout (pre-codex-native). */
105
+ function migrateLegacyFile(): void {
106
+ if (fs.existsSync(AUTH_FILE)) {
107
+ // If auth.json already has chatgpt tokens, nothing to do.
108
+ const existing = readAuthFile();
109
+ if (existing?.tokens?.refresh_token) return;
55
110
  }
111
+ if (!fs.existsSync(LEGACY_AUTH_FILE)) return;
56
112
 
57
- // Decode JWT claims for account info
58
113
  try {
59
- const payload = JSON.parse(Buffer.from(tokens.access_token.split('.')[1], 'base64url').toString());
60
- const authClaims = payload['https://api.openai.com/auth'] || {};
61
- if (authClaims.chatgpt_account_id) credentials.chatgpt_account_id = authClaims.chatgpt_account_id;
62
- if (authClaims.chatgpt_plan_type) credentials.chatgpt_plan_type = authClaims.chatgpt_plan_type;
63
- } catch {
64
- log.warn('Codex: failed to decode JWT claims');
114
+ const legacy = JSON.parse(fs.readFileSync(LEGACY_AUTH_FILE, 'utf-8'));
115
+ if (!legacy.access_token || !legacy.refresh_token) return;
116
+
117
+ const existing = readAuthFile() || { OPENAI_API_KEY: null };
118
+ const next: AuthDotJson = {
119
+ ...existing,
120
+ auth_mode: 'chatgpt',
121
+ tokens: {
122
+ id_token: legacy.id_token || '',
123
+ access_token: legacy.access_token,
124
+ refresh_token: legacy.refresh_token,
125
+ account_id: legacy.chatgpt_account_id || extractAccountId(legacy.access_token),
126
+ },
127
+ last_refresh: new Date().toISOString(),
128
+ };
129
+ writeAuthFile(next);
130
+ try { fs.unlinkSync(LEGACY_AUTH_FILE); } catch {}
131
+ log.ok('Codex: migrated legacy codedeck-auth.json → auth.json');
132
+ } catch (err: any) {
133
+ log.warn(`Codex: legacy migration failed — ${err.message}`);
65
134
  }
135
+ }
66
136
 
67
- fs.writeFileSync(AUTH_FILE, JSON.stringify(credentials, null, 2), 'utf-8');
68
- try { fs.chmodSync(AUTH_FILE, 0o600); } catch {}
69
- log.ok('Codex credentials stored');
137
+ /* ── Token exchange & refresh ── */
138
+
139
+ function storeTokens(tokens: { access_token: string; refresh_token?: string; id_token?: string }): void {
140
+ const existing = readAuthFile() || { OPENAI_API_KEY: null };
141
+ const prev = existing.tokens;
142
+ const idToken = tokens.id_token ?? prev?.id_token ?? '';
143
+
144
+ const next: AuthDotJson = {
145
+ ...existing,
146
+ OPENAI_API_KEY: existing.OPENAI_API_KEY ?? null,
147
+ auth_mode: 'chatgpt',
148
+ tokens: {
149
+ id_token: idToken,
150
+ access_token: tokens.access_token,
151
+ refresh_token: tokens.refresh_token ?? prev?.refresh_token ?? '',
152
+ account_id: idToken ? extractAccountId(idToken) ?? prev?.account_id : prev?.account_id,
153
+ },
154
+ last_refresh: new Date().toISOString(),
155
+ };
156
+ writeAuthFile(next);
70
157
  }
71
158
 
72
159
  async function exchangeCode(code: string): Promise<{ success: boolean; error?: string }> {
@@ -94,28 +181,62 @@ async function exchangeCode(code: string): Promise<{ success: boolean; error?: s
94
181
  }
95
182
 
96
183
  const tokens = await response.json();
97
- storeCredentials(tokens);
184
+ if (!tokens.access_token) {
185
+ return { success: false, error: 'OAuth response missing access_token.' };
186
+ }
187
+ storeTokens(tokens);
98
188
  codeVerifier = null;
99
189
  oauthState = null;
190
+ log.ok('Codex credentials stored');
191
+ // Clean up the legacy file on first successful new auth.
192
+ try { fs.unlinkSync(LEGACY_AUTH_FILE); } catch {}
100
193
  return { success: true };
101
194
  } catch (err: any) {
102
195
  return { success: false, error: err.message };
103
196
  }
104
197
  }
105
198
 
199
+ async function refreshTokens(refreshToken: string): Promise<boolean> {
200
+ try {
201
+ log.ok('Codex: refreshing access token...');
202
+ const response = await fetch(OAUTH_CONFIG.TOKEN_URL, {
203
+ method: 'POST',
204
+ headers: { 'Content-Type': 'application/json' },
205
+ body: JSON.stringify({
206
+ client_id: OAUTH_CONFIG.CLIENT_ID,
207
+ grant_type: 'refresh_token',
208
+ refresh_token: refreshToken,
209
+ }),
210
+ });
211
+ if (!response.ok) {
212
+ log.warn(`Codex: refresh failed (${response.status})`);
213
+ return false;
214
+ }
215
+ const tokens = await response.json();
216
+ if (!tokens.access_token) {
217
+ log.warn('Codex: refresh response missing access_token');
218
+ return false;
219
+ }
220
+ storeTokens(tokens);
221
+ log.ok('Codex: token refreshed');
222
+ return true;
223
+ } catch (err: any) {
224
+ log.warn(`Codex: refresh error — ${err.message}`);
225
+ return false;
226
+ }
227
+ }
228
+
106
229
  /* ── Public API ── */
107
230
 
108
231
  export function startCodexOAuth(): Promise<{ success: boolean; authUrl?: string; error?: string }> {
109
232
  return new Promise((resolve) => {
110
233
  stopCallbackServer();
111
234
 
112
- // Generate PKCE
113
235
  codeVerifier = crypto.randomBytes(32).toString('base64url');
114
236
  const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
115
237
  oauthState = crypto.randomUUID();
116
238
 
117
- // Start local callback server
118
- callbackServer = http.createServer((req, res) => {
239
+ const handleCallback = (req: http.IncomingMessage, res: http.ServerResponse) => {
119
240
  if (!req.url?.startsWith('/auth/callback')) {
120
241
  res.writeHead(404);
121
242
  res.end();
@@ -126,6 +247,7 @@ export function startCodexOAuth(): Promise<{ success: boolean; authUrl?: string;
126
247
  const code = url.searchParams.get('code');
127
248
  const returnedState = url.searchParams.get('state');
128
249
  const error = url.searchParams.get('error');
250
+ log.ok(`Codex OAuth callback hit (code=${code ? 'yes' : 'no'}, state=${returnedState === oauthState ? 'ok' : 'mismatch'}, error=${error || 'none'})`);
129
251
 
130
252
  res.writeHead(200, { 'Content-Type': 'text/html' });
131
253
  res.end(`<html><body style="font-family:sans-serif;text-align:center;padding:60px;background:#0a0a0a;color:#fff">
@@ -137,11 +259,24 @@ export function startCodexOAuth(): Promise<{ success: boolean; authUrl?: string;
137
259
 
138
260
  if (error || !code || returnedState !== oauthState) return;
139
261
  exchangeCode(code);
140
- });
262
+ };
263
+
264
+ // Bind BOTH IPv4 and IPv6 loopback. On dual-stack systems where
265
+ // `localhost` resolves to ::1 first, an IPv4-only bind silently fails
266
+ // when the browser hits the callback URL.
267
+ const bindHosts: Array<{ host: string; required: boolean }> = [
268
+ { host: '127.0.0.1', required: true },
269
+ { host: '::1', required: false }, // tolerate machines without IPv6
270
+ ];
141
271
 
142
- callbackServer.listen(OAUTH_CONFIG.PORT, '127.0.0.1', () => {
143
- log.ok(`Codex OAuth callback server on port ${OAUTH_CONFIG.PORT}`);
272
+ let resolved = false;
273
+ let pendingBinds = bindHosts.length;
274
+ let bindFailures = 0;
144
275
 
276
+ const finishWithSuccess = () => {
277
+ if (resolved) return;
278
+ resolved = true;
279
+ log.ok(`Codex OAuth callback servers listening on port ${OAUTH_CONFIG.PORT} (${callbackServers.length} bind${callbackServers.length === 1 ? '' : 's'})`);
145
280
  const params = new URLSearchParams({
146
281
  response_type: 'code',
147
282
  client_id: OAUTH_CONFIG.CLIENT_ID,
@@ -153,16 +288,41 @@ export function startCodexOAuth(): Promise<{ success: boolean; authUrl?: string;
153
288
  id_token_add_organizations: 'true',
154
289
  codex_cli_simplified_flow: 'true',
155
290
  });
156
-
157
291
  resolve({ success: true, authUrl: `${OAUTH_CONFIG.AUTHORIZE_URL}?${params.toString()}` });
158
- });
292
+ };
159
293
 
160
- callbackServer.on('error', (err: any) => {
161
- const error = err.code === 'EADDRINUSE'
294
+ const finishWithError = (err: any) => {
295
+ if (resolved) return;
296
+ resolved = true;
297
+ stopCallbackServer();
298
+ const error = err?.code === 'EADDRINUSE'
162
299
  ? `Port ${OAUTH_CONFIG.PORT} is busy. Close other Codex instances.`
163
- : err.message;
300
+ : (err?.message || String(err));
164
301
  resolve({ success: false, error });
165
- });
302
+ };
303
+
304
+ for (const { host, required } of bindHosts) {
305
+ const server = http.createServer(handleCallback);
306
+
307
+ server.once('error', (err: any) => {
308
+ log.warn(`Codex OAuth bind ${host}:${OAUTH_CONFIG.PORT} failed — ${err.code || err.message}`);
309
+ if (required) {
310
+ finishWithError(err);
311
+ } else {
312
+ bindFailures++;
313
+ pendingBinds--;
314
+ // If only the optional bind failed but we already have at least one
315
+ // server up, that's still a success.
316
+ if (pendingBinds === 0 && callbackServers.length > 0) finishWithSuccess();
317
+ }
318
+ });
319
+
320
+ server.listen(OAUTH_CONFIG.PORT, host, () => {
321
+ callbackServers.push(server);
322
+ pendingBinds--;
323
+ if (pendingBinds === 0) finishWithSuccess();
324
+ });
325
+ }
166
326
  });
167
327
  }
168
328
 
@@ -172,28 +332,71 @@ export function cancelCodexOAuth(): void {
172
332
  oauthState = null;
173
333
  }
174
334
 
175
- export function getCodexAuthStatus(): { authenticated: boolean; plan?: string; error?: string } {
335
+ export async function getCodexAuthStatus(): Promise<{
336
+ authenticated: boolean;
337
+ plan?: string;
338
+ error?: string;
339
+ }> {
176
340
  try {
177
- if (!fs.existsSync(AUTH_FILE)) return { authenticated: false };
178
- const creds = JSON.parse(fs.readFileSync(AUTH_FILE, 'utf-8'));
179
- if (!creds.access_token) return { authenticated: false };
180
- if (creds.expires_at && Date.now() >= creds.expires_at) {
181
- return { authenticated: false, error: 'Token expired' };
341
+ migrateLegacyFile();
342
+ const auth = readAuthFile();
343
+ const tokens = auth?.tokens;
344
+ if (!tokens?.access_token) return { authenticated: false };
345
+
346
+ const expMs = jwtExpiryMs(tokens.access_token);
347
+ const valid = expMs ? Date.now() + REFRESH_LEEWAY_MS < expMs : true;
348
+
349
+ if (!valid && tokens.refresh_token) {
350
+ const ok = await refreshTokens(tokens.refresh_token);
351
+ if (!ok) return { authenticated: false, error: 'Token expired and refresh failed' };
182
352
  }
183
- return { authenticated: true, plan: creds.chatgpt_plan_type || 'plus' };
353
+
354
+ const fresh = readAuthFile();
355
+ const idClaims = fresh?.tokens?.id_token ? decodeJwt(fresh.tokens.id_token) : null;
356
+ const plan = idClaims?.['https://api.openai.com/auth']?.chatgpt_plan_type || 'plus';
357
+ return { authenticated: true, plan };
184
358
  } catch (err: any) {
185
359
  return { authenticated: false, error: err.message };
186
360
  }
187
361
  }
188
362
 
363
+ /**
364
+ * Read a valid Codex access token, refreshing if expired. Returns null if unavailable.
365
+ * This is what the Codex harness should call before each app-server invocation.
366
+ */
367
+ export async function getCodexAccessToken(): Promise<string | null> {
368
+ migrateLegacyFile();
369
+ const auth = readAuthFile();
370
+ const tokens = auth?.tokens;
371
+ if (!tokens?.access_token) return null;
372
+
373
+ const expMs = jwtExpiryMs(tokens.access_token);
374
+ if (!expMs || Date.now() + REFRESH_LEEWAY_MS < expMs) {
375
+ return tokens.access_token;
376
+ }
377
+
378
+ if (!tokens.refresh_token) return null;
379
+ const ok = await refreshTokens(tokens.refresh_token);
380
+ if (!ok) return null;
381
+
382
+ return readAuthFile()?.tokens?.access_token ?? null;
383
+ }
384
+
385
+ /** Synchronous accessor — returns the stored access token without refreshing. */
189
386
  export function readCodexAccessToken(): string | null {
190
- try {
191
- if (!fs.existsSync(AUTH_FILE)) return null;
192
- const creds = JSON.parse(fs.readFileSync(AUTH_FILE, 'utf-8'));
193
- if (!creds.access_token) return null;
194
- if (creds.expires_at && Date.now() >= creds.expires_at) return null;
195
- return creds.access_token;
196
- } catch {
197
- return null;
387
+ const auth = readAuthFile();
388
+ const token = auth?.tokens?.access_token;
389
+ if (!token) return null;
390
+ const expMs = jwtExpiryMs(token);
391
+ if (expMs && Date.now() >= expMs) return null;
392
+ return token;
393
+ }
394
+
395
+ /* ── Helpers ── */
396
+
397
+ function stopCallbackServer(): void {
398
+ for (const server of callbackServers) {
399
+ try { server.close(); } catch {}
198
400
  }
401
+ callbackServers = [];
199
402
  }
package/worker/index.ts CHANGED
@@ -9,7 +9,7 @@ import { initDb, closeDb, listConversations, createConversation, deleteConversat
9
9
  import webpush from 'web-push';
10
10
  import { TOTP } from 'otpauth';
11
11
  import QRCode from 'qrcode';
12
- import { startCodexOAuth, cancelCodexOAuth, getCodexAuthStatus, readCodexAccessToken } from './codex-auth.js';
12
+ import { startCodexOAuth, cancelCodexOAuth, getCodexAuthStatus } from './codex-auth.js';
13
13
  import { startClaudeOAuth, exchangeClaudeCode, getClaudeAuthStatus, readClaudeAccessToken } from './claude-auth.js';
14
14
  import { checkAvailability, registerHandle, claimReservedHandle, releaseHandle, updateTunnelUrl, startHeartbeat, stopHeartbeat } from '../shared/relay.js';
15
15
  import { ensureFileDirs } from '../supervisor/file-saver.js';
@@ -181,8 +181,8 @@ app.post('/api/auth/codex/cancel', (_req, res) => {
181
181
  res.json({ ok: true });
182
182
  });
183
183
 
184
- app.get('/api/auth/codex/status', (_req, res) => {
185
- res.json(getCodexAuthStatus());
184
+ app.get('/api/auth/codex/status', async (_req, res) => {
185
+ res.json(await getCodexAuthStatus());
186
186
  });
187
187
 
188
188
  // ── Claude OAuth routes ──
@@ -729,10 +729,10 @@ app.post('/api/onboard', (req, res) => {
729
729
  currentCfg.ai.model = model || '';
730
730
  currentCfg.ai.baseUrl = baseUrl || undefined;
731
731
 
732
- // Read OAuth token if no API key provided
733
- if (!apiKey && provider === 'openai') {
734
- currentCfg.ai.apiKey = readCodexAccessToken() || '';
735
- } else if (!apiKey && provider === 'anthropic') {
732
+ // OAuth providers store their tokens in their own credentials files
733
+ // (~/.codex/auth.json, ~/.claude/.credentials.json) and refresh them as
734
+ // needed — config.ai.apiKey only holds raw API keys.
735
+ if (!apiKey && provider === 'anthropic') {
736
736
  currentCfg.ai.apiKey = readClaudeAccessToken() || '';
737
737
  } else {
738
738
  currentCfg.ai.apiKey = apiKey || '';
@@ -16,9 +16,9 @@ function DashboardError() {
16
16
  playsInline
17
17
  style={{ height: 120, width: 120, borderRadius: '50%', objectFit: 'cover', marginBottom: 32 }}
18
18
  />
19
- <h1 style={{ fontSize: 20, fontWeight: 600, marginBottom: 8 }}>Your app crashed</h1>
19
+ <h1 style={{ fontSize: 20, fontWeight: 600, marginBottom: 8 }}>Oopss.. Something wrong is not right</h1>
20
20
  <p style={{ fontSize: 14, color: 'rgba(255,255,255,0.5)', maxWidth: 320, lineHeight: 1.5 }}>
21
- Ask the agent to fix it using the chat.
21
+ If your agent is working, this is normal. If not, go poke them
22
22
  </p>
23
23
  </div>
24
24
  );