gekto 0.0.6 → 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.
- package/dist/proxy.js +449 -286
- package/package.json +2 -1
package/dist/proxy.js
CHANGED
|
@@ -1,321 +1,484 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
:
|
|
64
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
132
|
+
if (p.isCancel(portAnswer)) {
|
|
133
|
+
p.cancel('Setup cancelled.');
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
136
|
+
TARGET_PORT = parseInt(portAnswer, 10);
|
|
146
137
|
}
|
|
147
|
-
//
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
236
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
<
|
|
268
|
-
<
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
308
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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.
|
|
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
|
},
|