gekto 0.0.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/dist/agents/ClaudeAgent.js +72 -0
- package/dist/agents/HeadlessAgent.js +141 -0
- package/dist/agents/agentPool.js +211 -0
- package/dist/agents/agentWebSocket.js +264 -0
- package/dist/agents/gektoOrchestrator.js +195 -0
- package/dist/agents/gektoPersistent.js +239 -0
- package/dist/agents/gektoSimple.js +137 -0
- package/dist/agents/gektoTools.js +223 -0
- package/dist/proxy.js +318 -0
- package/dist/store.js +53 -0
- package/dist/terminal.js +106 -0
- package/dist/widget/gekto-widget.iife.js +4077 -0
- package/dist/widget/logo.svg +32 -0
- package/dist/widget/vite.svg +1 -0
- package/package.json +37 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
// === Tool Definitions ===
|
|
3
|
+
const TOOLS_DESCRIPTION = `
|
|
4
|
+
Available tools:
|
|
5
|
+
1. chat - For greetings, questions, conversations (not coding tasks)
|
|
6
|
+
2. build - For coding tasks: features, bug fixes, refactoring, file changes
|
|
7
|
+
3. remove - For removing/cleaning up worker agents
|
|
8
|
+
`;
|
|
9
|
+
const GEKTO_SYSTEM_PROMPT = `You are Gekto, a task orchestration assistant. You MUST respond with ONLY valid JSON - no other text.
|
|
10
|
+
|
|
11
|
+
${TOOLS_DESCRIPTION}
|
|
12
|
+
|
|
13
|
+
Analyze the user message and respond with the appropriate tool as JSON.
|
|
14
|
+
|
|
15
|
+
JSON FORMAT (respond with ONLY this, no markdown, no explanation):
|
|
16
|
+
{"tool":"chat"|"build"|"remove","params":{...}}
|
|
17
|
+
|
|
18
|
+
Tool params:
|
|
19
|
+
- chat: {"message":"your response"}
|
|
20
|
+
- build: {"tasks":[{"id":"task_1","description":"Brief desc","prompt":"Detailed prompt for worker agent","files":["path/file.ts"],"dependencies":[]}]}
|
|
21
|
+
- remove: {"target":"all"|"workers"|"completed"|["id1","id2"]}
|
|
22
|
+
|
|
23
|
+
Examples (respond EXACTLY like this):
|
|
24
|
+
"hey" -> {"tool":"chat","params":{"message":"Hey! How can I help?"}}
|
|
25
|
+
"add dark mode" -> {"tool":"build","params":{"tasks":[{"id":"task_1","description":"Add dark mode toggle","prompt":"Implement dark mode toggle in the settings","files":[],"dependencies":[]}]}}
|
|
26
|
+
"remove all agents" -> {"tool":"remove","params":{"target":"all"}}
|
|
27
|
+
"spawn 3 agents" -> {"tool":"build","params":{"tasks":[{"id":"task_1","description":"Agent 1","prompt":"Task 1","files":[],"dependencies":[]},{"id":"task_2","description":"Agent 2","prompt":"Task 2","files":[],"dependencies":[]},{"id":"task_3","description":"Agent 3","prompt":"Task 3","files":[],"dependencies":[]}]}}
|
|
28
|
+
|
|
29
|
+
CRITICAL: Output ONLY the JSON object. No markdown code blocks. No explanation. Just the raw JSON.`;
|
|
30
|
+
// === Main Processing Function ===
|
|
31
|
+
export async function processWithTools(prompt, planId, workingDir, activeAgents = [], callbacks) {
|
|
32
|
+
// Add context about active agents for remove decisions
|
|
33
|
+
const contextPrompt = activeAgents.length > 0
|
|
34
|
+
? `${prompt}\n\n[Context: Active agents: ${activeAgents.map(a => a.lizardId).join(', ')}]`
|
|
35
|
+
: prompt;
|
|
36
|
+
const result = await runClaudeOnce(contextPrompt, GEKTO_SYSTEM_PROMPT, workingDir, callbacks);
|
|
37
|
+
// Parse the JSON response
|
|
38
|
+
try {
|
|
39
|
+
// Try to find JSON - handle markdown code blocks too
|
|
40
|
+
let jsonStr = result.trim();
|
|
41
|
+
// Strip markdown code blocks if present
|
|
42
|
+
if (jsonStr.startsWith('```json')) {
|
|
43
|
+
jsonStr = jsonStr.replace(/^```json\s*/, '').replace(/\s*```$/, '');
|
|
44
|
+
}
|
|
45
|
+
else if (jsonStr.startsWith('```')) {
|
|
46
|
+
jsonStr = jsonStr.replace(/^```\s*/, '').replace(/\s*```$/, '');
|
|
47
|
+
}
|
|
48
|
+
// Find JSON object
|
|
49
|
+
const jsonMatch = jsonStr.match(/\{[\s\S]*\}/);
|
|
50
|
+
if (!jsonMatch) {
|
|
51
|
+
return { type: 'chat', message: result.trim() || "I'm here to help! What would you like me to work on?" };
|
|
52
|
+
}
|
|
53
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
54
|
+
const tool = parsed.tool;
|
|
55
|
+
const params = parsed.params || {};
|
|
56
|
+
switch (tool) {
|
|
57
|
+
case 'chat':
|
|
58
|
+
return {
|
|
59
|
+
type: 'chat',
|
|
60
|
+
message: params.message || 'Hello!',
|
|
61
|
+
};
|
|
62
|
+
case 'build':
|
|
63
|
+
const plan = createPlanFromTasks(params.tasks || [], planId, prompt);
|
|
64
|
+
return {
|
|
65
|
+
type: 'build',
|
|
66
|
+
plan,
|
|
67
|
+
};
|
|
68
|
+
case 'remove':
|
|
69
|
+
return {
|
|
70
|
+
type: 'remove',
|
|
71
|
+
removedAgents: resolveRemoveTarget(params.target, activeAgents),
|
|
72
|
+
};
|
|
73
|
+
default:
|
|
74
|
+
return { type: 'chat', message: "I'm not sure how to help with that." };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Use the raw response as a chat message instead of showing an error
|
|
79
|
+
return { type: 'chat', message: result.trim() || "I'm here to help! What would you like me to work on?" };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// === Helper Functions ===
|
|
83
|
+
function createPlanFromTasks(tasks, planId, originalPrompt) {
|
|
84
|
+
// Extract taskId from planId (planId format: "plan_test_123456")
|
|
85
|
+
// taskId should be "test_123456" for task IDs like "test_123456_1"
|
|
86
|
+
const taskId = planId.replace(/^plan_/, '');
|
|
87
|
+
// Use same format as hardcoded Test button: test_X_1, test_X_2, etc.
|
|
88
|
+
const parsedTasks = tasks.map((t, i) => ({
|
|
89
|
+
id: `${taskId}_${i + 1}`,
|
|
90
|
+
description: t.description || 'Task',
|
|
91
|
+
prompt: t.prompt || originalPrompt,
|
|
92
|
+
files: t.files || [],
|
|
93
|
+
status: 'pending',
|
|
94
|
+
dependencies: t.dependencies || [],
|
|
95
|
+
}));
|
|
96
|
+
// Fallback to single task if empty
|
|
97
|
+
if (parsedTasks.length === 0) {
|
|
98
|
+
parsedTasks.push({
|
|
99
|
+
id: `${taskId}_1`,
|
|
100
|
+
description: 'Execute task',
|
|
101
|
+
prompt: originalPrompt,
|
|
102
|
+
files: [],
|
|
103
|
+
status: 'pending',
|
|
104
|
+
dependencies: [],
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
id: planId,
|
|
109
|
+
status: 'ready',
|
|
110
|
+
originalPrompt,
|
|
111
|
+
tasks: parsedTasks,
|
|
112
|
+
createdAt: new Date().toISOString(),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function resolveRemoveTarget(target, activeAgents) {
|
|
116
|
+
if (Array.isArray(target)) {
|
|
117
|
+
// Specific agent IDs
|
|
118
|
+
return target;
|
|
119
|
+
}
|
|
120
|
+
switch (target) {
|
|
121
|
+
case 'all':
|
|
122
|
+
// All agents (including regular lizards, but not master)
|
|
123
|
+
return activeAgents.filter(a => a.lizardId !== 'master').map(a => a.lizardId);
|
|
124
|
+
case 'workers':
|
|
125
|
+
// Only worker agents (by flag or by ID prefix)
|
|
126
|
+
return activeAgents.filter(a => a.isWorker || a.lizardId.startsWith('worker_')).map(a => a.lizardId);
|
|
127
|
+
case 'completed':
|
|
128
|
+
// This would need status info - for now return workers
|
|
129
|
+
return activeAgents.filter(a => a.isWorker || a.lizardId.startsWith('worker_')).map(a => a.lizardId);
|
|
130
|
+
default:
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// === Claude Helper ===
|
|
135
|
+
function runClaudeOnce(prompt, systemPrompt, workingDir, callbacks) {
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const args = [
|
|
138
|
+
'-p', prompt,
|
|
139
|
+
'--output-format', 'stream-json',
|
|
140
|
+
'--verbose',
|
|
141
|
+
'--model', 'claude-opus-4-5-20251101',
|
|
142
|
+
'--system-prompt', systemPrompt,
|
|
143
|
+
'--dangerously-skip-permissions',
|
|
144
|
+
];
|
|
145
|
+
const proc = spawn('claude', args, {
|
|
146
|
+
cwd: workingDir,
|
|
147
|
+
env: process.env,
|
|
148
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
149
|
+
});
|
|
150
|
+
proc.stdin?.end();
|
|
151
|
+
let buffer = '';
|
|
152
|
+
let resultText = '';
|
|
153
|
+
let currentTool = null;
|
|
154
|
+
proc.stdout.on('data', (data) => {
|
|
155
|
+
buffer += data.toString();
|
|
156
|
+
const lines = buffer.split('\n');
|
|
157
|
+
buffer = lines.pop() || '';
|
|
158
|
+
for (const line of lines) {
|
|
159
|
+
if (!line.trim())
|
|
160
|
+
continue;
|
|
161
|
+
try {
|
|
162
|
+
const event = JSON.parse(line);
|
|
163
|
+
// Stream tool events
|
|
164
|
+
if (event.type === 'assistant' && event.message?.content) {
|
|
165
|
+
for (const block of event.message.content) {
|
|
166
|
+
if (block.type === 'tool_use' && block.name) {
|
|
167
|
+
currentTool = block.name;
|
|
168
|
+
callbacks?.onToolStart?.(block.name, block.input);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Tool completed
|
|
173
|
+
if (event.type === 'user' && event.message?.content) {
|
|
174
|
+
for (const block of event.message.content) {
|
|
175
|
+
if (block.type === 'tool_result' && currentTool) {
|
|
176
|
+
callbacks?.onToolEnd?.(currentTool);
|
|
177
|
+
currentTool = null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Text streaming
|
|
182
|
+
if (event.type === 'content_block_delta') {
|
|
183
|
+
const delta = event.delta;
|
|
184
|
+
if (delta?.type === 'text_delta' && delta.text) {
|
|
185
|
+
callbacks?.onText?.(delta.text);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (event.type === 'result' && event.result) {
|
|
189
|
+
resultText = event.result;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// Ignore parse errors
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
let stderrOutput = '';
|
|
198
|
+
proc.stderr.on('data', (data) => {
|
|
199
|
+
stderrOutput += data.toString();
|
|
200
|
+
});
|
|
201
|
+
proc.on('close', (code) => {
|
|
202
|
+
if (buffer.trim()) {
|
|
203
|
+
try {
|
|
204
|
+
const event = JSON.parse(buffer);
|
|
205
|
+
if (event.type === 'result' && event.result) {
|
|
206
|
+
resultText = event.result;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Ignore
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (resultText) {
|
|
214
|
+
resolve(resultText);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
const errorMsg = stderrOutput || `Process exited with code ${code}`;
|
|
218
|
+
reject(new Error(`No result from Gekto: ${errorMsg}`));
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
proc.on('error', reject);
|
|
222
|
+
});
|
|
223
|
+
}
|
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
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
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
// Parse CLI arguments
|
|
12
|
+
const { values: args } = parseArgs({
|
|
13
|
+
options: {
|
|
14
|
+
port: { type: 'string', short: 'p' },
|
|
15
|
+
target: { type: 'string', short: 't' },
|
|
16
|
+
help: { type: 'boolean', short: 'h' },
|
|
17
|
+
},
|
|
18
|
+
strict: false,
|
|
19
|
+
});
|
|
20
|
+
if (args.help) {
|
|
21
|
+
console.log(`
|
|
22
|
+
Gekto Proxy - Inject widget into any web app
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
bun gekto.ts --target 3000
|
|
26
|
+
bun gekto.ts -t 3000 -p 8080
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
-t, --target Target app port (required)
|
|
30
|
+
-p, --port Proxy port (default: 3200)
|
|
31
|
+
-h, --help Show this help
|
|
32
|
+
`);
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
// Only require --target for bundled version (not dev mode)
|
|
36
|
+
if (!args.target && !process.env.TARGET_PORT && !process.env.GEKTO_DEV) {
|
|
37
|
+
console.error('Error: --target port is required\n');
|
|
38
|
+
console.error('Usage: bun gekto.ts --target 3000');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
// Configuration
|
|
42
|
+
const PROXY_PORT = parseInt(String(args.port ?? process.env.PORT ?? '3200'), 10);
|
|
43
|
+
const TARGET_PORT = parseInt(String(args.target ?? process.env.TARGET_PORT ?? '5173'), 10);
|
|
44
|
+
const WIDGET_PORT = parseInt(process.env.WIDGET_PORT ?? '5174', 10);
|
|
45
|
+
const DEV_MODE = process.env.GEKTO_DEV === '1';
|
|
46
|
+
// Initialize store
|
|
47
|
+
initStore();
|
|
48
|
+
// Widget paths - in dev mode use source, in production use bundled widget folder
|
|
49
|
+
const WIDGET_DIST_PATH = DEV_MODE
|
|
50
|
+
? path.resolve(__dirname, '../../widget/dist')
|
|
51
|
+
: path.resolve(__dirname, './widget');
|
|
52
|
+
const WIDGET_JS_PATH = path.join(WIDGET_DIST_PATH, 'gekto-widget.iife.js');
|
|
53
|
+
const WIDGET_CSS_PATH = path.join(WIDGET_DIST_PATH, 'style.css');
|
|
54
|
+
// Load widget bundle
|
|
55
|
+
function loadWidgetBundle() {
|
|
56
|
+
try {
|
|
57
|
+
const js = fs.readFileSync(WIDGET_JS_PATH, 'utf8');
|
|
58
|
+
const css = fs.existsSync(WIDGET_CSS_PATH)
|
|
59
|
+
? fs.readFileSync(WIDGET_CSS_PATH, 'utf8')
|
|
60
|
+
: '';
|
|
61
|
+
return { js, css };
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.error('❌ Could not load widget bundle:', err);
|
|
65
|
+
return { js: '// Widget bundle not found', css: '' };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Generate injection script
|
|
69
|
+
function getInjectionScript() {
|
|
70
|
+
if (DEV_MODE) {
|
|
71
|
+
// In dev mode, load as ES module from widget dev server
|
|
72
|
+
return `
|
|
73
|
+
<!-- Gekto Widget (dev) -->
|
|
74
|
+
<script type="module" id="gekto-widget" src="http://localhost:${WIDGET_PORT}/src/main.tsx"></script>
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
// In production, load IIFE bundle
|
|
78
|
+
return `
|
|
79
|
+
<!-- Gekto Widget -->
|
|
80
|
+
<script id="gekto-widget" src="/__gekto/widget.js"></script>
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
const server = http.createServer((req, res) => {
|
|
84
|
+
const url = req.url || '/';
|
|
85
|
+
// API: Get lizards (must be before widget handling)
|
|
86
|
+
if (url === '/__gekto/api/lizards' && req.method === 'GET') {
|
|
87
|
+
const lizards = getData('lizards') || [];
|
|
88
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
89
|
+
res.end(JSON.stringify(lizards));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// API: Save lizards (must be before widget handling)
|
|
93
|
+
if (url === '/__gekto/api/lizards' && req.method === 'POST') {
|
|
94
|
+
let body = '';
|
|
95
|
+
req.on('data', chunk => { body += chunk; });
|
|
96
|
+
req.on('end', () => {
|
|
97
|
+
try {
|
|
98
|
+
const lizards = JSON.parse(body);
|
|
99
|
+
setData('lizards', lizards);
|
|
100
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
101
|
+
res.end(JSON.stringify({ success: true }));
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
console.error('[Store] Failed to save lizards:', err);
|
|
105
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
106
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// API: Get chat history for a lizard
|
|
112
|
+
const chatGetMatch = url.match(/^\/__gekto\/api\/chats\/([^/]+)$/);
|
|
113
|
+
if (chatGetMatch && req.method === 'GET') {
|
|
114
|
+
const lizardId = chatGetMatch[1];
|
|
115
|
+
const allChats = getData('chats') || {};
|
|
116
|
+
const messages = allChats[lizardId] || [];
|
|
117
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
118
|
+
res.end(JSON.stringify(messages));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// API: Save chat history for a lizard
|
|
122
|
+
const chatPostMatch = url.match(/^\/__gekto\/api\/chats\/([^/]+)$/);
|
|
123
|
+
if (chatPostMatch && req.method === 'POST') {
|
|
124
|
+
const lizardId = chatPostMatch[1];
|
|
125
|
+
let body = '';
|
|
126
|
+
req.on('data', chunk => { body += chunk; });
|
|
127
|
+
req.on('end', () => {
|
|
128
|
+
try {
|
|
129
|
+
const messages = JSON.parse(body);
|
|
130
|
+
const allChats = getData('chats') || {};
|
|
131
|
+
allChats[lizardId] = messages;
|
|
132
|
+
setData('chats', allChats);
|
|
133
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
134
|
+
res.end(JSON.stringify({ success: true }));
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
console.error('[Store] Failed to save chat:', err);
|
|
138
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
139
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// Serve widget assets - proxy to widget dev server or serve from dist
|
|
145
|
+
if (url.startsWith('/__gekto/')) {
|
|
146
|
+
if (DEV_MODE) {
|
|
147
|
+
// In dev mode, if widget.js is requested, return a module loader script
|
|
148
|
+
// This handles cases where the non-module script tag is cached
|
|
149
|
+
if (url === '/__gekto/widget.js') {
|
|
150
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-cache' });
|
|
151
|
+
res.end(`
|
|
152
|
+
// Dev mode: dynamically load as ES module
|
|
153
|
+
const script = document.createElement('script');
|
|
154
|
+
script.type = 'module';
|
|
155
|
+
script.src = 'http://localhost:${WIDGET_PORT}/src/main.tsx';
|
|
156
|
+
document.head.appendChild(script);
|
|
157
|
+
`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// Proxy other widget assets to widget dev server for HMR
|
|
161
|
+
const widgetPath = url.replace('/__gekto/', '/@fs' + path.resolve(__dirname, '../../widget/') + '/');
|
|
162
|
+
const widgetReq = http.request({
|
|
163
|
+
hostname: 'localhost',
|
|
164
|
+
port: WIDGET_PORT,
|
|
165
|
+
path: widgetPath,
|
|
166
|
+
method: 'GET',
|
|
167
|
+
headers: { host: `localhost:${WIDGET_PORT}` }
|
|
168
|
+
}, (widgetRes) => {
|
|
169
|
+
const headers = {};
|
|
170
|
+
for (const [key, value] of Object.entries(widgetRes.headers)) {
|
|
171
|
+
headers[key] = value;
|
|
172
|
+
}
|
|
173
|
+
res.writeHead(widgetRes.statusCode || 200, headers);
|
|
174
|
+
widgetRes.pipe(res);
|
|
175
|
+
});
|
|
176
|
+
widgetReq.on('error', () => {
|
|
177
|
+
// Fallback to dist if widget dev server not available
|
|
178
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-cache' });
|
|
179
|
+
const { js } = loadWidgetBundle();
|
|
180
|
+
res.end(js);
|
|
181
|
+
});
|
|
182
|
+
widgetReq.end();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
// Production: serve from dist
|
|
187
|
+
if (url === '/__gekto/widget.js') {
|
|
188
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-cache' });
|
|
189
|
+
const { js } = loadWidgetBundle();
|
|
190
|
+
res.end(js);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (url === '/__gekto/widget.css') {
|
|
194
|
+
res.writeHead(200, { 'Content-Type': 'text/css', 'Cache-Control': 'no-cache' });
|
|
195
|
+
const { css } = loadWidgetBundle();
|
|
196
|
+
res.end(css);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Proxy request to target
|
|
202
|
+
// Strip headers that prevent HTML injection from working
|
|
203
|
+
const forwardHeaders = {
|
|
204
|
+
...req.headers,
|
|
205
|
+
host: `localhost:${TARGET_PORT}`
|
|
206
|
+
};
|
|
207
|
+
delete forwardHeaders['accept-encoding']; // Disable compression so we can read HTML
|
|
208
|
+
delete forwardHeaders['if-none-match']; // Prevent 304 responses
|
|
209
|
+
delete forwardHeaders['if-modified-since'];
|
|
210
|
+
const proxyReq = http.request({
|
|
211
|
+
hostname: 'localhost',
|
|
212
|
+
port: TARGET_PORT,
|
|
213
|
+
path: url,
|
|
214
|
+
method: req.method,
|
|
215
|
+
headers: forwardHeaders
|
|
216
|
+
}, (proxyRes) => {
|
|
217
|
+
const contentType = proxyRes.headers['content-type'] || '';
|
|
218
|
+
const isHtml = contentType.includes('text/html');
|
|
219
|
+
if (isHtml) {
|
|
220
|
+
// Buffer HTML response and inject widget
|
|
221
|
+
const chunks = [];
|
|
222
|
+
proxyRes.on('data', (chunk) => chunks.push(chunk));
|
|
223
|
+
proxyRes.on('end', () => {
|
|
224
|
+
let html = Buffer.concat(chunks).toString('utf8');
|
|
225
|
+
const injection = getInjectionScript();
|
|
226
|
+
if (html.includes('</body>')) {
|
|
227
|
+
html = html.replace('</body>', `${injection}</body>`);
|
|
228
|
+
}
|
|
229
|
+
else if (html.includes('</html>')) {
|
|
230
|
+
html = html.replace('</html>', `${injection}</html>`);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
html += injection;
|
|
234
|
+
}
|
|
235
|
+
// Copy headers but remove ones that break injection
|
|
236
|
+
const headers = {};
|
|
237
|
+
const skipHeaders = ['content-length', 'transfer-encoding', 'content-encoding', 'content-security-policy', 'content-security-policy-report-only'];
|
|
238
|
+
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
|
239
|
+
if (!skipHeaders.includes(key.toLowerCase())) {
|
|
240
|
+
headers[key] = value;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
res.writeHead(proxyRes.statusCode || 200, headers);
|
|
244
|
+
res.end(html);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
// Stream non-HTML responses directly
|
|
249
|
+
const headers = {};
|
|
250
|
+
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
|
251
|
+
headers[key] = value;
|
|
252
|
+
}
|
|
253
|
+
res.writeHead(proxyRes.statusCode || 200, headers);
|
|
254
|
+
proxyRes.pipe(res);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
proxyReq.on('error', (err) => {
|
|
258
|
+
res.writeHead(502, { 'Content-Type': 'text/html' });
|
|
259
|
+
res.end(`
|
|
260
|
+
<html>
|
|
261
|
+
<body style="font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: linear-gradient(135deg, #ff6b6b, #ff8e53);">
|
|
262
|
+
<div style="text-align: center; color: white;">
|
|
263
|
+
<h1>🔥 Proxy Error</h1>
|
|
264
|
+
<p>Could not connect to localhost:${TARGET_PORT}</p>
|
|
265
|
+
<pre style="background: rgba(0,0,0,0.2); padding: 10px; border-radius: 5px;">${err.message}</pre>
|
|
266
|
+
</div>
|
|
267
|
+
</body>
|
|
268
|
+
</html>
|
|
269
|
+
`);
|
|
270
|
+
});
|
|
271
|
+
// Forward request body
|
|
272
|
+
req.pipe(proxyReq);
|
|
273
|
+
});
|
|
274
|
+
// Setup terminal WebSocket (handles /__gekto/terminal)
|
|
275
|
+
setupTerminalWebSocket(server);
|
|
276
|
+
// Setup agent WebSocket (handles /__gekto/agent)
|
|
277
|
+
setupAgentWebSocket(server);
|
|
278
|
+
// Handle WebSocket upgrades for Vite HMR (skip gekto paths)
|
|
279
|
+
server.on('upgrade', (req, socket, _head) => {
|
|
280
|
+
const url = req.url || '';
|
|
281
|
+
// Terminal and Agent WebSockets are handled separately
|
|
282
|
+
if (url.startsWith('/__gekto/terminal') || url.startsWith('/__gekto/agent')) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const proxyReq = http.request({
|
|
286
|
+
hostname: 'localhost',
|
|
287
|
+
port: TARGET_PORT,
|
|
288
|
+
path: req.url,
|
|
289
|
+
method: req.method,
|
|
290
|
+
headers: {
|
|
291
|
+
...req.headers,
|
|
292
|
+
host: `localhost:${TARGET_PORT}`
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
proxyReq.on('upgrade', (proxyRes, proxySocket, _proxyHead) => {
|
|
296
|
+
socket.write('HTTP/1.1 101 Switching Protocols\r\n' +
|
|
297
|
+
Object.entries(proxyRes.headers)
|
|
298
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
299
|
+
.join('\r\n') +
|
|
300
|
+
'\r\n\r\n');
|
|
301
|
+
proxySocket.pipe(socket);
|
|
302
|
+
socket.pipe(proxySocket);
|
|
303
|
+
});
|
|
304
|
+
proxyReq.on('error', () => {
|
|
305
|
+
socket.end();
|
|
306
|
+
});
|
|
307
|
+
proxyReq.end();
|
|
308
|
+
});
|
|
309
|
+
server.listen(PROXY_PORT, () => {
|
|
310
|
+
console.log(`
|
|
311
|
+
Gekto Proxy Server
|
|
312
|
+
|
|
313
|
+
Proxy: http://localhost:${PROXY_PORT}
|
|
314
|
+
Target: http://localhost:${TARGET_PORT}
|
|
315
|
+
Mode: ${DEV_MODE ? 'development' : 'production'}${DEV_MODE ? `
|
|
316
|
+
Widget: http://localhost:${WIDGET_PORT}` : ''}
|
|
317
|
+
`);
|
|
318
|
+
});
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const STORE_FILENAME = 'gekto-store.json';
|
|
4
|
+
function getStorePath() {
|
|
5
|
+
return path.join(process.cwd(), STORE_FILENAME);
|
|
6
|
+
}
|
|
7
|
+
function createEmptyStore() {
|
|
8
|
+
const now = new Date().toISOString();
|
|
9
|
+
return {
|
|
10
|
+
version: 1,
|
|
11
|
+
createdAt: now,
|
|
12
|
+
updatedAt: now,
|
|
13
|
+
data: {},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function initStore() {
|
|
17
|
+
const storePath = getStorePath();
|
|
18
|
+
if (!fs.existsSync(storePath)) {
|
|
19
|
+
const emptyStore = createEmptyStore();
|
|
20
|
+
fs.writeFileSync(storePath, JSON.stringify(emptyStore, null, 2), 'utf8');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function readStore() {
|
|
24
|
+
const storePath = getStorePath();
|
|
25
|
+
if (!fs.existsSync(storePath)) {
|
|
26
|
+
initStore();
|
|
27
|
+
}
|
|
28
|
+
const content = fs.readFileSync(storePath, 'utf8');
|
|
29
|
+
return JSON.parse(content);
|
|
30
|
+
}
|
|
31
|
+
export function writeStore(data) {
|
|
32
|
+
const storePath = getStorePath();
|
|
33
|
+
data.updatedAt = new Date().toISOString();
|
|
34
|
+
fs.writeFileSync(storePath, JSON.stringify(data, null, 2), 'utf8');
|
|
35
|
+
}
|
|
36
|
+
export function getData(key) {
|
|
37
|
+
const store = readStore();
|
|
38
|
+
return store.data[key];
|
|
39
|
+
}
|
|
40
|
+
export function setData(key, value) {
|
|
41
|
+
const store = readStore();
|
|
42
|
+
store.data[key] = value;
|
|
43
|
+
writeStore(store);
|
|
44
|
+
}
|
|
45
|
+
export function deleteData(key) {
|
|
46
|
+
const store = readStore();
|
|
47
|
+
if (key in store.data) {
|
|
48
|
+
delete store.data[key];
|
|
49
|
+
writeStore(store);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|