gekto 0.0.5 → 0.0.7

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.
@@ -152,8 +152,8 @@ export function deleteSession(lizardId) {
152
152
  sessions.delete(lizardId);
153
153
  }
154
154
  export function getWorkingDir() {
155
- // In development, use test-app as the working directory
156
- if (process.env.NODE_ENV !== 'production') {
155
+ // In dev mode (GEKTO_DEV=1), use test-app as the working directory
156
+ if (process.env.GEKTO_DEV === '1') {
157
157
  return path.resolve(process.cwd(), '../test-app');
158
158
  }
159
159
  return process.cwd();
package/dist/proxy.js CHANGED
@@ -1,321 +1,484 @@
1
1
  #!/usr/bin/env node
2
- import http from 'http';
3
- import fs from 'fs';
4
- import path from 'path';
5
- import { fileURLToPath } from 'url';
6
- import { parseArgs } from 'util';
7
- import { setupTerminalWebSocket } from './terminal.js';
8
- import { setupAgentWebSocket } from './agents/agentWebSocket.js';
9
- import { initStore, getData, setData } from './store.js';
10
- import { CLAUDE_PATH } from './claudePath.js';
11
- // Re-export for backwards compatibility
12
- export { CLAUDE_PATH };
13
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
- // Parse CLI arguments
15
- const { values: args } = parseArgs({
16
- options: {
17
- port: { type: 'string', short: 'p' },
18
- target: { type: 'string', short: 't' },
19
- help: { type: 'boolean', short: 'h' },
20
- },
21
- strict: false,
22
- });
23
- if (args.help) {
2
+ import * as p from '@clack/prompts';
3
+ // Colors for TUI
4
+ const c = {
5
+ reset: '\x1b[0m',
6
+ bold: '\x1b[1m',
7
+ dim: '\x1b[2m',
8
+ green: '\x1b[32m',
9
+ cyan: '\x1b[36m',
10
+ magenta: '\x1b[35m',
11
+ yellow: '\x1b[33m',
12
+ white: '\x1b[37m',
13
+ };
14
+ function printLogo() {
24
15
  console.log(`
25
- Gekto Proxy - Inject widget into any web app
16
+ ${c.green} ██████╗ ███████╗██╗ ██╗████████╗ ██████╗ ${c.reset}
17
+ ${c.green} ██╔════╝ ██╔════╝██║ ██╔╝╚══██╔══╝██╔═══██╗${c.reset}
18
+ ${c.green} ██║ ███╗█████╗ █████╔╝ ██║ ██║ ██║${c.reset}
19
+ ${c.green} ██║ ██║██╔══╝ ██╔═██╗ ██║ ██║ ██║${c.reset}
20
+ ${c.green} ╚██████╔╝███████╗██║ ██╗ ██║ ╚██████╔╝${c.reset}
21
+ ${c.green} ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ${c.reset}
26
22
 
27
- Usage:
28
- bun gekto.ts --target 3000
29
- bun gekto.ts -t 3000 -p 8080
30
-
31
- Options:
32
- -t, --target Target app port (required)
33
- -p, --port Proxy port (default: 3200)
34
- -h, --help Show this help
35
- `);
36
- process.exit(0);
23
+ ${c.dim} AI Development Assistant${c.reset}
24
+ `);
37
25
  }
38
- // Only require --target for bundled version (not dev mode)
39
- if (!args.target && !process.env.TARGET_PORT && !process.env.GEKTO_DEV) {
40
- console.error('Error: --target port is required\n');
41
- console.error('Usage: npx gekto --target 3000');
42
- process.exit(1);
26
+ // Print a box around lines of text
27
+ function printBox(lines, color = '\x1b[32m') {
28
+ const reset = '\x1b[0m';
29
+ const maxLen = Math.max(...lines.map(l => l.replace(/\x1b\[[0-9;]*m/g, '').length));
30
+ const top = `${color}╭${'─'.repeat(maxLen + 2)}╮${reset}`;
31
+ const bottom = `${color}╰${'─'.repeat(maxLen + 2)}╯${reset}`;
32
+ console.log(top);
33
+ for (const line of lines) {
34
+ const cleanLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
35
+ const padding = ' '.repeat(maxLen - cleanLen);
36
+ console.log(`${color}│${reset} ${line}${padding} ${color}│${reset}`);
37
+ }
38
+ console.log(bottom);
43
39
  }
44
- // Configuration
45
- const PROXY_PORT = parseInt(String(args.port ?? process.env.PORT ?? '3200'), 10);
46
- const TARGET_PORT = parseInt(String(args.target ?? process.env.TARGET_PORT ?? '5173'), 10);
47
- const WIDGET_PORT = parseInt(process.env.WIDGET_PORT ?? '5174', 10);
48
- const DEV_MODE = process.env.GEKTO_DEV === '1';
49
- // Initialize store
50
- initStore();
51
- // Widget paths - in dev mode use source, in production use bundled widget folder
52
- const WIDGET_DIST_PATH = DEV_MODE
53
- ? path.resolve(__dirname, '../../widget/dist')
54
- : path.resolve(__dirname, './widget');
55
- const WIDGET_JS_PATH = path.join(WIDGET_DIST_PATH, 'gekto-widget.iife.js');
56
- const WIDGET_CSS_PATH = path.join(WIDGET_DIST_PATH, 'style.css');
57
- // Load widget bundle
58
- function loadWidgetBundle() {
40
+ // Configuration - will be set after prompts
41
+ let PROXY_PORT = 3200;
42
+ let TARGET_PORT = 5173;
43
+ let PROJECT_TYPE = 'frontend';
44
+ let DEV_MODE = false;
45
+ let WIDGET_PORT = 5174;
46
+ // Save lead to SheetDB
47
+ async function saveLeadToSheetDB(data) {
59
48
  try {
60
- const js = fs.readFileSync(WIDGET_JS_PATH, 'utf8');
61
- const css = fs.existsSync(WIDGET_CSS_PATH)
62
- ? fs.readFileSync(WIDGET_CSS_PATH, 'utf8')
63
- : '';
64
- return { js, css };
65
- }
66
- catch (err) {
67
- console.error('❌ Could not load widget bundle:', err);
68
- return { js: '// Widget bundle not found', css: '' };
49
+ await fetch('https://sheetdb.io/api/v1/hxn1hd5nzjxhd?sheet=npx%20data', {
50
+ method: 'POST',
51
+ headers: { 'Content-Type': 'application/json' },
52
+ body: JSON.stringify({ data }),
53
+ });
69
54
  }
70
- }
71
- // Generate injection script
72
- function getInjectionScript() {
73
- if (DEV_MODE) {
74
- // In dev mode, load as ES module from widget dev server
75
- return `
76
- <!-- Gekto Widget (dev) -->
77
- <script type="module" id="gekto-widget" src="http://localhost:${WIDGET_PORT}/src/main.tsx"></script>
78
- `;
55
+ catch {
56
+ // Silently fail - don't block onboarding
79
57
  }
80
- // In production, load IIFE bundle
81
- return `
82
- <!-- Gekto Widget -->
83
- <script id="gekto-widget" src="/__gekto/widget.js"></script>
84
- `;
85
58
  }
86
- const server = http.createServer((req, res) => {
87
- const url = req.url || '/';
88
- // API: Get lizards (must be before widget handling)
89
- if (url === '/__gekto/api/lizards' && req.method === 'GET') {
90
- const lizards = getData('lizards') || [];
91
- res.writeHead(200, { 'Content-Type': 'application/json' });
92
- res.end(JSON.stringify(lizards));
59
+ // Load settings from store (imported dynamically later)
60
+ let loadSettings;
61
+ let saveSettings;
62
+ // Onboarding prompts - runs FIRST before anything else
63
+ async function runOnboarding() {
64
+ // Skip onboarding in dev mode (bun run dev)
65
+ if (process.env.GEKTO_DEV === '1') {
66
+ DEV_MODE = true;
67
+ TARGET_PORT = 5173;
93
68
  return;
94
69
  }
95
- // API: Save lizards (must be before widget handling)
96
- if (url === '/__gekto/api/lizards' && req.method === 'POST') {
97
- let body = '';
98
- req.on('data', chunk => { body += chunk; });
99
- req.on('end', () => {
100
- try {
101
- const lizards = JSON.parse(body);
102
- setData('lizards', lizards);
103
- res.writeHead(200, { 'Content-Type': 'application/json' });
104
- res.end(JSON.stringify({ success: true }));
70
+ // Dynamic import to avoid loading before onboarding
71
+ const fs = await import('fs');
72
+ const path = await import('path');
73
+ const STORE_PATH = path.join(process.cwd(), 'gekto-store.json');
74
+ loadSettings = () => {
75
+ try {
76
+ if (fs.existsSync(STORE_PATH)) {
77
+ const store = JSON.parse(fs.readFileSync(STORE_PATH, 'utf8'));
78
+ return store.data?.settings;
105
79
  }
106
- catch (err) {
107
- console.error('[Store] Failed to save lizards:', err);
108
- res.writeHead(400, { 'Content-Type': 'application/json' });
109
- res.end(JSON.stringify({ error: 'Invalid JSON' }));
80
+ }
81
+ catch { }
82
+ return undefined;
83
+ };
84
+ saveSettings = (settings) => {
85
+ let store = { version: 1, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), data: {} };
86
+ try {
87
+ if (fs.existsSync(STORE_PATH)) {
88
+ store = JSON.parse(fs.readFileSync(STORE_PATH, 'utf8'));
110
89
  }
111
- });
90
+ }
91
+ catch { }
92
+ store.data.settings = settings;
93
+ store.updatedAt = new Date().toISOString();
94
+ fs.writeFileSync(STORE_PATH, JSON.stringify(store, null, 2));
95
+ };
96
+ // Check for existing settings
97
+ const existingSettings = loadSettings();
98
+ if (existingSettings?.onboardingCompleted) {
99
+ // Use saved settings
100
+ PROJECT_TYPE = existingSettings.projectType;
101
+ TARGET_PORT = existingSettings.targetPort;
102
+ PROXY_PORT = existingSettings.proxyPort;
103
+ console.log(`${c.dim}Loaded settings from gekto-store.json${c.reset}`);
112
104
  return;
113
105
  }
114
- // API: Get chat history for a lizard
115
- const chatGetMatch = url.match(/^\/__gekto\/api\/chats\/([^/]+)$/);
116
- if (chatGetMatch && req.method === 'GET') {
117
- const lizardId = chatGetMatch[1];
118
- const allChats = getData('chats') || {};
119
- const messages = allChats[lizardId] || [];
120
- res.writeHead(200, { 'Content-Type': 'application/json' });
121
- res.end(JSON.stringify(messages));
122
- return;
106
+ console.clear();
107
+ p.intro(`${c.green}${c.bold}create-gekto${c.reset}`);
108
+ // Ask project type
109
+ const projectType = await p.select({
110
+ message: 'Select project type',
111
+ options: [
112
+ { label: 'Frontend (React, Vue, Next.js, etc.)', value: 'frontend' },
113
+ { label: 'Backend (Node.js, Express, FastAPI, etc.)', value: 'backend' },
114
+ { label: 'CLI (Command-line tools)', value: 'cli' },
115
+ { label: 'Fullstack (Frontend + Backend)', value: 'fullstack' },
116
+ { label: 'Mobile (React Native, Flutter, etc.)', value: 'mobile' },
117
+ { label: 'Other', value: 'other' },
118
+ ],
119
+ });
120
+ if (p.isCancel(projectType)) {
121
+ p.cancel('Setup cancelled.');
122
+ process.exit(0);
123
123
  }
124
- // API: Save chat history for a lizard
125
- const chatPostMatch = url.match(/^\/__gekto\/api\/chats\/([^/]+)$/);
126
- if (chatPostMatch && req.method === 'POST') {
127
- const lizardId = chatPostMatch[1];
128
- let body = '';
129
- req.on('data', chunk => { body += chunk; });
130
- req.on('end', () => {
131
- try {
132
- const messages = JSON.parse(body);
133
- const allChats = getData('chats') || {};
134
- allChats[lizardId] = messages;
135
- setData('chats', allChats);
136
- res.writeHead(200, { 'Content-Type': 'application/json' });
137
- res.end(JSON.stringify({ success: true }));
138
- }
139
- catch (err) {
140
- console.error('[Store] Failed to save chat:', err);
141
- res.writeHead(400, { 'Content-Type': 'application/json' });
142
- res.end(JSON.stringify({ error: 'Invalid JSON' }));
143
- }
124
+ PROJECT_TYPE = projectType;
125
+ // For frontend apps, ask for the port
126
+ if (PROJECT_TYPE === 'frontend') {
127
+ const portAnswer = await p.text({
128
+ message: 'What port is your app running on?',
129
+ placeholder: '3000',
130
+ defaultValue: '3000',
144
131
  });
145
- return;
132
+ if (p.isCancel(portAnswer)) {
133
+ p.cancel('Setup cancelled.');
134
+ process.exit(0);
135
+ }
136
+ TARGET_PORT = parseInt(portAnswer, 10);
146
137
  }
147
- // Serve widget assets - proxy to widget dev server or serve from dist
148
- if (url.startsWith('/__gekto/')) {
138
+ // Ask for email
139
+ const emailAnswer = await p.text({
140
+ message: 'Enter your email for updates (optional)',
141
+ placeholder: 'you@example.com',
142
+ });
143
+ if (p.isCancel(emailAnswer)) {
144
+ p.cancel('Setup cancelled.');
145
+ process.exit(0);
146
+ }
147
+ const email = emailAnswer || '';
148
+ // Show spinner while saving
149
+ const spinner = p.spinner();
150
+ spinner.start('Preparing Gekto...');
151
+ // Save lead to SheetDB (always send, even if email is empty)
152
+ await saveLeadToSheetDB({
153
+ project_type: PROJECT_TYPE,
154
+ port: String(TARGET_PORT),
155
+ email,
156
+ });
157
+ // Save settings for next time
158
+ saveSettings({
159
+ projectType: PROJECT_TYPE,
160
+ targetPort: TARGET_PORT,
161
+ proxyPort: PROXY_PORT,
162
+ onboardingCompleted: true,
163
+ email,
164
+ });
165
+ spinner.stop('Ready!');
166
+ p.outro(`${c.green}Starting Gekto...${c.reset}`);
167
+ }
168
+ // Main function - runs after onboarding completes
169
+ async function main() {
170
+ // === STEP 1: Run onboarding FIRST (nothing else runs yet) ===
171
+ await runOnboarding();
172
+ // === STEP 2: Now load all the heavy modules ===
173
+ const http = await import('http');
174
+ const fs = await import('fs');
175
+ const path = await import('path');
176
+ const { fileURLToPath } = await import('url');
177
+ const { parseArgs } = await import('util');
178
+ const { setupTerminalWebSocket } = await import('./terminal.js');
179
+ const { setupAgentWebSocket } = await import('./agents/agentWebSocket.js');
180
+ const { initStore, getData, setData } = await import('./store.js');
181
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
182
+ // Parse CLI arguments (for overrides)
183
+ const { values: args } = parseArgs({
184
+ options: {
185
+ port: { type: 'string', short: 'p' },
186
+ target: { type: 'string', short: 't' },
187
+ },
188
+ strict: false,
189
+ });
190
+ // Apply CLI overrides if provided
191
+ if (args.port && typeof args.port === 'string')
192
+ PROXY_PORT = parseInt(args.port, 10);
193
+ if (args.target && typeof args.target === 'string')
194
+ TARGET_PORT = parseInt(args.target, 10);
195
+ DEV_MODE = process.env.GEKTO_DEV === '1';
196
+ WIDGET_PORT = parseInt(process.env.WIDGET_PORT ?? '5174', 10);
197
+ // Initialize store
198
+ initStore();
199
+ // Widget paths - check multiple locations
200
+ const possibleWidgetPaths = [
201
+ path.resolve(__dirname, './widget'), // production (dist/widget)
202
+ path.resolve(__dirname, '../../widget/dist'), // dev/preview mode
203
+ ];
204
+ const WIDGET_DIST_PATH = possibleWidgetPaths.find(p => fs.existsSync(path.join(p, 'gekto-widget.iife.js'))) || possibleWidgetPaths[0];
205
+ const WIDGET_JS_PATH = path.join(WIDGET_DIST_PATH, 'gekto-widget.iife.js');
206
+ const WIDGET_CSS_PATH = path.join(WIDGET_DIST_PATH, 'style.css');
207
+ // Load widget bundle
208
+ function loadWidgetBundle() {
209
+ try {
210
+ const js = fs.readFileSync(WIDGET_JS_PATH, 'utf8');
211
+ const css = fs.existsSync(WIDGET_CSS_PATH)
212
+ ? fs.readFileSync(WIDGET_CSS_PATH, 'utf8')
213
+ : '';
214
+ return { js, css };
215
+ }
216
+ catch (err) {
217
+ console.error('❌ Could not load widget bundle:', err);
218
+ return { js: '// Widget bundle not found', css: '' };
219
+ }
220
+ }
221
+ // Generate injection script
222
+ function getInjectionScript() {
149
223
  if (DEV_MODE) {
150
- // In dev mode, if widget.js is requested, return a module loader script
151
- // This handles cases where the non-module script tag is cached
152
- if (url === '/__gekto/widget.js') {
153
- res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-cache' });
154
- res.end(`
155
- // Dev mode: dynamically load as ES module
156
- const script = document.createElement('script');
157
- script.type = 'module';
158
- script.src = 'http://localhost:${WIDGET_PORT}/src/main.tsx';
159
- document.head.appendChild(script);
160
- `);
161
- return;
162
- }
163
- // Proxy other widget assets to widget dev server for HMR
164
- const widgetPath = url.replace('/__gekto/', '/@fs' + path.resolve(__dirname, '../../widget/') + '/');
165
- const widgetReq = http.request({
166
- hostname: 'localhost',
167
- port: WIDGET_PORT,
168
- path: widgetPath,
169
- method: 'GET',
170
- headers: { host: `localhost:${WIDGET_PORT}` }
171
- }, (widgetRes) => {
172
- const headers = {};
173
- for (const [key, value] of Object.entries(widgetRes.headers)) {
174
- headers[key] = value;
224
+ return `
225
+ <!-- Gekto Widget (dev) -->
226
+ <script type="module" id="gekto-widget" src="http://localhost:${WIDGET_PORT}/src/main.tsx"></script>
227
+ `;
228
+ }
229
+ return `
230
+ <!-- Gekto Widget -->
231
+ <script id="gekto-widget" src="/__gekto/widget.js"></script>
232
+ `;
233
+ }
234
+ // === STEP 3: Create and start server ===
235
+ const server = http.createServer((req, res) => {
236
+ const url = req.url || '/';
237
+ // API: Get lizards
238
+ if (url === '/__gekto/api/lizards' && req.method === 'GET') {
239
+ const lizards = getData('lizards') || [];
240
+ res.writeHead(200, { 'Content-Type': 'application/json' });
241
+ res.end(JSON.stringify(lizards));
242
+ return;
243
+ }
244
+ // API: Save lizards
245
+ if (url === '/__gekto/api/lizards' && req.method === 'POST') {
246
+ let body = '';
247
+ req.on('data', (chunk) => { body += chunk; });
248
+ req.on('end', () => {
249
+ try {
250
+ const lizards = JSON.parse(body);
251
+ setData('lizards', lizards);
252
+ res.writeHead(200, { 'Content-Type': 'application/json' });
253
+ res.end(JSON.stringify({ success: true }));
254
+ }
255
+ catch (err) {
256
+ res.writeHead(400, { 'Content-Type': 'application/json' });
257
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
175
258
  }
176
- res.writeHead(widgetRes.statusCode || 200, headers);
177
- widgetRes.pipe(res);
178
- });
179
- widgetReq.on('error', () => {
180
- // Fallback to dist if widget dev server not available
181
- res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-cache' });
182
- const { js } = loadWidgetBundle();
183
- res.end(js);
184
259
  });
185
- widgetReq.end();
186
260
  return;
187
261
  }
188
- else {
189
- // Production: serve from dist
190
- if (url === '/__gekto/widget.js') {
191
- res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-cache' });
192
- const { js } = loadWidgetBundle();
193
- res.end(js);
194
- return;
195
- }
196
- if (url === '/__gekto/widget.css') {
197
- res.writeHead(200, { 'Content-Type': 'text/css', 'Cache-Control': 'no-cache' });
198
- const { css } = loadWidgetBundle();
199
- res.end(css);
200
- return;
201
- }
262
+ // API: Get chat history
263
+ const chatGetMatch = url.match(/^\/__gekto\/api\/chats\/([^/]+)$/);
264
+ if (chatGetMatch && req.method === 'GET') {
265
+ const lizardId = chatGetMatch[1];
266
+ const allChats = getData('chats') || {};
267
+ const messages = allChats[lizardId] || [];
268
+ res.writeHead(200, { 'Content-Type': 'application/json' });
269
+ res.end(JSON.stringify(messages));
270
+ return;
202
271
  }
203
- }
204
- // Proxy request to target
205
- // Strip headers that prevent HTML injection from working
206
- const forwardHeaders = {
207
- ...req.headers,
208
- host: `localhost:${TARGET_PORT}`
209
- };
210
- delete forwardHeaders['accept-encoding']; // Disable compression so we can read HTML
211
- delete forwardHeaders['if-none-match']; // Prevent 304 responses
212
- delete forwardHeaders['if-modified-since'];
213
- const proxyReq = http.request({
214
- hostname: 'localhost',
215
- port: TARGET_PORT,
216
- path: url,
217
- method: req.method,
218
- headers: forwardHeaders
219
- }, (proxyRes) => {
220
- const contentType = proxyRes.headers['content-type'] || '';
221
- const isHtml = contentType.includes('text/html');
222
- if (isHtml) {
223
- // Buffer HTML response and inject widget
224
- const chunks = [];
225
- proxyRes.on('data', (chunk) => chunks.push(chunk));
226
- proxyRes.on('end', () => {
227
- let html = Buffer.concat(chunks).toString('utf8');
228
- const injection = getInjectionScript();
229
- if (html.includes('</body>')) {
230
- html = html.replace('</body>', `${injection}</body>`);
231
- }
232
- else if (html.includes('</html>')) {
233
- html = html.replace('</html>', `${injection}</html>`);
272
+ // API: Save chat history
273
+ const chatPostMatch = url.match(/^\/__gekto\/api\/chats\/([^/]+)$/);
274
+ if (chatPostMatch && req.method === 'POST') {
275
+ const lizardId = chatPostMatch[1];
276
+ let body = '';
277
+ req.on('data', (chunk) => { body += chunk; });
278
+ req.on('end', () => {
279
+ try {
280
+ const messages = JSON.parse(body);
281
+ const allChats = getData('chats') || {};
282
+ allChats[lizardId] = messages;
283
+ setData('chats', allChats);
284
+ res.writeHead(200, { 'Content-Type': 'application/json' });
285
+ res.end(JSON.stringify({ success: true }));
234
286
  }
235
- else {
236
- html += injection;
287
+ catch (err) {
288
+ res.writeHead(400, { 'Content-Type': 'application/json' });
289
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
237
290
  }
238
- // Copy headers but remove ones that break injection
239
- const headers = {};
240
- const skipHeaders = ['content-length', 'transfer-encoding', 'content-encoding', 'content-security-policy', 'content-security-policy-report-only'];
241
- for (const [key, value] of Object.entries(proxyRes.headers)) {
242
- if (!skipHeaders.includes(key.toLowerCase())) {
243
- headers[key] = value;
244
- }
245
- }
246
- res.writeHead(proxyRes.statusCode || 200, headers);
247
- res.end(html);
248
291
  });
292
+ return;
249
293
  }
250
- else {
251
- // Stream non-HTML responses directly
252
- const headers = {};
253
- for (const [key, value] of Object.entries(proxyRes.headers)) {
254
- headers[key] = value;
294
+ // Serve widget assets
295
+ if (url.startsWith('/__gekto/')) {
296
+ if (DEV_MODE) {
297
+ if (url === '/__gekto/widget.js') {
298
+ res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-cache' });
299
+ res.end(`
300
+ const script = document.createElement('script');
301
+ script.type = 'module';
302
+ script.src = 'http://localhost:${WIDGET_PORT}/src/main.tsx';
303
+ document.head.appendChild(script);
304
+ `);
305
+ return;
306
+ }
307
+ const widgetPath = url.replace('/__gekto/', '/@fs' + path.resolve(__dirname, '../../widget/') + '/');
308
+ const widgetReq = http.request({
309
+ hostname: 'localhost',
310
+ port: WIDGET_PORT,
311
+ path: widgetPath,
312
+ method: 'GET',
313
+ headers: { host: `localhost:${WIDGET_PORT}` }
314
+ }, (widgetRes) => {
315
+ res.writeHead(widgetRes.statusCode || 200, widgetRes.headers);
316
+ widgetRes.pipe(res);
317
+ });
318
+ widgetReq.on('error', () => {
319
+ res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-cache' });
320
+ res.end(loadWidgetBundle().js);
321
+ });
322
+ widgetReq.end();
323
+ return;
324
+ }
325
+ else {
326
+ if (url === '/__gekto/widget.js') {
327
+ res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-cache' });
328
+ res.end(loadWidgetBundle().js);
329
+ return;
330
+ }
331
+ if (url === '/__gekto/widget.css') {
332
+ res.writeHead(200, { 'Content-Type': 'text/css', 'Cache-Control': 'no-cache' });
333
+ res.end(loadWidgetBundle().css);
334
+ return;
335
+ }
255
336
  }
256
- res.writeHead(proxyRes.statusCode || 200, headers);
257
- proxyRes.pipe(res);
258
337
  }
259
- });
260
- proxyReq.on('error', (err) => {
261
- res.writeHead(502, { 'Content-Type': 'text/html' });
262
- res.end(`
263
- <html>
264
- <body style="font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: linear-gradient(135deg, #ff6b6b, #ff8e53);">
265
- <div style="text-align: center; color: white;">
266
- <h1>🔥 Proxy Error</h1>
267
- <p>Could not connect to localhost:${TARGET_PORT}</p>
268
- <pre style="background: rgba(0,0,0,0.2); padding: 10px; border-radius: 5px;">${err.message}</pre>
269
- </div>
270
- </body>
271
- </html>
272
- `);
273
- });
274
- // Forward request body
275
- req.pipe(proxyReq);
276
- });
277
- // Setup terminal WebSocket (handles /__gekto/terminal)
278
- setupTerminalWebSocket(server);
279
- // Setup agent WebSocket (handles /__gekto/agent)
280
- setupAgentWebSocket(server);
281
- // Handle WebSocket upgrades for Vite HMR (skip gekto paths)
282
- server.on('upgrade', (req, socket, _head) => {
283
- const url = req.url || '';
284
- // Terminal and Agent WebSockets are handled separately
285
- if (url.startsWith('/__gekto/terminal') || url.startsWith('/__gekto/agent')) {
286
- return;
287
- }
288
- const proxyReq = http.request({
289
- hostname: 'localhost',
290
- port: TARGET_PORT,
291
- path: req.url,
292
- method: req.method,
293
- headers: {
338
+ // For non-frontend projects, serve standalone page
339
+ if (PROJECT_TYPE !== 'frontend' && (url === '/' || url === '/index.html')) {
340
+ const injection = getInjectionScript();
341
+ res.writeHead(200, { 'Content-Type': 'text/html' });
342
+ res.end(`
343
+ <!DOCTYPE html>
344
+ <html>
345
+ <head>
346
+ <meta charset="utf-8">
347
+ <meta name="viewport" content="width=device-width, initial-scale=1">
348
+ <title>Gekto</title>
349
+ <style>
350
+ * { margin: 0; padding: 0; box-sizing: border-box; }
351
+ body { background: #0a0a0a; min-height: 100vh; }
352
+ </style>
353
+ </head>
354
+ <body>
355
+ ${injection}
356
+ </body>
357
+ </html>
358
+ `);
359
+ return;
360
+ }
361
+ // Proxy request to target
362
+ const forwardHeaders = {
294
363
  ...req.headers,
295
364
  host: `localhost:${TARGET_PORT}`
296
- }
365
+ };
366
+ delete forwardHeaders['accept-encoding'];
367
+ delete forwardHeaders['if-none-match'];
368
+ delete forwardHeaders['if-modified-since'];
369
+ const proxyReq = http.request({
370
+ hostname: 'localhost',
371
+ port: TARGET_PORT,
372
+ path: url,
373
+ method: req.method,
374
+ headers: forwardHeaders
375
+ }, (proxyRes) => {
376
+ const contentType = proxyRes.headers['content-type'] || '';
377
+ const isHtml = contentType.includes('text/html');
378
+ if (isHtml) {
379
+ const chunks = [];
380
+ proxyRes.on('data', (chunk) => chunks.push(chunk));
381
+ proxyRes.on('end', () => {
382
+ let html = Buffer.concat(chunks).toString('utf8');
383
+ const injection = getInjectionScript();
384
+ if (html.includes('</body>')) {
385
+ html = html.replace('</body>', `${injection}</body>`);
386
+ }
387
+ else if (html.includes('</html>')) {
388
+ html = html.replace('</html>', `${injection}</html>`);
389
+ }
390
+ else {
391
+ html += injection;
392
+ }
393
+ const headers = {};
394
+ const skipHeaders = ['content-length', 'transfer-encoding', 'content-encoding', 'content-security-policy', 'content-security-policy-report-only'];
395
+ for (const [key, value] of Object.entries(proxyRes.headers)) {
396
+ if (!skipHeaders.includes(key.toLowerCase())) {
397
+ headers[key] = value;
398
+ }
399
+ }
400
+ res.writeHead(proxyRes.statusCode || 200, headers);
401
+ res.end(html);
402
+ });
403
+ }
404
+ else {
405
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
406
+ proxyRes.pipe(res);
407
+ }
408
+ });
409
+ proxyReq.on('error', (err) => {
410
+ res.writeHead(502, { 'Content-Type': 'text/html' });
411
+ res.end(`
412
+ <html>
413
+ <body style="font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: linear-gradient(135deg, #ff6b6b, #ff8e53);">
414
+ <div style="text-align: center; color: white;">
415
+ <h1>🔥 Proxy Error</h1>
416
+ <p>Could not connect to localhost:${TARGET_PORT}</p>
417
+ <pre style="background: rgba(0,0,0,0.2); padding: 10px; border-radius: 5px;">${err.message}</pre>
418
+ </div>
419
+ </body>
420
+ </html>
421
+ `);
422
+ });
423
+ req.pipe(proxyReq);
297
424
  });
298
- proxyReq.on('upgrade', (proxyRes, proxySocket, _proxyHead) => {
299
- socket.write('HTTP/1.1 101 Switching Protocols\r\n' +
300
- Object.entries(proxyRes.headers)
301
- .map(([k, v]) => `${k}: ${v}`)
302
- .join('\r\n') +
303
- '\r\n\r\n');
304
- proxySocket.pipe(socket);
305
- socket.pipe(proxySocket);
425
+ // Setup WebSockets
426
+ setupTerminalWebSocket(server);
427
+ setupAgentWebSocket(server);
428
+ // Handle WebSocket upgrades for Vite HMR
429
+ server.on('upgrade', (req, socket, _head) => {
430
+ const url = req.url || '';
431
+ if (url.startsWith('/__gekto/terminal') || url.startsWith('/__gekto/agent')) {
432
+ return;
433
+ }
434
+ const proxyReq = http.request({
435
+ hostname: 'localhost',
436
+ port: TARGET_PORT,
437
+ path: req.url,
438
+ method: req.method,
439
+ headers: { ...req.headers, host: `localhost:${TARGET_PORT}` }
440
+ });
441
+ proxyReq.on('upgrade', (proxyRes, proxySocket, _proxyHead) => {
442
+ socket.write('HTTP/1.1 101 Switching Protocols\r\n' +
443
+ Object.entries(proxyRes.headers).map(([k, v]) => `${k}: ${v}`).join('\r\n') +
444
+ '\r\n\r\n');
445
+ proxySocket.pipe(socket);
446
+ socket.pipe(proxySocket);
447
+ });
448
+ proxyReq.on('error', () => socket.end());
449
+ proxyReq.end();
306
450
  });
307
- proxyReq.on('error', () => {
308
- socket.end();
451
+ // === STEP 4: Show logo and start listening ===
452
+ console.clear();
453
+ printLogo();
454
+ server.listen(PROXY_PORT, () => {
455
+ if (PROJECT_TYPE === 'frontend') {
456
+ printBox([
457
+ `${c.bold}Gekto is ready!${c.reset}`,
458
+ ``,
459
+ `${c.dim}Source:${c.reset} ${c.cyan}http://localhost:${TARGET_PORT}${c.reset}`,
460
+ `${c.dim}Proxy:${c.reset} ${c.magenta}http://localhost:${PROXY_PORT}${c.reset}`,
461
+ `${c.dim}Mode:${c.reset} ${c.yellow}${DEV_MODE ? 'development' : 'production'}${c.reset}`,
462
+ ``,
463
+ `${c.dim}Open ${c.white}http://localhost:${PROXY_PORT}${c.dim} in your browser${c.reset}`,
464
+ `${c.dim}Press ${c.white}Ctrl+C${c.dim} to stop${c.reset}`,
465
+ ], c.green);
466
+ }
467
+ else {
468
+ printBox([
469
+ `${c.bold}Gekto is ready!${c.reset}`,
470
+ ``,
471
+ `${c.dim}Open:${c.reset} ${c.magenta}http://localhost:${PROXY_PORT}${c.reset}`,
472
+ `${c.dim}Mode:${c.reset} ${c.yellow}${DEV_MODE ? 'development' : 'production'}${c.reset}`,
473
+ ``,
474
+ `${c.dim}Press ${c.white}Ctrl+C${c.dim} to stop${c.reset}`,
475
+ ], c.green);
476
+ }
477
+ // Footer
478
+ console.log();
479
+ console.log(` ${c.dim}Enjoying Gekto? ⭐ Star us on GitHub: ${c.white}https://github.com/Badaboom1995/gekto${c.reset}`);
480
+ console.log();
309
481
  });
310
- proxyReq.end();
311
- });
312
- server.listen(PROXY_PORT, () => {
313
- console.log(`
314
- Gekto Proxy Server
315
-
316
- Proxy: http://localhost:${PROXY_PORT}
317
- Target: http://localhost:${TARGET_PORT}
318
- Mode: ${DEV_MODE ? 'development' : 'production'}${DEV_MODE ? `
319
- Widget: http://localhost:${WIDGET_PORT}` : ''}
320
- `);
321
- });
482
+ }
483
+ // Run
484
+ main().catch(console.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gekto",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "AI coding assistant widget - inject into any web app",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,6 +25,7 @@
25
25
  ],
26
26
  "license": "MIT",
27
27
  "dependencies": {
28
+ "@clack/prompts": "^1.0.0",
28
29
  "node-pty": "^1.1.0-beta30",
29
30
  "ws": "^8.18.3"
30
31
  },