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,72 @@
|
|
|
1
|
+
import * as pty from 'node-pty';
|
|
2
|
+
export class ClaudeAgent {
|
|
3
|
+
pty;
|
|
4
|
+
buffer = '';
|
|
5
|
+
state = 'loading';
|
|
6
|
+
config;
|
|
7
|
+
readyPromise;
|
|
8
|
+
resolveReady;
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.readyPromise = new Promise((resolve) => {
|
|
12
|
+
this.resolveReady = resolve;
|
|
13
|
+
});
|
|
14
|
+
this.pty = pty.spawn('claude', [], {
|
|
15
|
+
name: 'xterm-256color',
|
|
16
|
+
cols: 120,
|
|
17
|
+
rows: 40,
|
|
18
|
+
cwd: config.workingDir,
|
|
19
|
+
env: process.env,
|
|
20
|
+
});
|
|
21
|
+
this.pty.onData((data) => {
|
|
22
|
+
this.buffer += data;
|
|
23
|
+
this.config.onOutput(data);
|
|
24
|
+
this.detectState(data);
|
|
25
|
+
});
|
|
26
|
+
this.pty.onExit(({ exitCode }) => {
|
|
27
|
+
console.log(`[ClaudeAgent] Exited with code ${exitCode}`);
|
|
28
|
+
this.setState('error');
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
setState(state) {
|
|
32
|
+
if (this.state !== state) {
|
|
33
|
+
this.state = state;
|
|
34
|
+
this.config.onStateChange(state);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
detectState(data) {
|
|
38
|
+
// Strip ANSI codes for analysis
|
|
39
|
+
const clean = data.replace(/\x1b\[[0-9;]*m/g, '').replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
|
|
40
|
+
// Check for ready prompt (Claude Code shows > or ❯ when ready for input)
|
|
41
|
+
// This indicates Claude has finished and is waiting for next input
|
|
42
|
+
if ((clean.includes('>') || clean.includes('❯')) && (this.state === 'loading' || this.state === 'working')) {
|
|
43
|
+
if (this.state === 'loading') {
|
|
44
|
+
this.config.onReady();
|
|
45
|
+
this.resolveReady();
|
|
46
|
+
}
|
|
47
|
+
this.setState('ready');
|
|
48
|
+
}
|
|
49
|
+
// Check for permission prompts
|
|
50
|
+
if (/Do you want to|Allow|Proceed\?|\[y\/N\]|\[Y\/n\]/i.test(clean)) {
|
|
51
|
+
this.setState('waiting_input');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async waitUntilReady() {
|
|
55
|
+
return this.readyPromise;
|
|
56
|
+
}
|
|
57
|
+
sendMessage(message) {
|
|
58
|
+
this.setState('working');
|
|
59
|
+
this.buffer = '';
|
|
60
|
+
this.pty.write(message + '\r');
|
|
61
|
+
}
|
|
62
|
+
respond(input) {
|
|
63
|
+
this.setState('working');
|
|
64
|
+
this.pty.write(input + '\r');
|
|
65
|
+
}
|
|
66
|
+
getState() {
|
|
67
|
+
return this.state;
|
|
68
|
+
}
|
|
69
|
+
kill() {
|
|
70
|
+
this.pty.kill();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
export class HeadlessAgent {
|
|
3
|
+
sessionId = null;
|
|
4
|
+
config;
|
|
5
|
+
currentProc = null;
|
|
6
|
+
constructor(config = {}) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
}
|
|
9
|
+
kill() {
|
|
10
|
+
if (this.currentProc && !this.currentProc.killed) {
|
|
11
|
+
this.currentProc.kill('SIGTERM');
|
|
12
|
+
this.currentProc = null;
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
isRunning() {
|
|
18
|
+
return this.currentProc !== null && !this.currentProc.killed;
|
|
19
|
+
}
|
|
20
|
+
async send(message, callbacks) {
|
|
21
|
+
const args = [
|
|
22
|
+
'-p', message,
|
|
23
|
+
'--output-format', 'stream-json',
|
|
24
|
+
'--verbose',
|
|
25
|
+
'--model', 'claude-opus-4-5-20251101',
|
|
26
|
+
'--dangerously-skip-permissions',
|
|
27
|
+
];
|
|
28
|
+
if (this.config.systemPrompt) {
|
|
29
|
+
args.push('--system-prompt', this.config.systemPrompt);
|
|
30
|
+
}
|
|
31
|
+
if (this.config.disallowedTools && this.config.disallowedTools.length > 0) {
|
|
32
|
+
args.push('--disallowed-tools', this.config.disallowedTools.join(','));
|
|
33
|
+
}
|
|
34
|
+
if (this.sessionId) {
|
|
35
|
+
args.push('--resume', this.sessionId);
|
|
36
|
+
}
|
|
37
|
+
return this.runClaudeStreaming(args, callbacks);
|
|
38
|
+
}
|
|
39
|
+
runClaudeStreaming(args, callbacks) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const proc = spawn('claude', args, {
|
|
42
|
+
cwd: this.config.workingDir || process.cwd(),
|
|
43
|
+
env: process.env,
|
|
44
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
45
|
+
});
|
|
46
|
+
this.currentProc = proc;
|
|
47
|
+
// Close stdin immediately - we pass everything via args
|
|
48
|
+
proc.stdin?.end();
|
|
49
|
+
let buffer = '';
|
|
50
|
+
let lastResult = null;
|
|
51
|
+
let currentTool = null;
|
|
52
|
+
proc.stdout.on('data', (data) => {
|
|
53
|
+
buffer += data.toString();
|
|
54
|
+
const lines = buffer.split('\n');
|
|
55
|
+
buffer = lines.pop() || '';
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
if (!line.trim())
|
|
58
|
+
continue;
|
|
59
|
+
try {
|
|
60
|
+
const event = JSON.parse(line);
|
|
61
|
+
this.processStreamEvent(event, callbacks, (tool) => {
|
|
62
|
+
currentTool = tool;
|
|
63
|
+
}, () => {
|
|
64
|
+
currentTool = null;
|
|
65
|
+
});
|
|
66
|
+
if (event.type === 'result') {
|
|
67
|
+
lastResult = event;
|
|
68
|
+
this.sessionId = event.session_id;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Ignore parse errors
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
proc.stderr.on('data', () => {
|
|
77
|
+
// Ignore stderr
|
|
78
|
+
});
|
|
79
|
+
proc.on('close', () => {
|
|
80
|
+
this.currentProc = null;
|
|
81
|
+
if (buffer.trim()) {
|
|
82
|
+
try {
|
|
83
|
+
const event = JSON.parse(buffer);
|
|
84
|
+
if (event.type === 'result') {
|
|
85
|
+
lastResult = event;
|
|
86
|
+
this.sessionId = event.session_id;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Ignore parse errors
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (currentTool && callbacks?.onToolEnd) {
|
|
94
|
+
callbacks.onToolEnd(currentTool);
|
|
95
|
+
}
|
|
96
|
+
if (lastResult) {
|
|
97
|
+
resolve(lastResult);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
reject(new Error('No result received from Claude'));
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
proc.on('error', reject);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
processStreamEvent(event, callbacks, setCurrentTool, clearCurrentTool) {
|
|
107
|
+
if (event.type === 'assistant' && event.message) {
|
|
108
|
+
const message = event.message;
|
|
109
|
+
if (message.content) {
|
|
110
|
+
for (const block of message.content) {
|
|
111
|
+
if (block.type === 'tool_use' && block.name) {
|
|
112
|
+
setCurrentTool?.(block.name);
|
|
113
|
+
callbacks?.onToolStart?.(block.name, block.input);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (event.type === 'user' && event.message) {
|
|
119
|
+
const message = event.message;
|
|
120
|
+
if (message.content) {
|
|
121
|
+
for (const block of message.content) {
|
|
122
|
+
if (block.type === 'tool_result') {
|
|
123
|
+
clearCurrentTool?.();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (event.type === 'content_block_delta') {
|
|
129
|
+
const delta = event.delta;
|
|
130
|
+
if (delta?.type === 'text_delta' && delta.text) {
|
|
131
|
+
callbacks?.onText?.(delta.text);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
getSessionId() {
|
|
136
|
+
return this.sessionId;
|
|
137
|
+
}
|
|
138
|
+
resetSession() {
|
|
139
|
+
this.sessionId = null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { HeadlessAgent } from './HeadlessAgent.js';
|
|
3
|
+
// Per-lizard sessions
|
|
4
|
+
const sessions = new Map();
|
|
5
|
+
// Summarize tool input for display
|
|
6
|
+
function summarizeInput(input) {
|
|
7
|
+
if (input.file_path)
|
|
8
|
+
return String(input.file_path);
|
|
9
|
+
if (input.pattern)
|
|
10
|
+
return String(input.pattern);
|
|
11
|
+
if (input.command)
|
|
12
|
+
return String(input.command).substring(0, 50);
|
|
13
|
+
if (input.path)
|
|
14
|
+
return String(input.path);
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
const DEFAULT_SYSTEM_PROMPT = `You are a helpful coding assistant. Be concise and direct in your responses.
|
|
18
|
+
|
|
19
|
+
IMPORTANT RESTRICTIONS - You MUST follow these rules:
|
|
20
|
+
1. DO NOT use Bash or shell commands - you cannot run terminal commands
|
|
21
|
+
2. DO NOT try to build, compile, or bundle the project
|
|
22
|
+
3. DO NOT try to start, restart, or run any servers or dev environments
|
|
23
|
+
4. DO NOT run tests, linters, or any CLI tools
|
|
24
|
+
5. DO NOT install packages or run npm/yarn/pnpm commands
|
|
25
|
+
|
|
26
|
+
Your job is ONLY to:
|
|
27
|
+
- Read and understand code using Read, Glob, Grep tools
|
|
28
|
+
- Write and edit code using Write and Edit tools
|
|
29
|
+
- Make the requested code changes
|
|
30
|
+
|
|
31
|
+
After making changes, simply report what you did. The user will handle building, testing, and running the code themselves.`;
|
|
32
|
+
// Tools that agents are not allowed to use
|
|
33
|
+
const DISALLOWED_TOOLS = ['Bash', 'Task'];
|
|
34
|
+
function getOrCreateSession(lizardId, ws) {
|
|
35
|
+
let session = sessions.get(lizardId);
|
|
36
|
+
if (!session) {
|
|
37
|
+
session = {
|
|
38
|
+
agent: new HeadlessAgent({
|
|
39
|
+
systemPrompt: DEFAULT_SYSTEM_PROMPT,
|
|
40
|
+
workingDir: getWorkingDir(),
|
|
41
|
+
disallowedTools: DISALLOWED_TOOLS,
|
|
42
|
+
}),
|
|
43
|
+
isProcessing: false,
|
|
44
|
+
queue: [],
|
|
45
|
+
currentWs: ws ?? null,
|
|
46
|
+
};
|
|
47
|
+
sessions.set(lizardId, session);
|
|
48
|
+
}
|
|
49
|
+
else if (ws) {
|
|
50
|
+
// Update WebSocket reference for existing session
|
|
51
|
+
session.currentWs = ws;
|
|
52
|
+
}
|
|
53
|
+
return session;
|
|
54
|
+
}
|
|
55
|
+
export function isProcessing(lizardId) {
|
|
56
|
+
const session = sessions.get(lizardId);
|
|
57
|
+
return session?.isProcessing ?? false;
|
|
58
|
+
}
|
|
59
|
+
export function getQueueLength(lizardId) {
|
|
60
|
+
const session = sessions.get(lizardId);
|
|
61
|
+
return session?.queue.length ?? 0;
|
|
62
|
+
}
|
|
63
|
+
// Helper to safely send to current WebSocket
|
|
64
|
+
function safeSend(session, data) {
|
|
65
|
+
const ws = session.currentWs;
|
|
66
|
+
if (ws && ws.readyState === ws.OPEN) {
|
|
67
|
+
ws.send(JSON.stringify(data));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export async function sendMessage(lizardId, message, ws) {
|
|
71
|
+
const session = getOrCreateSession(lizardId, ws);
|
|
72
|
+
// Create streaming callbacks that use session's current WebSocket
|
|
73
|
+
const callbacks = {
|
|
74
|
+
onToolStart: (tool, input) => {
|
|
75
|
+
safeSend(session, {
|
|
76
|
+
type: 'tool',
|
|
77
|
+
lizardId,
|
|
78
|
+
status: 'running',
|
|
79
|
+
tool,
|
|
80
|
+
input: input ? summarizeInput(input) : undefined,
|
|
81
|
+
fullInput: input, // Send full input for expandable view
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
onToolEnd: (tool) => {
|
|
85
|
+
safeSend(session, {
|
|
86
|
+
type: 'tool',
|
|
87
|
+
lizardId,
|
|
88
|
+
status: 'completed',
|
|
89
|
+
tool,
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
// If already processing, queue the message
|
|
94
|
+
if (session.isProcessing) {
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
session.queue.push({ message, ws, callbacks, resolve, reject });
|
|
97
|
+
const position = session.queue.length;
|
|
98
|
+
ws.send(JSON.stringify({
|
|
99
|
+
type: 'queued',
|
|
100
|
+
lizardId,
|
|
101
|
+
position,
|
|
102
|
+
}));
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
// Process immediately
|
|
106
|
+
return processMessage(lizardId, session, message, ws, callbacks);
|
|
107
|
+
}
|
|
108
|
+
async function processMessage(lizardId, session, message, _ws, // Kept for queue compatibility, but we use session.currentWs
|
|
109
|
+
callbacks) {
|
|
110
|
+
session.isProcessing = true;
|
|
111
|
+
safeSend(session, { type: 'state', lizardId, state: 'working' });
|
|
112
|
+
try {
|
|
113
|
+
const response = await session.agent.send(message, callbacks);
|
|
114
|
+
safeSend(session, {
|
|
115
|
+
type: 'response',
|
|
116
|
+
lizardId,
|
|
117
|
+
text: response.result,
|
|
118
|
+
sessionId: response.session_id,
|
|
119
|
+
cost: response.total_cost_usd,
|
|
120
|
+
duration: response.duration_ms,
|
|
121
|
+
});
|
|
122
|
+
return response;
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
safeSend(session, {
|
|
126
|
+
type: 'error',
|
|
127
|
+
lizardId,
|
|
128
|
+
message: String(err),
|
|
129
|
+
});
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
session.isProcessing = false;
|
|
134
|
+
safeSend(session, { type: 'state', lizardId, state: 'ready' });
|
|
135
|
+
// Process next queued message if any
|
|
136
|
+
if (session.queue.length > 0) {
|
|
137
|
+
const next = session.queue.shift();
|
|
138
|
+
processMessage(lizardId, session, next.message, next.ws, next.callbacks)
|
|
139
|
+
.then(next.resolve)
|
|
140
|
+
.catch(next.reject);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export function resetSession(lizardId) {
|
|
145
|
+
const session = sessions.get(lizardId);
|
|
146
|
+
if (session) {
|
|
147
|
+
session.agent.resetSession();
|
|
148
|
+
session.queue = [];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
export function deleteSession(lizardId) {
|
|
152
|
+
sessions.delete(lizardId);
|
|
153
|
+
}
|
|
154
|
+
export function getWorkingDir() {
|
|
155
|
+
// In development, use test-app as the working directory
|
|
156
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
157
|
+
return path.resolve(process.cwd(), '../test-app');
|
|
158
|
+
}
|
|
159
|
+
return process.cwd();
|
|
160
|
+
}
|
|
161
|
+
// Update WebSocket for all sessions (called when new client connects)
|
|
162
|
+
export function attachWebSocket(ws) {
|
|
163
|
+
for (const session of sessions.values()) {
|
|
164
|
+
session.currentWs = ws;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
export function getActiveSessions() {
|
|
168
|
+
const result = [];
|
|
169
|
+
for (const [lizardId, session] of sessions) {
|
|
170
|
+
// Determine state
|
|
171
|
+
let state = 'ready';
|
|
172
|
+
let queuePosition = 0;
|
|
173
|
+
if (session.isProcessing) {
|
|
174
|
+
state = 'working';
|
|
175
|
+
}
|
|
176
|
+
else if (session.queue.length > 0) {
|
|
177
|
+
state = 'queued';
|
|
178
|
+
queuePosition = session.queue.length;
|
|
179
|
+
}
|
|
180
|
+
result.push({
|
|
181
|
+
lizardId,
|
|
182
|
+
isProcessing: session.isProcessing,
|
|
183
|
+
isRunning: session.agent.isRunning(),
|
|
184
|
+
queueLength: session.queue.length,
|
|
185
|
+
state,
|
|
186
|
+
queuePosition,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
export function killSession(lizardId) {
|
|
192
|
+
const session = sessions.get(lizardId);
|
|
193
|
+
if (session) {
|
|
194
|
+
const killed = session.agent.kill();
|
|
195
|
+
session.isProcessing = false;
|
|
196
|
+
session.queue = [];
|
|
197
|
+
return killed;
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
export function killAllSessions() {
|
|
202
|
+
let count = 0;
|
|
203
|
+
for (const [, session] of sessions) {
|
|
204
|
+
if (session.agent.kill()) {
|
|
205
|
+
count++;
|
|
206
|
+
}
|
|
207
|
+
session.isProcessing = false;
|
|
208
|
+
session.queue = [];
|
|
209
|
+
}
|
|
210
|
+
return count;
|
|
211
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { WebSocketServer } from 'ws';
|
|
2
|
+
import { sendMessage, resetSession, getWorkingDir, getActiveSessions, killSession, killAllSessions, attachWebSocket } from './agentPool.js';
|
|
3
|
+
import { processWithTools } from './gektoTools.js';
|
|
4
|
+
import { initGekto, sendToGekto, getGektoState, abortGekto, setStateCallback } from './gektoPersistent.js';
|
|
5
|
+
// Track connected clients to broadcast Gekto state
|
|
6
|
+
const connectedClients = new Set();
|
|
7
|
+
let gektoInitialized = false;
|
|
8
|
+
function broadcastGektoState(state) {
|
|
9
|
+
const message = JSON.stringify({ type: 'gekto_state', state });
|
|
10
|
+
for (const client of connectedClients) {
|
|
11
|
+
if (client.readyState === 1) { // WebSocket.OPEN
|
|
12
|
+
client.send(message);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// Summarize tool input for display
|
|
17
|
+
function summarizeToolInput(input) {
|
|
18
|
+
if (!input)
|
|
19
|
+
return '';
|
|
20
|
+
if (input.file_path)
|
|
21
|
+
return String(input.file_path);
|
|
22
|
+
if (input.pattern)
|
|
23
|
+
return String(input.pattern);
|
|
24
|
+
if (input.command)
|
|
25
|
+
return String(input.command).substring(0, 50);
|
|
26
|
+
if (input.path)
|
|
27
|
+
return String(input.path);
|
|
28
|
+
if (input.query)
|
|
29
|
+
return String(input.query).substring(0, 50);
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
// Store active plans
|
|
33
|
+
const activePlans = new Map();
|
|
34
|
+
export function setupAgentWebSocket(server, path = '/__gekto/agent') {
|
|
35
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
36
|
+
server.on('upgrade', (request, socket, head) => {
|
|
37
|
+
const url = request.url || '';
|
|
38
|
+
if (url === path || url.startsWith(path + '?')) {
|
|
39
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
40
|
+
wss.emit('connection', ws, request);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
wss.on('connection', (ws) => {
|
|
45
|
+
// Track connected clients for Gekto state broadcasts
|
|
46
|
+
connectedClients.add(ws);
|
|
47
|
+
// Always ensure Gekto is initialized and callback is set
|
|
48
|
+
setStateCallback(broadcastGektoState);
|
|
49
|
+
if (!gektoInitialized) {
|
|
50
|
+
initGekto(getWorkingDir(), broadcastGektoState);
|
|
51
|
+
gektoInitialized = true;
|
|
52
|
+
}
|
|
53
|
+
// Attach this WebSocket to all existing sessions (for reconnection)
|
|
54
|
+
attachWebSocket(ws);
|
|
55
|
+
// Send working directory info
|
|
56
|
+
ws.send(JSON.stringify({ type: 'info', workingDir: getWorkingDir() }));
|
|
57
|
+
// Send current Gekto state
|
|
58
|
+
ws.send(JSON.stringify({ type: 'gekto_state', state: getGektoState() }));
|
|
59
|
+
// Send current state for all active sessions
|
|
60
|
+
const activeSessions = getActiveSessions();
|
|
61
|
+
for (const session of activeSessions) {
|
|
62
|
+
ws.send(JSON.stringify({
|
|
63
|
+
type: 'state',
|
|
64
|
+
lizardId: session.lizardId,
|
|
65
|
+
state: session.state,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
ws.on('message', async (message) => {
|
|
69
|
+
try {
|
|
70
|
+
const msg = JSON.parse(message.toString());
|
|
71
|
+
// Commands that don't require lizardId
|
|
72
|
+
switch (msg.type) {
|
|
73
|
+
case 'list_agents':
|
|
74
|
+
ws.send(JSON.stringify({
|
|
75
|
+
type: 'agents_list',
|
|
76
|
+
agents: getActiveSessions(),
|
|
77
|
+
}));
|
|
78
|
+
return;
|
|
79
|
+
case 'debug_pool':
|
|
80
|
+
const sessions = getActiveSessions();
|
|
81
|
+
ws.send(JSON.stringify({
|
|
82
|
+
type: 'debug_pool_result',
|
|
83
|
+
sessions,
|
|
84
|
+
}));
|
|
85
|
+
return;
|
|
86
|
+
case 'kill_all':
|
|
87
|
+
const killedCount = killAllSessions();
|
|
88
|
+
ws.send(JSON.stringify({
|
|
89
|
+
type: 'kill_all_result',
|
|
90
|
+
killed: killedCount,
|
|
91
|
+
}));
|
|
92
|
+
// Notify about state changes
|
|
93
|
+
for (const session of getActiveSessions()) {
|
|
94
|
+
ws.send(JSON.stringify({ type: 'state', lizardId: session.lizardId, state: 'ready' }));
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
case 'create_plan':
|
|
98
|
+
// Mode is passed from client (default: 'plan', can toggle to 'direct')
|
|
99
|
+
const mode = msg.mode || 'plan';
|
|
100
|
+
// Set master lizard to working state
|
|
101
|
+
ws.send(JSON.stringify({ type: 'state', lizardId: 'master', state: 'working' }));
|
|
102
|
+
try {
|
|
103
|
+
// Create callbacks for streaming events to client
|
|
104
|
+
const callbacks = {
|
|
105
|
+
onToolStart: (tool, input) => {
|
|
106
|
+
ws.send(JSON.stringify({
|
|
107
|
+
type: 'tool',
|
|
108
|
+
lizardId: 'master',
|
|
109
|
+
status: 'running',
|
|
110
|
+
tool,
|
|
111
|
+
input: summarizeToolInput(input),
|
|
112
|
+
fullInput: input,
|
|
113
|
+
}));
|
|
114
|
+
},
|
|
115
|
+
onToolEnd: (tool) => {
|
|
116
|
+
ws.send(JSON.stringify({
|
|
117
|
+
type: 'tool',
|
|
118
|
+
lizardId: 'master',
|
|
119
|
+
status: 'completed',
|
|
120
|
+
tool,
|
|
121
|
+
}));
|
|
122
|
+
},
|
|
123
|
+
onText: (text) => {
|
|
124
|
+
ws.send(JSON.stringify({
|
|
125
|
+
type: 'gekto_text',
|
|
126
|
+
planId: msg.planId,
|
|
127
|
+
text,
|
|
128
|
+
}));
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
const response = await sendToGekto(msg.prompt, mode, callbacks);
|
|
132
|
+
if (response.mode === 'plan') {
|
|
133
|
+
// Plan mode - use gektoTools for task breakdown
|
|
134
|
+
// Create callbacks for plan tool streaming
|
|
135
|
+
const planCallbacks = {
|
|
136
|
+
onToolStart: (tool, input) => {
|
|
137
|
+
ws.send(JSON.stringify({
|
|
138
|
+
type: 'tool',
|
|
139
|
+
lizardId: 'master',
|
|
140
|
+
status: 'running',
|
|
141
|
+
tool,
|
|
142
|
+
input: summarizeToolInput(input),
|
|
143
|
+
fullInput: input,
|
|
144
|
+
}));
|
|
145
|
+
},
|
|
146
|
+
onToolEnd: (tool) => {
|
|
147
|
+
ws.send(JSON.stringify({
|
|
148
|
+
type: 'tool',
|
|
149
|
+
lizardId: 'master',
|
|
150
|
+
status: 'completed',
|
|
151
|
+
tool,
|
|
152
|
+
}));
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
const planResult = await processWithTools(msg.prompt, msg.planId, getWorkingDir(), getActiveSessions(), planCallbacks);
|
|
156
|
+
if (planResult.type === 'build' && planResult.plan) {
|
|
157
|
+
activePlans.set(msg.planId, planResult.plan);
|
|
158
|
+
ws.send(JSON.stringify({
|
|
159
|
+
type: 'plan_created',
|
|
160
|
+
planId: msg.planId,
|
|
161
|
+
plan: planResult.plan,
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
else if (planResult.type === 'remove' && planResult.removedAgents) {
|
|
165
|
+
ws.send(JSON.stringify({
|
|
166
|
+
type: 'gekto_remove',
|
|
167
|
+
planId: msg.planId,
|
|
168
|
+
agents: planResult.removedAgents,
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
ws.send(JSON.stringify({
|
|
173
|
+
type: 'gekto_chat',
|
|
174
|
+
planId: msg.planId,
|
|
175
|
+
message: planResult.message || 'Plan created.',
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
// Direct mode - response already sent via callbacks
|
|
181
|
+
ws.send(JSON.stringify({
|
|
182
|
+
type: 'gekto_chat',
|
|
183
|
+
planId: msg.planId,
|
|
184
|
+
message: response.message,
|
|
185
|
+
timing: {
|
|
186
|
+
workMs: response.workMs,
|
|
187
|
+
},
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
ws.send(JSON.stringify({ type: 'state', lizardId: 'master', state: 'ready' }));
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
console.error('[Agent] Gekto processing failed:', err);
|
|
194
|
+
ws.send(JSON.stringify({
|
|
195
|
+
type: 'gekto_chat',
|
|
196
|
+
planId: msg.planId,
|
|
197
|
+
message: `Error: ${err instanceof Error ? err.message : 'Processing failed'}`,
|
|
198
|
+
}));
|
|
199
|
+
ws.send(JSON.stringify({ type: 'state', lizardId: 'master', state: 'ready' }));
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
case 'execute_plan':
|
|
203
|
+
// Execute an existing plan
|
|
204
|
+
const plan = activePlans.get(msg.planId);
|
|
205
|
+
if (!plan) {
|
|
206
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Plan not found' }));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// Client handles execution locally (spawns workers, assigns tasks)
|
|
210
|
+
// Don't send plan_updated here - it would overwrite client's assignedLizardId
|
|
211
|
+
plan.status = 'executing';
|
|
212
|
+
return;
|
|
213
|
+
case 'cancel_plan':
|
|
214
|
+
const cancelPlan = activePlans.get(msg.planId);
|
|
215
|
+
if (cancelPlan) {
|
|
216
|
+
cancelPlan.status = 'failed';
|
|
217
|
+
activePlans.delete(msg.planId);
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// Commands that require lizardId
|
|
222
|
+
const lizardId = msg.lizardId;
|
|
223
|
+
if (!lizardId) {
|
|
224
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Missing lizardId' }));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
switch (msg.type) {
|
|
228
|
+
case 'chat':
|
|
229
|
+
try {
|
|
230
|
+
await sendMessage(lizardId, msg.content, ws);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
console.error(`[Agent] [${lizardId}] Error:`, err);
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
case 'reset':
|
|
237
|
+
resetSession(lizardId);
|
|
238
|
+
ws.send(JSON.stringify({ type: 'state', lizardId, state: 'ready' }));
|
|
239
|
+
break;
|
|
240
|
+
case 'kill':
|
|
241
|
+
// For master, abort the persistent Gekto process instead of killing session
|
|
242
|
+
const killed = lizardId === 'master' ? abortGekto() : killSession(lizardId);
|
|
243
|
+
ws.send(JSON.stringify({
|
|
244
|
+
type: 'kill_result',
|
|
245
|
+
lizardId,
|
|
246
|
+
killed,
|
|
247
|
+
}));
|
|
248
|
+
ws.send(JSON.stringify({ type: 'state', lizardId, state: 'ready' }));
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
console.error('[Agent] Failed to parse message:', err);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
ws.on('close', () => {
|
|
257
|
+
connectedClients.delete(ws);
|
|
258
|
+
});
|
|
259
|
+
ws.on('error', (err) => {
|
|
260
|
+
console.error('[Agent] WebSocket error:', err);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
return wss;
|
|
264
|
+
}
|