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,195 @@
1
+ import { spawn } from 'child_process';
2
+ // === Gekto System Prompt ===
3
+ const GEKTO_SYSTEM_PROMPT = `You are Gekto, a friendly task orchestration assistant. You help users with coding tasks by delegating work to specialized agents.
4
+
5
+ IMPORTANT: Analyze each message and decide how to respond:
6
+
7
+ 1. If the message is a GREETING, QUESTION, or CONVERSATION (not a coding task):
8
+ - Respond naturally and friendly
9
+ - Your response will be shown directly to the user
10
+
11
+ 2. If the message is a CODING TASK (feature request, bug fix, refactoring, file changes):
12
+ - Start your response with exactly: [PLAN_MODE]
13
+ - Then provide a JSON execution plan:
14
+ {
15
+ "tasks": [
16
+ {
17
+ "id": "task_1",
18
+ "description": "Brief description",
19
+ "prompt": "Detailed prompt for the worker agent",
20
+ "files": ["path/to/file.ts"],
21
+ "dependencies": []
22
+ }
23
+ ]
24
+ }
25
+
26
+ Examples:
27
+ - "hey" -> "Hey! How can I help you today?"
28
+ - "what's up?" -> "Not much! Ready to help with any coding tasks you have."
29
+ - "add dark mode" -> [PLAN_MODE]{"tasks":[...]}
30
+ - "fix the login bug" -> [PLAN_MODE]{"tasks":[...]}`;
31
+ // Process a message to Gekto - returns either a chat response or an execution plan
32
+ export async function processGektoMessage(prompt, planId, workingDir) {
33
+ console.log('[Orchestrator] Processing message:', prompt.substring(0, 100));
34
+ const result = await runClaudeOnce(prompt, GEKTO_SYSTEM_PROMPT, workingDir);
35
+ console.log('[Orchestrator] Gekto response:', result.substring(0, 200));
36
+ // Check if response indicates plan mode
37
+ if (result.includes('[PLAN_MODE]')) {
38
+ // Extract the JSON plan after [PLAN_MODE]
39
+ const planPart = result.split('[PLAN_MODE]')[1]?.trim() || '';
40
+ return {
41
+ type: 'plan',
42
+ plan: parsePlanFromResponse(planPart, planId, prompt),
43
+ };
44
+ }
45
+ // Regular chat response
46
+ return {
47
+ type: 'chat',
48
+ message: result.trim(),
49
+ };
50
+ }
51
+ // Parse plan JSON from response
52
+ function parsePlanFromResponse(result, planId, originalPrompt) {
53
+ let parsedTasks = [];
54
+ try {
55
+ const jsonMatch = result.match(/\{[\s\S]*\}/);
56
+ if (jsonMatch) {
57
+ const parsed = JSON.parse(jsonMatch[0]);
58
+ if (parsed.tasks && Array.isArray(parsed.tasks)) {
59
+ parsedTasks = parsed.tasks.map((t, i) => ({
60
+ id: t.id || `task_${i + 1}`,
61
+ description: t.description || 'Task',
62
+ prompt: t.prompt || originalPrompt,
63
+ files: t.files || [],
64
+ status: 'pending',
65
+ dependencies: t.dependencies || [],
66
+ }));
67
+ }
68
+ }
69
+ }
70
+ catch (err) {
71
+ console.error('[Orchestrator] Failed to parse plan JSON:', err);
72
+ }
73
+ // Fallback to single task if parsing failed
74
+ if (parsedTasks.length === 0) {
75
+ parsedTasks = [{
76
+ id: 'task_1',
77
+ description: 'Execute task',
78
+ prompt: originalPrompt,
79
+ files: [],
80
+ status: 'pending',
81
+ dependencies: [],
82
+ }];
83
+ }
84
+ return {
85
+ id: planId,
86
+ status: 'ready',
87
+ originalPrompt,
88
+ tasks: parsedTasks,
89
+ createdAt: new Date().toISOString(),
90
+ };
91
+ }
92
+ // Legacy function for backwards compatibility
93
+ export async function generatePlan(prompt, planId, workingDir) {
94
+ const response = await processGektoMessage(prompt, planId, workingDir);
95
+ if (response.type === 'plan') {
96
+ return response.plan;
97
+ }
98
+ // If it returned chat, wrap in a single task (shouldn't happen with proper prompts)
99
+ return {
100
+ id: planId,
101
+ status: 'ready',
102
+ originalPrompt: prompt,
103
+ tasks: [{
104
+ id: 'task_1',
105
+ description: 'Execute task',
106
+ prompt: prompt,
107
+ files: [],
108
+ status: 'pending',
109
+ dependencies: [],
110
+ }],
111
+ createdAt: new Date().toISOString(),
112
+ };
113
+ }
114
+ // === Helper: Run Claude once without session ===
115
+ function runClaudeOnce(prompt, systemPrompt, workingDir) {
116
+ return new Promise((resolve, reject) => {
117
+ const args = [
118
+ '-p', prompt,
119
+ '--output-format', 'stream-json',
120
+ '--verbose',
121
+ '--model', 'claude-haiku-4-5-20251001',
122
+ '--system-prompt', systemPrompt,
123
+ '--dangerously-skip-permissions',
124
+ ];
125
+ console.log('[Orchestrator] Running claude (haiku) for orchestration');
126
+ const proc = spawn('claude', args, {
127
+ cwd: workingDir,
128
+ env: process.env,
129
+ stdio: ['pipe', 'pipe', 'pipe'],
130
+ });
131
+ proc.stdin?.end();
132
+ let buffer = '';
133
+ let resultText = '';
134
+ proc.stdout.on('data', (data) => {
135
+ buffer += data.toString();
136
+ const lines = buffer.split('\n');
137
+ buffer = lines.pop() || '';
138
+ for (const line of lines) {
139
+ if (!line.trim())
140
+ continue;
141
+ try {
142
+ const event = JSON.parse(line);
143
+ if (event.type === 'result' && event.result) {
144
+ resultText = event.result;
145
+ }
146
+ }
147
+ catch {
148
+ // Ignore parse errors
149
+ }
150
+ }
151
+ });
152
+ proc.stderr.on('data', (data) => {
153
+ console.error('[Orchestrator] stderr:', data.toString());
154
+ });
155
+ proc.on('close', (code) => {
156
+ console.log('[Orchestrator] Plan generation complete, code:', code);
157
+ // Try to parse any remaining buffer
158
+ if (buffer.trim()) {
159
+ try {
160
+ const event = JSON.parse(buffer);
161
+ if (event.type === 'result' && event.result) {
162
+ resultText = event.result;
163
+ }
164
+ }
165
+ catch {
166
+ // Ignore
167
+ }
168
+ }
169
+ if (resultText) {
170
+ resolve(resultText);
171
+ }
172
+ else {
173
+ reject(new Error('No result from plan generation'));
174
+ }
175
+ });
176
+ proc.on('error', (err) => {
177
+ console.error('[Orchestrator] Spawn error:', err);
178
+ reject(err);
179
+ });
180
+ });
181
+ }
182
+ // === Delegation Helper ===
183
+ export function shouldDelegateToTask(prompt, tasks) {
184
+ // Simple heuristic: check if prompt mentions any file from a task
185
+ const lowerPrompt = prompt.toLowerCase();
186
+ for (const task of tasks) {
187
+ for (const file of task.files) {
188
+ const fileName = file.split('/').pop()?.toLowerCase() || '';
189
+ if (fileName && lowerPrompt.includes(fileName)) {
190
+ return task;
191
+ }
192
+ }
193
+ }
194
+ return null;
195
+ }
@@ -0,0 +1,239 @@
1
+ import { spawn } from 'child_process';
2
+ import { randomUUID } from 'crypto';
3
+ // === Opus Worker (persistent, for direct mode) ===
4
+ const OPUS_SYSTEM_PROMPT = `You are Gekto, a friendly and capable coding assistant. You help users with their coding tasks directly.
5
+
6
+ Be concise and helpful. When you need to make changes, use the available tools. Explain what you're doing briefly.
7
+
8
+ For greetings and questions, just respond naturally without using tools.
9
+
10
+ IMPORTANT: You can ONLY use Read, Write, Edit, Glob, and Grep tools. You CANNOT use Bash or Task tools - they are disabled.`;
11
+ let opusProcess = null;
12
+ let opusReady = false;
13
+ let opusLoading = false;
14
+ let opusPendingResolve = null;
15
+ let opusBuffer = '';
16
+ let opusCallbacks = null;
17
+ let opusCurrentTool = null;
18
+ // Session ID for persistent history - generated once and shared across all Gekto calls
19
+ // This allows both direct mode (persistent process) and plan mode (one-shot calls) to share history
20
+ let gektoSessionId = randomUUID();
21
+ let workingDir = process.cwd();
22
+ let stateChangeCallback = null;
23
+ // === Initialization ===
24
+ export function initGekto(cwd, onStateChange) {
25
+ workingDir = cwd;
26
+ stateChangeCallback = onStateChange || null;
27
+ // Start Opus process
28
+ spawnOpus();
29
+ }
30
+ export function getGektoState() {
31
+ if (opusLoading)
32
+ return 'loading';
33
+ if (opusReady)
34
+ return 'ready';
35
+ return 'loading';
36
+ }
37
+ // === Opus Process ===
38
+ function spawnOpus() {
39
+ if (opusProcess)
40
+ return;
41
+ opusLoading = true;
42
+ opusReady = false;
43
+ updateState();
44
+ const args = [
45
+ '--input-format', 'stream-json',
46
+ '--output-format', 'stream-json',
47
+ '--verbose',
48
+ '--model', 'claude-opus-4-5-20251101',
49
+ '--system-prompt', OPUS_SYSTEM_PROMPT,
50
+ '--dangerously-skip-permissions',
51
+ '--disallowed-tools', 'Bash', 'Task',
52
+ '--session-id', gektoSessionId,
53
+ ];
54
+ opusProcess = spawn('claude', args, {
55
+ cwd: workingDir,
56
+ env: process.env,
57
+ stdio: ['pipe', 'pipe', 'pipe'],
58
+ });
59
+ opusProcess.stdout.on('data', (data) => {
60
+ opusBuffer += data.toString();
61
+ const lines = opusBuffer.split('\n');
62
+ opusBuffer = lines.pop() || '';
63
+ for (const line of lines) {
64
+ if (!line.trim())
65
+ continue;
66
+ try {
67
+ const event = JSON.parse(line);
68
+ handleOpusEvent(event);
69
+ }
70
+ catch {
71
+ // Ignore non-JSON lines
72
+ }
73
+ }
74
+ });
75
+ opusProcess.stderr.on('data', () => {
76
+ // Ignore stderr
77
+ });
78
+ opusProcess.on('close', () => {
79
+ opusProcess = null;
80
+ opusReady = false;
81
+ opusLoading = true;
82
+ updateState();
83
+ if (opusPendingResolve) {
84
+ opusPendingResolve('Process restarting, please try again.');
85
+ opusPendingResolve = null;
86
+ }
87
+ // Auto-restart
88
+ setTimeout(spawnOpus, 1000);
89
+ });
90
+ opusProcess.on('error', () => {
91
+ opusProcess = null;
92
+ opusReady = false;
93
+ opusLoading = true;
94
+ updateState();
95
+ });
96
+ // Warm up: send a quick message to trigger ready state
97
+ setTimeout(() => {
98
+ if (opusProcess && !opusReady) {
99
+ const warmup = { type: 'user', message: { role: 'user', content: 'hi' } };
100
+ opusProcess.stdin.write(JSON.stringify(warmup) + '\n');
101
+ }
102
+ }, 500);
103
+ }
104
+ function handleOpusEvent(event) {
105
+ // Result event means process is working
106
+ if (event.type === 'result') {
107
+ if (!opusReady) {
108
+ opusReady = true;
109
+ opusLoading = false;
110
+ updateState();
111
+ }
112
+ }
113
+ // Tool use detection from assistant message
114
+ if (event.type === 'assistant' && event.message) {
115
+ const message = event.message;
116
+ if (message.content) {
117
+ for (const block of message.content) {
118
+ if (block.type === 'tool_use' && block.name) {
119
+ opusCurrentTool = block.name;
120
+ opusCallbacks?.onToolStart?.(block.name, block.input);
121
+ }
122
+ }
123
+ }
124
+ }
125
+ // Tool result (tool completed)
126
+ if (event.type === 'user' && event.message) {
127
+ const message = event.message;
128
+ if (message.content) {
129
+ for (const block of message.content) {
130
+ if (block.type === 'tool_result' && opusCurrentTool) {
131
+ opusCallbacks?.onToolEnd?.(opusCurrentTool);
132
+ opusCurrentTool = null;
133
+ }
134
+ }
135
+ }
136
+ }
137
+ // Text streaming
138
+ if (event.type === 'content_block_delta') {
139
+ const delta = event.delta;
140
+ if (delta?.type === 'text_delta' && delta.text) {
141
+ opusCallbacks?.onText?.(delta.text);
142
+ }
143
+ }
144
+ // Final result
145
+ if (event.type === 'result' && event.result) {
146
+ if (opusPendingResolve) {
147
+ opusPendingResolve(event.result);
148
+ opusPendingResolve = null;
149
+ }
150
+ }
151
+ }
152
+ async function sendToOpus(prompt, callbacks) {
153
+ if (!opusProcess || !opusReady) {
154
+ spawnOpus();
155
+ await new Promise(resolve => setTimeout(resolve, 1000));
156
+ }
157
+ if (!opusProcess) {
158
+ return 'Gekto is starting up, please try again.';
159
+ }
160
+ opusCallbacks = callbacks;
161
+ opusCurrentTool = null;
162
+ return new Promise((resolve) => {
163
+ opusPendingResolve = (result) => {
164
+ opusCallbacks = null;
165
+ resolve(result);
166
+ };
167
+ const inputMessage = {
168
+ type: 'user',
169
+ message: { role: 'user', content: prompt },
170
+ };
171
+ opusProcess.stdin.write(JSON.stringify(inputMessage) + '\n');
172
+ // Timeout after 5 min for complex tasks
173
+ setTimeout(() => {
174
+ if (opusPendingResolve) {
175
+ opusPendingResolve('Task timed out. Please try breaking it into smaller steps.');
176
+ opusPendingResolve = null;
177
+ }
178
+ }, 300000);
179
+ });
180
+ }
181
+ // Mode is now passed as parameter - default is 'plan', UI can toggle to 'direct'
182
+ export async function sendToGekto(prompt, mode = 'plan', callbacks) {
183
+ const startTime = Date.now();
184
+ // Plan mode - return immediately, caller will use gektoTools.ts for planning
185
+ if (mode === 'plan') {
186
+ return {
187
+ mode: 'plan',
188
+ message: 'Creating plan...',
189
+ };
190
+ }
191
+ // Direct mode - use Opus
192
+ const result = await sendToOpus(prompt, callbacks || {});
193
+ const workMs = Date.now() - startTime;
194
+ callbacks?.onResult?.(result);
195
+ return {
196
+ mode: 'direct',
197
+ message: result,
198
+ workMs,
199
+ };
200
+ }
201
+ // === State Management ===
202
+ function updateState() {
203
+ const state = getGektoState();
204
+ stateChangeCallback?.(state);
205
+ }
206
+ export function isGektoReady() {
207
+ return opusReady;
208
+ }
209
+ // Set/update state change callback (for reconnections)
210
+ export function setStateCallback(callback) {
211
+ stateChangeCallback = callback;
212
+ }
213
+ // Get current session ID (shared between direct and plan modes)
214
+ export function getGektoSessionId() {
215
+ return gektoSessionId;
216
+ }
217
+ // Reset session to start fresh (clears history)
218
+ export function resetGektoSession() {
219
+ gektoSessionId = randomUUID();
220
+ // Kill current process to force restart with new session
221
+ if (opusProcess) {
222
+ opusProcess.kill('SIGTERM');
223
+ }
224
+ }
225
+ // === Abort current task (like pressing ESC in CLI) ===
226
+ export function abortGekto() {
227
+ let aborted = false;
228
+ // Send SIGINT to interrupt current Opus task (like Ctrl+C / ESC)
229
+ if (opusProcess && opusPendingResolve) {
230
+ opusProcess.kill('SIGINT');
231
+ // Resolve the pending promise so caller doesn't hang
232
+ opusPendingResolve('Task was stopped.');
233
+ opusPendingResolve = null;
234
+ opusCallbacks = null;
235
+ opusCurrentTool = null;
236
+ aborted = true;
237
+ }
238
+ return aborted;
239
+ }
@@ -0,0 +1,137 @@
1
+ import { spawn } from 'child_process';
2
+ // Simple Gekto - just chat with Haiku and return text response
3
+ const GEKTO_SYSTEM_PROMPT = `You are Gekto, a friendly coding assistant. Decide what to do with the user's message.
4
+
5
+ Options:
6
+ 1. answer - For greetings, questions, conversation. Just reply naturally.
7
+ 2. write_code - For coding tasks that need implementation.
8
+ 3. plan - For complex tasks that need breaking into steps.
9
+
10
+ Respond with JSON: { "action": "answer|write_code|plan", "response": "your message" }
11
+
12
+ Examples:
13
+ - "yo" -> { "action": "answer", "response": "Hey! What can I help you with?" }
14
+ - "add dark mode" -> { "action": "write_code", "response": "I'll spawn an agent to add dark mode." }
15
+ - "refactor the auth system" -> { "action": "plan", "response": "Let me break this down into steps..." }
16
+
17
+ Always respond with valid JSON.`;
18
+ export async function processSimple(prompt, workingDir) {
19
+ const startTime = Date.now();
20
+ try {
21
+ const rawResponse = await runHaiku(prompt, GEKTO_SYSTEM_PROMPT, workingDir);
22
+ const durationMs = Date.now() - startTime;
23
+ // Parse JSON response
24
+ const jsonMatch = rawResponse.match(/\{[\s\S]*\}/);
25
+ if (jsonMatch) {
26
+ const parsed = JSON.parse(jsonMatch[0]);
27
+ return {
28
+ type: 'chat',
29
+ action: parsed.action || 'answer',
30
+ message: parsed.response || rawResponse,
31
+ durationMs,
32
+ };
33
+ }
34
+ // Fallback: use raw response as message
35
+ return {
36
+ type: 'chat',
37
+ action: 'answer',
38
+ message: rawResponse,
39
+ durationMs,
40
+ };
41
+ }
42
+ catch {
43
+ const durationMs = Date.now() - startTime;
44
+ // Always return a friendly message, never an error
45
+ return {
46
+ type: 'chat',
47
+ action: 'answer',
48
+ message: "Hey! Something went wrong on my end. Could you try again?",
49
+ durationMs,
50
+ };
51
+ }
52
+ }
53
+ function runHaiku(prompt, systemPrompt, workingDir) {
54
+ return new Promise((resolve) => {
55
+ const args = [
56
+ '-p', prompt,
57
+ '--output-format', 'stream-json',
58
+ '--model', 'claude-haiku-4-5-20251001',
59
+ '--system-prompt', systemPrompt,
60
+ '--dangerously-skip-permissions',
61
+ ];
62
+ const proc = spawn('claude', args, {
63
+ cwd: workingDir,
64
+ env: process.env,
65
+ stdio: ['pipe', 'pipe', 'pipe'],
66
+ });
67
+ proc.stdin?.end();
68
+ let buffer = '';
69
+ let resultText = '';
70
+ let allOutput = '';
71
+ proc.stdout.on('data', (data) => {
72
+ const chunk = data.toString();
73
+ buffer += chunk;
74
+ allOutput += chunk;
75
+ const lines = buffer.split('\n');
76
+ buffer = lines.pop() || '';
77
+ for (const line of lines) {
78
+ if (!line.trim())
79
+ continue;
80
+ try {
81
+ const event = JSON.parse(line);
82
+ // Capture any text content
83
+ if (event.type === 'result' && event.result) {
84
+ resultText = event.result;
85
+ }
86
+ else if (event.type === 'assistant' && event.message?.content) {
87
+ // Also check for assistant message format
88
+ for (const block of event.message.content) {
89
+ if (block.type === 'text') {
90
+ resultText = block.text;
91
+ }
92
+ }
93
+ }
94
+ else if (event.type === 'content_block_delta' && event.delta?.text) {
95
+ resultText += event.delta.text;
96
+ }
97
+ }
98
+ catch {
99
+ // Ignore parse errors
100
+ }
101
+ }
102
+ });
103
+ proc.stderr.on('data', (data) => {
104
+ console.error('[GektoSimple] stderr:', data.toString());
105
+ });
106
+ proc.on('close', (code) => {
107
+ console.log('[GektoSimple] Exit code:', code);
108
+ // Try to parse remaining buffer
109
+ if (buffer.trim()) {
110
+ try {
111
+ const event = JSON.parse(buffer);
112
+ if (event.type === 'result' && event.result) {
113
+ resultText = event.result;
114
+ }
115
+ }
116
+ catch {
117
+ // Ignore
118
+ }
119
+ }
120
+ // Always resolve with something
121
+ if (resultText) {
122
+ console.log('[GektoSimple] Result:', resultText.substring(0, 100));
123
+ resolve(resultText);
124
+ }
125
+ else {
126
+ // Log all output for debugging
127
+ console.log('[GektoSimple] No result found. All output:', allOutput.substring(0, 500));
128
+ resolve('{ "action": "answer", "response": "Hey there! How can I help?" }');
129
+ }
130
+ });
131
+ proc.on('error', (err) => {
132
+ console.error('[GektoSimple] Spawn error:', err);
133
+ // Still resolve with a default response
134
+ resolve('{ "action": "answer", "response": "Hey! How can I help you today?" }');
135
+ });
136
+ });
137
+ }