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.
@@ -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
+ }