antigravity-chat-proxy 0.1.0

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 (76) hide show
  1. package/README.md +362 -0
  2. package/app/api/v1/artifacts/[convId]/[filename]/route.ts +75 -0
  3. package/app/api/v1/artifacts/[convId]/route.ts +47 -0
  4. package/app/api/v1/artifacts/active/[filename]/route.ts +50 -0
  5. package/app/api/v1/artifacts/active/route.ts +89 -0
  6. package/app/api/v1/artifacts/route.ts +43 -0
  7. package/app/api/v1/chat/action/route.ts +30 -0
  8. package/app/api/v1/chat/approve/route.ts +21 -0
  9. package/app/api/v1/chat/history/route.ts +23 -0
  10. package/app/api/v1/chat/mode/route.ts +59 -0
  11. package/app/api/v1/chat/new/route.ts +21 -0
  12. package/app/api/v1/chat/reject/route.ts +21 -0
  13. package/app/api/v1/chat/route.ts +105 -0
  14. package/app/api/v1/chat/state/route.ts +23 -0
  15. package/app/api/v1/chat/stream/route.ts +258 -0
  16. package/app/api/v1/conversations/active/route.ts +117 -0
  17. package/app/api/v1/conversations/route.ts +189 -0
  18. package/app/api/v1/conversations/select/route.ts +114 -0
  19. package/app/api/v1/debug/dom/route.ts +30 -0
  20. package/app/api/v1/debug/scrape/route.ts +56 -0
  21. package/app/api/v1/health/route.ts +13 -0
  22. package/app/api/v1/windows/cdp-start/route.ts +32 -0
  23. package/app/api/v1/windows/cdp-status/route.ts +32 -0
  24. package/app/api/v1/windows/close/route.ts +67 -0
  25. package/app/api/v1/windows/open/route.ts +49 -0
  26. package/app/api/v1/windows/recent/route.ts +25 -0
  27. package/app/api/v1/windows/route.ts +27 -0
  28. package/app/api/v1/windows/select/route.ts +35 -0
  29. package/app/debug/page.tsx +228 -0
  30. package/app/favicon.ico +0 -0
  31. package/app/globals.css +1234 -0
  32. package/app/layout.tsx +42 -0
  33. package/app/page.tsx +10 -0
  34. package/bin/cli.js +601 -0
  35. package/components/agent-message.tsx +63 -0
  36. package/components/artifact-panel.tsx +133 -0
  37. package/components/chat-container.tsx +82 -0
  38. package/components/chat-input.tsx +92 -0
  39. package/components/conversation-selector.tsx +97 -0
  40. package/components/header.tsx +302 -0
  41. package/components/hitl-dialog.tsx +23 -0
  42. package/components/message-list.tsx +41 -0
  43. package/components/thinking-block.tsx +14 -0
  44. package/components/tool-call-card.tsx +75 -0
  45. package/components/typing-indicator.tsx +11 -0
  46. package/components/user-message.tsx +13 -0
  47. package/components/welcome-screen.tsx +38 -0
  48. package/hooks/use-artifacts.ts +85 -0
  49. package/hooks/use-chat.ts +278 -0
  50. package/hooks/use-conversations.ts +190 -0
  51. package/lib/actions/hitl.ts +113 -0
  52. package/lib/actions/new-chat.ts +116 -0
  53. package/lib/actions/send-message.ts +31 -0
  54. package/lib/actions/switch-conversation.ts +92 -0
  55. package/lib/cdp/connection.ts +95 -0
  56. package/lib/cdp/process-manager.ts +327 -0
  57. package/lib/cdp/recent-projects.ts +137 -0
  58. package/lib/cdp/selectors.ts +11 -0
  59. package/lib/context.ts +38 -0
  60. package/lib/init.ts +48 -0
  61. package/lib/logger.ts +32 -0
  62. package/lib/scraper/agent-mode.ts +122 -0
  63. package/lib/scraper/agent-state.ts +756 -0
  64. package/lib/scraper/chat-history.ts +138 -0
  65. package/lib/scraper/ide-conversations.ts +124 -0
  66. package/lib/sse/diff-states.ts +141 -0
  67. package/lib/types.ts +146 -0
  68. package/lib/utils.ts +7 -0
  69. package/next.config.ts +7 -0
  70. package/package.json +50 -0
  71. package/public/file.svg +1 -0
  72. package/public/globe.svg +1 -0
  73. package/public/next.svg +1 -0
  74. package/public/vercel.svg +1 -0
  75. package/public/window.svg +1 -0
  76. package/tsconfig.json +34 -0
package/app/layout.tsx ADDED
@@ -0,0 +1,42 @@
1
+ import type { Metadata } from 'next';
2
+ import { Inter, JetBrains_Mono } from 'next/font/google';
3
+ import './globals.css';
4
+
5
+ const inter = Inter({
6
+ subsets: ['latin'],
7
+ variable: '--font-inter',
8
+ display: 'swap',
9
+ weight: ['300', '400', '500', '600', '700'],
10
+ });
11
+
12
+ const jetbrainsMono = JetBrains_Mono({
13
+ subsets: ['latin'],
14
+ variable: '--font-jetbrains',
15
+ display: 'swap',
16
+ weight: ['400', '500'],
17
+ });
18
+
19
+ export const metadata: Metadata = {
20
+ title: 'Antigravity Agent',
21
+ description: 'Chat with the Antigravity AI Agent from any browser',
22
+ };
23
+
24
+ export const viewport = {
25
+ width: 'device-width',
26
+ initialScale: 1,
27
+ maximumScale: 1,
28
+ viewportFit: 'cover' as const,
29
+ themeColor: '#0a0a0f',
30
+ };
31
+
32
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
33
+ return (
34
+ <html lang="en" className={`${inter.variable} ${jetbrainsMono.variable}`}>
35
+ <head>
36
+ <meta name="apple-mobile-web-app-capable" content="yes" />
37
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
38
+ </head>
39
+ <body>{children}</body>
40
+ </html>
41
+ );
42
+ }
package/app/page.tsx ADDED
@@ -0,0 +1,10 @@
1
+ import ChatContainer from '@/components/chat-container';
2
+ import { Suspense } from 'react';
3
+
4
+ export default function ChatPage() {
5
+ return (
6
+ <Suspense fallback={<div className="p-8 text-center text-gray-500">Loading Chat Interface...</div>}>
7
+ <ChatContainer />
8
+ </Suspense>
9
+ );
10
+ }
package/bin/cli.js ADDED
@@ -0,0 +1,601 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn, execSync } = require('child_process');
4
+ const path = require('path');
5
+ const readline = require('readline');
6
+ const fs = require('fs');
7
+ const os = require('os');
8
+
9
+ // ── Colors & Formatting ─────────────────────────────────────────────────
10
+ const c = {
11
+ reset: '\x1b[0m',
12
+ bold: '\x1b[1m',
13
+ dim: '\x1b[2m',
14
+ italic: '\x1b[3m',
15
+ underline: '\x1b[4m',
16
+ cyan: '\x1b[36m',
17
+ green: '\x1b[32m',
18
+ yellow: '\x1b[33m',
19
+ red: '\x1b[31m',
20
+ magenta: '\x1b[35m',
21
+ blue: '\x1b[34m',
22
+ white: '\x1b[37m',
23
+ bgCyan: '\x1b[46m',
24
+ bgMagenta: '\x1b[45m',
25
+ };
26
+
27
+ const fmt = {
28
+ bold: (s) => `${c.bold}${s}${c.reset}`,
29
+ dim: (s) => `${c.dim}${s}${c.reset}`,
30
+ cyan: (s) => `${c.cyan}${s}${c.reset}`,
31
+ green: (s) => `${c.green}${s}${c.reset}`,
32
+ yellow: (s) => `${c.yellow}${s}${c.reset}`,
33
+ red: (s) => `${c.red}${s}${c.reset}`,
34
+ magenta: (s) => `${c.magenta}${s}${c.reset}`,
35
+ blue: (s) => `${c.blue}${s}${c.reset}`,
36
+ link: (s) => `${c.cyan}${c.underline}${s}${c.reset}`,
37
+ success: (s) => `${c.green}✔${c.reset} ${s}`,
38
+ error: (s) => `${c.red}✖${c.reset} ${s}`,
39
+ warn: (s) => `${c.yellow}⚠${c.reset} ${s}`,
40
+ info: (s) => `${c.cyan}ℹ${c.reset} ${s}`,
41
+ step: (n, s) => `${c.dim}[${n}]${c.reset} ${s}`,
42
+ };
43
+
44
+ // ── Config file path ────────────────────────────────────────────────────
45
+ const CONFIG_DIR = path.join(os.homedir(), '.antigravity-chat-proxy');
46
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
47
+
48
+ function loadConfig() {
49
+ try {
50
+ if (fs.existsSync(CONFIG_FILE)) {
51
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
52
+ }
53
+ } catch {}
54
+ return {};
55
+ }
56
+
57
+ function saveConfig(config) {
58
+ try {
59
+ if (!fs.existsSync(CONFIG_DIR)) {
60
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
61
+ }
62
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
63
+ } catch {}
64
+ }
65
+
66
+ // ── Readline helpers ────────────────────────────────────────────────────
67
+ const rl = readline.createInterface({
68
+ input: process.stdin,
69
+ output: process.stdout,
70
+ });
71
+
72
+ function ask(question) {
73
+ return new Promise((resolve) => {
74
+ rl.question(question, (answer) => resolve(answer.trim()));
75
+ });
76
+ }
77
+
78
+ async function askWithDefault(question, defaultVal) {
79
+ const hint = defaultVal ? ` ${c.dim}(${defaultVal})${c.reset}` : '';
80
+ const answer = await ask(`${question}${hint}: `);
81
+ return answer || defaultVal || '';
82
+ }
83
+
84
+ async function askYesNo(question, defaultYes = true) {
85
+ const hint = defaultYes ? `${c.dim}(Y/n)${c.reset}` : `${c.dim}(y/N)${c.reset}`;
86
+ const answer = await ask(`${question} ${hint}: `);
87
+ if (!answer) return defaultYes;
88
+ return answer.toLowerCase().startsWith('y');
89
+ }
90
+
91
+ async function askPassword(question) {
92
+ return new Promise((resolve) => {
93
+ process.stdout.write(question);
94
+ const stdin = process.stdin;
95
+ const wasRaw = stdin.isRaw;
96
+
97
+ if (stdin.isTTY) {
98
+ stdin.setRawMode(true);
99
+ }
100
+
101
+ let input = '';
102
+ const onData = (char) => {
103
+ const s = char.toString();
104
+
105
+ if (s === '\n' || s === '\r') {
106
+ stdin.removeListener('data', onData);
107
+ if (stdin.isTTY) {
108
+ stdin.setRawMode(wasRaw || false);
109
+ }
110
+ process.stdout.write('\n');
111
+ resolve(input.trim());
112
+ return;
113
+ }
114
+
115
+ if (s === '\u0003') { // Ctrl+C
116
+ process.stdout.write('\n');
117
+ process.exit(0);
118
+ }
119
+
120
+ if (s === '\u007f' || s === '\b') { // Backspace
121
+ if (input.length > 0) {
122
+ input = input.slice(0, -1);
123
+ process.stdout.write('\b \b');
124
+ }
125
+ return;
126
+ }
127
+
128
+ input += s;
129
+ process.stdout.write('•');
130
+ };
131
+
132
+ stdin.resume();
133
+ stdin.on('data', onData);
134
+ });
135
+ }
136
+
137
+ function printSeparator() {
138
+ console.log(c.dim + ' ─────────────────────────────────────────────' + c.reset);
139
+ }
140
+
141
+ function clearLine() {
142
+ process.stdout.write('\x1b[2K\r');
143
+ }
144
+
145
+ // ── Banner ──────────────────────────────────────────────────────────────
146
+ function printBanner() {
147
+ console.log('');
148
+ console.log(` ${c.bold}${c.cyan}╔═══════════════════════════════════════════════════════╗${c.reset}`);
149
+ console.log(` ${c.bold}${c.cyan}║${c.reset} ${c.bold}🚀 Antigravity Chat Proxy${c.reset} ${c.bold}${c.cyan}║${c.reset}`);
150
+ console.log(` ${c.bold}${c.cyan}║${c.reset} ${c.dim}Secure tunnel to your IDE with Google OAuth${c.reset} ${c.bold}${c.cyan}║${c.reset}`);
151
+ console.log(` ${c.bold}${c.cyan}╚═══════════════════════════════════════════════════════╝${c.reset}`);
152
+ console.log('');
153
+ }
154
+
155
+ // ── CLI Arg Parsing (skip interactive mode) ─────────────────────────────
156
+ function parseArgs() {
157
+ const args = process.argv.slice(2);
158
+ const parsed = {};
159
+
160
+ for (let i = 0; i < args.length; i++) {
161
+ if (args[i] === '--email' && args[i + 1]) parsed.email = args[++i];
162
+ else if (args[i] === '--port' && args[i + 1]) parsed.port = args[++i];
163
+ else if (args[i] === '--authtoken' && args[i + 1]) parsed.authtoken = args[++i];
164
+ else if (args[i] === '--no-tunnel') parsed.noTunnel = true;
165
+ else if (args[i] === '--help') parsed.help = true;
166
+ else if (args[i] === '--reset') parsed.reset = true;
167
+ else if (args[i] === '--non-interactive') parsed.nonInteractive = true;
168
+ }
169
+
170
+ return parsed;
171
+ }
172
+
173
+ function printHelp() {
174
+ printBanner();
175
+ console.log(` ${fmt.bold('Usage:')}`);
176
+ console.log(` ${fmt.cyan('npx antigravity-chat-proxy')} ${fmt.dim('# Interactive setup wizard')}`);
177
+ console.log(` ${fmt.cyan('npx antigravity-chat-proxy --email me@gmail.com')} ${fmt.dim('# Skip wizard')}`);
178
+ console.log('');
179
+ console.log(` ${fmt.bold('Options:')}`);
180
+ console.log(` ${fmt.cyan('--email')} <email> Google email to allow access`);
181
+ console.log(` ${fmt.cyan('--port')} <number> Local port (default: 5555)`);
182
+ console.log(` ${fmt.cyan('--authtoken')} <token> ngrok authtoken`);
183
+ console.log(` ${fmt.cyan('--no-tunnel')} Run locally without ngrok`);
184
+ console.log(` ${fmt.cyan('--reset')} Reset saved configuration`);
185
+ console.log(` ${fmt.cyan('--help')} Show this help`);
186
+ console.log('');
187
+ console.log(` ${fmt.bold('Environment Variables:')}`);
188
+ console.log(` ${fmt.cyan('NGROK_AUTHTOKEN')} Your ngrok authtoken`);
189
+ console.log('');
190
+ console.log(` ${fmt.bold('First-time setup?')} Just run ${fmt.cyan('npx antigravity-chat-proxy')} and follow the wizard!`);
191
+ console.log('');
192
+ }
193
+
194
+ // ── Check if ngrok authtoken exists ─────────────────────────────────────
195
+ function detectAuthtoken() {
196
+ // 1. Check env var
197
+ if (process.env.NGROK_AUTHTOKEN) {
198
+ return { token: process.env.NGROK_AUTHTOKEN, source: 'environment variable' };
199
+ }
200
+
201
+ // 2. Check our saved config
202
+ const config = loadConfig();
203
+ if (config.authtoken) {
204
+ return { token: config.authtoken, source: 'saved config' };
205
+ }
206
+
207
+ // 3. Check ngrok's own config file
208
+ const ngrokConfigPaths = [
209
+ path.join(os.homedir(), '.config', 'ngrok', 'ngrok.yml'),
210
+ path.join(os.homedir(), '.ngrok2', 'ngrok.yml'),
211
+ path.join(os.homedir(), 'Library', 'Application Support', 'ngrok', 'ngrok.yml'),
212
+ ];
213
+
214
+ for (const configPath of ngrokConfigPaths) {
215
+ try {
216
+ if (fs.existsSync(configPath)) {
217
+ const content = fs.readFileSync(configPath, 'utf-8');
218
+ const match = content.match(/authtoken:\s*(.+)/);
219
+ if (match && match[1].trim()) {
220
+ return { token: match[1].trim(), source: `ngrok config (${configPath})` };
221
+ }
222
+ }
223
+ } catch {}
224
+ }
225
+
226
+ return null;
227
+ }
228
+
229
+ // ── Interactive Setup Wizard ────────────────────────────────────────────
230
+ async function runWizard(cliArgs) {
231
+ printBanner();
232
+
233
+ const config = loadConfig();
234
+ const isFirstRun = !config.email && !config.port;
235
+
236
+ if (isFirstRun) {
237
+ console.log(` ${fmt.info('Welcome! Let\'s set up your Antigravity Chat Proxy.')}`);
238
+ console.log(` ${fmt.dim('This wizard will guide you through the configuration.')}`);
239
+ console.log(` ${fmt.dim('Your settings will be saved for next time.')}`);
240
+ console.log('');
241
+ } else {
242
+ console.log(` ${fmt.info('Welcome back! Loading your saved settings.')}`);
243
+ console.log(` ${fmt.dim('Press Enter to keep defaults, or type new values.')}`);
244
+ console.log('');
245
+ }
246
+
247
+ printSeparator();
248
+
249
+ // ── Step 1: ngrok Authtoken ───────────────────────────────────────
250
+ console.log('');
251
+ console.log(` ${fmt.step('1/3', fmt.bold('ngrok Authentication'))}`);
252
+ console.log('');
253
+
254
+ const existingAuth = detectAuthtoken();
255
+ let authtoken = null;
256
+
257
+ if (existingAuth) {
258
+ const masked = existingAuth.token.slice(0, 8) + '••••••••' + existingAuth.token.slice(-4);
259
+ console.log(` ${fmt.success(`Found authtoken from ${fmt.dim(existingAuth.source)}`)}`);
260
+ console.log(` ${fmt.dim(' Token:')} ${masked}`);
261
+ console.log('');
262
+
263
+ const useExisting = await askYesNo(` Use this token?`);
264
+ if (useExisting) {
265
+ authtoken = existingAuth.token;
266
+ }
267
+ }
268
+
269
+ if (!authtoken) {
270
+ console.log('');
271
+ console.log(` ${fmt.warn('ngrok authtoken is required for tunneling.')}`);
272
+ console.log('');
273
+ console.log(` ${fmt.bold('How to get your authtoken:')}`);
274
+ console.log(` ${fmt.cyan('1.')} Go to ${fmt.link('https://dashboard.ngrok.com/signup')}`);
275
+ console.log(` ${fmt.cyan('2.')} Sign up (it's free) or log in`);
276
+ console.log(` ${fmt.cyan('3.')} Go to ${fmt.link('https://dashboard.ngrok.com/authtokens')}`);
277
+ console.log(` ${fmt.cyan('4.')} Copy your authtoken`);
278
+ console.log('');
279
+
280
+ authtoken = await askPassword(` ${fmt.cyan('?')} Paste your authtoken: `);
281
+
282
+ if (!authtoken) {
283
+ console.log('');
284
+ console.log(` ${fmt.error('Authtoken is required. Exiting.')}`);
285
+ process.exit(1);
286
+ }
287
+
288
+ console.log(` ${fmt.success('Authtoken received')}`);
289
+
290
+ const shouldSave = await askYesNo(` ${fmt.cyan('?')} Save authtoken for future use?`);
291
+ if (shouldSave) {
292
+ config.authtoken = authtoken;
293
+ saveConfig(config);
294
+ console.log(` ${fmt.success(`Saved to ${fmt.dim(CONFIG_FILE)}`)}`);
295
+ }
296
+ }
297
+
298
+ // ── Step 2: Email ─────────────────────────────────────────────────
299
+ console.log('');
300
+ printSeparator();
301
+ console.log('');
302
+ console.log(` ${fmt.step('2/3', fmt.bold('Access Control'))}`);
303
+ console.log(` ${fmt.dim(' Only the specified Google email will be able to access your proxy.')}`);
304
+ console.log('');
305
+
306
+ const email = await askWithDefault(
307
+ ` ${fmt.cyan('?')} Google email to allow`,
308
+ config.email || ''
309
+ );
310
+
311
+ if (!email || !email.includes('@')) {
312
+ console.log('');
313
+ console.log(` ${fmt.error('A valid email address is required.')}`);
314
+ process.exit(1);
315
+ }
316
+
317
+ console.log(` ${fmt.success(`Access restricted to ${fmt.cyan(email)}`)}`);
318
+
319
+ // ── Step 3: Port ──────────────────────────────────────────────────
320
+ console.log('');
321
+ printSeparator();
322
+ console.log('');
323
+ console.log(` ${fmt.step('3/3', fmt.bold('Server Configuration'))}`);
324
+ console.log('');
325
+
326
+ const port = await askWithDefault(
327
+ ` ${fmt.cyan('?')} Local port for the server`,
328
+ config.port || '5555'
329
+ );
330
+
331
+ console.log(` ${fmt.success(`Server will run on port ${fmt.cyan(port)}`)}`);
332
+
333
+ // ── Save config ───────────────────────────────────────────────────
334
+ config.email = email;
335
+ config.port = port;
336
+ saveConfig(config);
337
+
338
+ // ── Summary ───────────────────────────────────────────────────────
339
+ console.log('');
340
+ printSeparator();
341
+ console.log('');
342
+ console.log(` ${fmt.bold('📋 Configuration Summary')}`);
343
+ console.log('');
344
+ console.log(` ${fmt.dim('Server Port')} ${fmt.cyan(port)}`);
345
+ console.log(` ${fmt.dim('Tunnel')} ${fmt.green('ngrok + Google OAuth')}`);
346
+ console.log(` ${fmt.dim('Allowed Email')} ${fmt.cyan(email)}`);
347
+ console.log(` ${fmt.dim('Authtoken')} ${authtoken.slice(0, 8)}••••••••${authtoken.slice(-4)}`);
348
+ console.log('');
349
+
350
+ const proceed = await askYesNo(` ${fmt.cyan('?')} Start the server?`);
351
+
352
+ if (!proceed) {
353
+ console.log('');
354
+ console.log(` ${fmt.dim('Settings saved. Run again anytime!')}`);
355
+ rl.close();
356
+ process.exit(0);
357
+ }
358
+
359
+ rl.close();
360
+
361
+ return { email, port, authtoken };
362
+ }
363
+
364
+ // ── Server Startup ──────────────────────────────────────────────────────
365
+ function startServer({ email, port, authtoken, noTunnel }) {
366
+ const projectRoot = path.resolve(__dirname, '..');
367
+
368
+ console.log('');
369
+ printSeparator();
370
+ console.log('');
371
+ console.log(` ${fmt.bold('🔧 Starting up...')}`);
372
+ console.log('');
373
+
374
+ // ── Build ─────────────────────────────────────────────────────────
375
+ process.stdout.write(` ${fmt.dim('▸ Building Next.js app...')}`);
376
+
377
+ const build = spawn('npx', ['next', 'build'], {
378
+ cwd: projectRoot,
379
+ stdio: ['ignore', 'pipe', 'pipe'],
380
+ shell: true,
381
+ });
382
+
383
+ let buildOutput = '';
384
+ build.stdout.on('data', (data) => { buildOutput += data.toString(); });
385
+ build.stderr.on('data', (data) => { buildOutput += data.toString(); });
386
+
387
+ build.on('close', (code) => {
388
+ clearLine();
389
+
390
+ if (code !== 0) {
391
+ console.log(` ${fmt.error('Build failed!')}`);
392
+ console.log('');
393
+ console.log(buildOutput);
394
+ process.exit(1);
395
+ }
396
+
397
+ console.log(` ${fmt.success('Build complete')}`);
398
+
399
+ // ── Start Next.js ─────────────────────────────────────────────
400
+ process.stdout.write(` ${fmt.dim('▸ Starting server on port ' + port + '...')}`);
401
+
402
+ const nextServer = spawn('npx', ['next', 'start', '-p', port], {
403
+ cwd: projectRoot,
404
+ stdio: ['ignore', 'pipe', 'pipe'],
405
+ shell: true,
406
+ });
407
+
408
+ let serverStarted = false;
409
+
410
+ nextServer.stdout.on('data', (data) => {
411
+ const line = data.toString();
412
+ if (!serverStarted && (line.includes('Ready') || line.includes('started') || line.includes(port))) {
413
+ serverStarted = true;
414
+ clearLine();
415
+ console.log(` ${fmt.success('Server running on port ' + port)}`);
416
+
417
+ if (!noTunnel) {
418
+ startTunnel({ port, email, authtoken });
419
+ } else {
420
+ printLocalOnly(port);
421
+ }
422
+ }
423
+ });
424
+
425
+ nextServer.stderr.on('data', (data) => {
426
+ const line = data.toString().trim();
427
+ // Suppress noisy Next.js output, only show errors
428
+ if (line && line.toLowerCase().includes('error')) {
429
+ console.log(` ${fmt.dim('[next]')} ${line}`);
430
+ }
431
+ });
432
+
433
+ nextServer.on('close', (exitCode) => {
434
+ console.log(`\n ${fmt.dim('Next.js server stopped.')}`);
435
+ process.exit(exitCode);
436
+ });
437
+
438
+ // ── Graceful shutdown ─────────────────────────────────────────
439
+ const cleanup = () => {
440
+ console.log('');
441
+ console.log(` ${fmt.dim('👋 Shutting down...')}`);
442
+ nextServer.kill();
443
+ process.exit(0);
444
+ };
445
+
446
+ process.on('SIGINT', cleanup);
447
+ process.on('SIGTERM', cleanup);
448
+
449
+ // Fallback: if we don't detect "Ready", start tunnel after timeout
450
+ setTimeout(() => {
451
+ if (!serverStarted) {
452
+ serverStarted = true;
453
+ clearLine();
454
+ console.log(` ${fmt.success('Server likely running on port ' + port)}`);
455
+ if (!noTunnel) {
456
+ startTunnel({ port, email, authtoken });
457
+ } else {
458
+ printLocalOnly(port);
459
+ }
460
+ }
461
+ }, 8000);
462
+ });
463
+ }
464
+
465
+ // ── Start ngrok Tunnel ──────────────────────────────────────────────────
466
+ async function startTunnel({ port, email, authtoken }) {
467
+ process.stdout.write(` ${fmt.dim('▸ Opening ngrok tunnel...')}`);
468
+
469
+ try {
470
+ const ngrok = require('@ngrok/ngrok');
471
+
472
+ const listener = await ngrok.forward({
473
+ addr: parseInt(port, 10),
474
+ authtoken: authtoken,
475
+ oauth_provider: 'google',
476
+ oauth_allow_emails: email,
477
+ });
478
+
479
+ clearLine();
480
+ console.log(` ${fmt.success('ngrok tunnel established')}`);
481
+
482
+ const url = listener.url();
483
+
484
+ console.log('');
485
+ console.log(` ${c.bold}${c.cyan}╔═══════════════════════════════════════════════════════╗${c.reset}`);
486
+ console.log(` ${c.bold}${c.cyan}║${c.reset} ${c.bold}${c.cyan}║${c.reset}`);
487
+ console.log(` ${c.bold}${c.cyan}║${c.reset} ${c.green}${c.bold}🌐 Your app is live!${c.reset} ${c.bold}${c.cyan}║${c.reset}`);
488
+ console.log(` ${c.bold}${c.cyan}║${c.reset} ${c.bold}${c.cyan}║${c.reset}`);
489
+ console.log(` ${c.bold}${c.cyan}║${c.reset} ${fmt.bold(url)}${' '.repeat(Math.max(0, 40 - url.length))}${c.bold}${c.cyan}║${c.reset}`);
490
+ console.log(` ${c.bold}${c.cyan}║${c.reset} ${c.bold}${c.cyan}║${c.reset}`);
491
+ console.log(` ${c.bold}${c.cyan}║${c.reset} ${c.dim}🔒 Google OAuth → ${email}${' '.repeat(Math.max(0, 23 - email.length))}${c.reset}${c.bold}${c.cyan}║${c.reset}`);
492
+ console.log(` ${c.bold}${c.cyan}║${c.reset} ${c.bold}${c.cyan}║${c.reset}`);
493
+ console.log(` ${c.bold}${c.cyan}╚═══════════════════════════════════════════════════════╝${c.reset}`);
494
+ console.log('');
495
+ console.log(` ${fmt.dim('Press Ctrl+C to stop.')}`);
496
+ console.log('');
497
+
498
+ } catch (err) {
499
+ clearLine();
500
+ console.log(` ${fmt.error('ngrok tunnel failed!')}`);
501
+ console.log('');
502
+ console.log(` ${fmt.red(err.message)}`);
503
+
504
+ if (err.message.includes('authtoken') || err.message.includes('ERR_NGROK_')) {
505
+ console.log('');
506
+ console.log(` ${fmt.warn('Your authtoken may be invalid or expired.')}`);
507
+ console.log(` ${fmt.dim('Get a new one at:')} ${fmt.link('https://dashboard.ngrok.com/authtokens')}`);
508
+ console.log(` ${fmt.dim('Then run:')} ${fmt.cyan('npx antigravity-chat-proxy --reset')}`);
509
+ }
510
+
511
+ console.log('');
512
+ }
513
+ }
514
+
515
+ function printLocalOnly(port) {
516
+ console.log('');
517
+ console.log(` ${c.bold}${c.cyan}╔═══════════════════════════════════════════════════════╗${c.reset}`);
518
+ console.log(` ${c.bold}${c.cyan}║${c.reset} ${c.bold}${c.cyan}║${c.reset}`);
519
+ console.log(` ${c.bold}${c.cyan}║${c.reset} ${c.green}${c.bold}🖥 Running locally${c.reset} ${c.bold}${c.cyan}║${c.reset}`);
520
+ console.log(` ${c.bold}${c.cyan}║${c.reset} ${fmt.cyan(`http://localhost:${port}`)} ${c.bold}${c.cyan}║${c.reset}`);
521
+ console.log(` ${c.bold}${c.cyan}║${c.reset} ${c.bold}${c.cyan}║${c.reset}`);
522
+ console.log(` ${c.bold}${c.cyan}╚═══════════════════════════════════════════════════════╝${c.reset}`);
523
+ console.log('');
524
+ console.log(` ${fmt.dim('Press Ctrl+C to stop.')}`);
525
+ console.log('');
526
+ }
527
+
528
+ // ── Main ────────────────────────────────────────────────────────────────
529
+ async function main() {
530
+ const args = parseArgs();
531
+
532
+ // Help
533
+ if (args.help) {
534
+ printHelp();
535
+ process.exit(0);
536
+ }
537
+
538
+ // Reset saved config
539
+ if (args.reset) {
540
+ try {
541
+ if (fs.existsSync(CONFIG_FILE)) {
542
+ fs.unlinkSync(CONFIG_FILE);
543
+ console.log(fmt.success('Configuration reset. Run again to reconfigure.'));
544
+ } else {
545
+ console.log(fmt.info('No saved configuration found.'));
546
+ }
547
+ } catch {}
548
+ process.exit(0);
549
+ }
550
+
551
+ // No-tunnel mode
552
+ if (args.noTunnel) {
553
+ printBanner();
554
+ startServer({
555
+ email: null,
556
+ port: args.port || '5555',
557
+ authtoken: null,
558
+ noTunnel: true,
559
+ });
560
+ return;
561
+ }
562
+
563
+ // Non-interactive mode (all args provided via CLI)
564
+ if (args.nonInteractive || (args.email && (args.authtoken || process.env.NGROK_AUTHTOKEN))) {
565
+ printBanner();
566
+ const authtoken = args.authtoken || process.env.NGROK_AUTHTOKEN;
567
+ const port = args.port || '5555';
568
+
569
+ console.log(` ${fmt.dim('Port:')} ${fmt.cyan(port)}`);
570
+ console.log(` ${fmt.dim('Email:')} ${fmt.cyan(args.email)}`);
571
+ console.log(` ${fmt.dim('Tunnel:')} ${fmt.green('ngrok + Google OAuth')}`);
572
+ console.log('');
573
+
574
+ startServer({
575
+ email: args.email,
576
+ port,
577
+ authtoken,
578
+ noTunnel: false,
579
+ });
580
+ return;
581
+ }
582
+
583
+ // Interactive wizard
584
+ try {
585
+ const settings = await runWizard(args);
586
+ startServer({
587
+ email: settings.email,
588
+ port: settings.port,
589
+ authtoken: settings.authtoken,
590
+ noTunnel: false,
591
+ });
592
+ } catch (err) {
593
+ if (err.message === 'readline was closed') {
594
+ process.exit(0);
595
+ }
596
+ console.error(`\n ${fmt.error(err.message)}`);
597
+ process.exit(1);
598
+ }
599
+ }
600
+
601
+ main();