gekto 0.0.7 → 0.0.8
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/HeadlessAgent.js +49 -6
- package/dist/agents/agentPool.js +93 -1
- package/dist/agents/agentWebSocket.js +90 -3
- package/dist/agents/bashSafetyRules.js +30 -0
- package/dist/agents/gektoPersistent.js +19 -4
- package/dist/agents/gektoSimple.js +7 -2
- package/dist/agents/gektoTools.js +243 -74
- package/dist/proxy.js +120 -2
- package/dist/widget/gekto-widget.css +1 -0
- package/dist/widget/gekto-widget.iife.js +713 -199
- package/package.json +1 -1
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
3
|
import { CLAUDE_PATH } from '../claudePath.js';
|
|
3
4
|
export class HeadlessAgent {
|
|
4
5
|
sessionId = null;
|
|
5
6
|
config;
|
|
6
7
|
currentProc = null;
|
|
8
|
+
pendingFileChanges = new Map();
|
|
7
9
|
constructor(config = {}) {
|
|
8
10
|
this.config = config;
|
|
9
11
|
}
|
|
12
|
+
// Read file content safely, returns null if file doesn't exist
|
|
13
|
+
readFileSafe(filePath) {
|
|
14
|
+
try {
|
|
15
|
+
// Resolve path relative to working directory
|
|
16
|
+
const fullPath = filePath.startsWith('/')
|
|
17
|
+
? filePath
|
|
18
|
+
: `${this.config.workingDir || process.cwd()}/${filePath}`;
|
|
19
|
+
if (!existsSync(fullPath))
|
|
20
|
+
return null;
|
|
21
|
+
return readFileSync(fullPath, 'utf-8');
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
10
27
|
kill() {
|
|
11
28
|
if (this.currentProc && !this.currentProc.killed) {
|
|
12
29
|
this.currentProc.kill('SIGTERM');
|
|
@@ -116,6 +133,20 @@ export class HeadlessAgent {
|
|
|
116
133
|
if (block.type === 'tool_use' && block.name) {
|
|
117
134
|
setCurrentTool?.(block.name);
|
|
118
135
|
callbacks?.onToolStart?.(block.name, block.input);
|
|
136
|
+
// Track file changes for Write/Edit tools
|
|
137
|
+
if ((block.name === 'Write' || block.name === 'Edit') && block.id && block.input?.file_path) {
|
|
138
|
+
const filePath = String(block.input.file_path);
|
|
139
|
+
const before = this.readFileSafe(filePath);
|
|
140
|
+
this.pendingFileChanges.set(block.id, {
|
|
141
|
+
tool: block.name,
|
|
142
|
+
filePath,
|
|
143
|
+
before,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Extract text content from assistant messages
|
|
148
|
+
if (block.type === 'text' && block.text) {
|
|
149
|
+
callbacks?.onText?.(block.text);
|
|
119
150
|
}
|
|
120
151
|
}
|
|
121
152
|
}
|
|
@@ -126,20 +157,32 @@ export class HeadlessAgent {
|
|
|
126
157
|
for (const block of message.content) {
|
|
127
158
|
if (block.type === 'tool_result') {
|
|
128
159
|
clearCurrentTool?.();
|
|
160
|
+
// Complete file change tracking
|
|
161
|
+
if (block.tool_use_id && this.pendingFileChanges.has(block.tool_use_id)) {
|
|
162
|
+
const pending = this.pendingFileChanges.get(block.tool_use_id);
|
|
163
|
+
this.pendingFileChanges.delete(block.tool_use_id);
|
|
164
|
+
const after = this.readFileSafe(pending.filePath);
|
|
165
|
+
// Only emit if file actually changed (or was created)
|
|
166
|
+
if (after !== null && after !== pending.before) {
|
|
167
|
+
callbacks?.onFileChange?.({
|
|
168
|
+
tool: pending.tool,
|
|
169
|
+
filePath: pending.filePath,
|
|
170
|
+
before: pending.before,
|
|
171
|
+
after,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
129
175
|
}
|
|
130
176
|
}
|
|
131
177
|
}
|
|
132
178
|
}
|
|
133
|
-
if (event.type === 'content_block_delta') {
|
|
134
|
-
const delta = event.delta;
|
|
135
|
-
if (delta?.type === 'text_delta' && delta.text) {
|
|
136
|
-
callbacks?.onText?.(delta.text);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
179
|
}
|
|
140
180
|
getSessionId() {
|
|
141
181
|
return this.sessionId;
|
|
142
182
|
}
|
|
183
|
+
setSessionId(id) {
|
|
184
|
+
this.sessionId = id;
|
|
185
|
+
}
|
|
143
186
|
resetSession() {
|
|
144
187
|
this.sessionId = null;
|
|
145
188
|
}
|
package/dist/agents/agentPool.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
+
import { writeFileSync, unlinkSync, existsSync, mkdirSync } from 'fs';
|
|
3
|
+
import { dirname } from 'path';
|
|
2
4
|
import { HeadlessAgent } from './HeadlessAgent.js';
|
|
3
5
|
// Per-lizard sessions
|
|
4
6
|
const sessions = new Map();
|
|
@@ -28,7 +30,16 @@ Your job is ONLY to:
|
|
|
28
30
|
- Write and edit code using Write and Edit tools
|
|
29
31
|
- Make the requested code changes
|
|
30
32
|
|
|
31
|
-
After making changes, simply report what you did. The user will handle building, testing, and running the code themselves
|
|
33
|
+
After making changes, simply report what you did. The user will handle building, testing, and running the code themselves.
|
|
34
|
+
|
|
35
|
+
STATUS MARKER - At the END of EVERY response, you MUST include exactly one of these markers:
|
|
36
|
+
- [STATUS:DONE] - Use when the task is complete and you have no questions for the user
|
|
37
|
+
- [STATUS:PENDING] - Use when you need user input, confirmation, clarification, or approval to proceed
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
- After completing a code change: "I've updated the function. [STATUS:DONE]"
|
|
41
|
+
- When asking a question: "Which approach would you prefer? [STATUS:PENDING]"
|
|
42
|
+
- After answering a simple question: "The file is located at src/utils.ts [STATUS:DONE]"`;
|
|
32
43
|
// Tools that agents are not allowed to use
|
|
33
44
|
const DISALLOWED_TOOLS = ['Bash', 'Task'];
|
|
34
45
|
function getOrCreateSession(lizardId, ws) {
|
|
@@ -52,6 +63,31 @@ function getOrCreateSession(lizardId, ws) {
|
|
|
52
63
|
}
|
|
53
64
|
return session;
|
|
54
65
|
}
|
|
66
|
+
export function resumeSession(lizardId, sessionId, ws) {
|
|
67
|
+
let session = sessions.get(lizardId);
|
|
68
|
+
if (!session) {
|
|
69
|
+
const agent = new HeadlessAgent({
|
|
70
|
+
systemPrompt: DEFAULT_SYSTEM_PROMPT,
|
|
71
|
+
workingDir: getWorkingDir(),
|
|
72
|
+
disallowedTools: DISALLOWED_TOOLS,
|
|
73
|
+
});
|
|
74
|
+
// Restore Claude Code session ID so --resume works
|
|
75
|
+
if (sessionId) {
|
|
76
|
+
agent.setSessionId(sessionId);
|
|
77
|
+
}
|
|
78
|
+
session = {
|
|
79
|
+
agent,
|
|
80
|
+
isProcessing: false,
|
|
81
|
+
queue: [],
|
|
82
|
+
currentWs: ws ?? null,
|
|
83
|
+
};
|
|
84
|
+
sessions.set(lizardId, session);
|
|
85
|
+
}
|
|
86
|
+
else if (ws) {
|
|
87
|
+
session.currentWs = ws;
|
|
88
|
+
}
|
|
89
|
+
return session;
|
|
90
|
+
}
|
|
55
91
|
export function isProcessing(lizardId) {
|
|
56
92
|
const session = sessions.get(lizardId);
|
|
57
93
|
return session?.isProcessing ?? false;
|
|
@@ -89,6 +125,20 @@ export async function sendMessage(lizardId, message, ws) {
|
|
|
89
125
|
tool,
|
|
90
126
|
});
|
|
91
127
|
},
|
|
128
|
+
onText: (text) => {
|
|
129
|
+
safeSend(session, {
|
|
130
|
+
type: 'text',
|
|
131
|
+
lizardId,
|
|
132
|
+
text,
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
onFileChange: (change) => {
|
|
136
|
+
safeSend(session, {
|
|
137
|
+
type: 'file_change',
|
|
138
|
+
lizardId,
|
|
139
|
+
change,
|
|
140
|
+
});
|
|
141
|
+
},
|
|
92
142
|
};
|
|
93
143
|
// If already processing, queue the message
|
|
94
144
|
if (session.isProcessing) {
|
|
@@ -194,10 +244,50 @@ export function killSession(lizardId) {
|
|
|
194
244
|
const killed = session.agent.kill();
|
|
195
245
|
session.isProcessing = false;
|
|
196
246
|
session.queue = [];
|
|
247
|
+
// Remove from map so getActiveSessions() doesn't return dead agents
|
|
248
|
+
sessions.delete(lizardId);
|
|
197
249
|
return killed;
|
|
198
250
|
}
|
|
199
251
|
return false;
|
|
200
252
|
}
|
|
253
|
+
// Revert files to their pre-agent state using the before content from FileChange objects
|
|
254
|
+
export function revertFiles(filePaths, fileChanges) {
|
|
255
|
+
const reverted = [];
|
|
256
|
+
const failed = [];
|
|
257
|
+
const workingDir = getWorkingDir();
|
|
258
|
+
for (const filePath of filePaths) {
|
|
259
|
+
const change = fileChanges.find(fc => fc.filePath === filePath);
|
|
260
|
+
if (!change) {
|
|
261
|
+
failed.push(filePath);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
const fullPath = filePath.startsWith('/')
|
|
266
|
+
? filePath
|
|
267
|
+
: path.resolve(workingDir, filePath);
|
|
268
|
+
if (change.before === null) {
|
|
269
|
+
// File was newly created by agent — delete it
|
|
270
|
+
if (existsSync(fullPath)) {
|
|
271
|
+
unlinkSync(fullPath);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
// File existed before — restore original content
|
|
276
|
+
const dir = dirname(fullPath);
|
|
277
|
+
if (!existsSync(dir)) {
|
|
278
|
+
mkdirSync(dir, { recursive: true });
|
|
279
|
+
}
|
|
280
|
+
writeFileSync(fullPath, change.before, 'utf-8');
|
|
281
|
+
}
|
|
282
|
+
reverted.push(filePath);
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
console.error(`[AgentPool] Failed to revert ${filePath}:`, err);
|
|
286
|
+
failed.push(filePath);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return { reverted, failed };
|
|
290
|
+
}
|
|
201
291
|
export function killAllSessions() {
|
|
202
292
|
let count = 0;
|
|
203
293
|
for (const [, session] of sessions) {
|
|
@@ -207,5 +297,7 @@ export function killAllSessions() {
|
|
|
207
297
|
session.isProcessing = false;
|
|
208
298
|
session.queue = [];
|
|
209
299
|
}
|
|
300
|
+
// Clear all sessions so getActiveSessions() returns empty
|
|
301
|
+
sessions.clear();
|
|
210
302
|
return count;
|
|
211
303
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { WebSocketServer } from 'ws';
|
|
2
|
-
import { sendMessage, resetSession, getWorkingDir, getActiveSessions, killSession, killAllSessions, attachWebSocket } from './agentPool.js';
|
|
3
|
-
import { processWithTools } from './gektoTools.js';
|
|
2
|
+
import { sendMessage, resumeSession, resetSession, getWorkingDir, getActiveSessions, killSession, killAllSessions, attachWebSocket, revertFiles } from './agentPool.js';
|
|
3
|
+
import { processWithTools, generateTaskPrompts } from './gektoTools.js';
|
|
4
4
|
import { initGekto, sendToGekto, getGektoState, abortGekto, setStateCallback } from './gektoPersistent.js';
|
|
5
5
|
// Track connected clients to broadcast Gekto state
|
|
6
6
|
const connectedClients = new Set();
|
|
@@ -151,8 +151,15 @@ export function setupAgentWebSocket(server, path = '/__gekto/agent') {
|
|
|
151
151
|
tool,
|
|
152
152
|
}));
|
|
153
153
|
},
|
|
154
|
+
onText: (text) => {
|
|
155
|
+
ws.send(JSON.stringify({
|
|
156
|
+
type: 'gekto_text',
|
|
157
|
+
planId: msg.planId,
|
|
158
|
+
text,
|
|
159
|
+
}));
|
|
160
|
+
},
|
|
154
161
|
};
|
|
155
|
-
const planResult = await processWithTools(msg.prompt, msg.planId, getWorkingDir(), getActiveSessions(), planCallbacks);
|
|
162
|
+
const planResult = await processWithTools(msg.prompt, msg.planId, getWorkingDir(), getActiveSessions(), planCallbacks, msg.existingPlan);
|
|
156
163
|
if (planResult.type === 'build' && planResult.plan) {
|
|
157
164
|
activePlans.set(msg.planId, planResult.plan);
|
|
158
165
|
ws.send(JSON.stringify({
|
|
@@ -199,6 +206,59 @@ export function setupAgentWebSocket(server, path = '/__gekto/agent') {
|
|
|
199
206
|
ws.send(JSON.stringify({ type: 'state', lizardId: 'master', state: 'ready' }));
|
|
200
207
|
}
|
|
201
208
|
return;
|
|
209
|
+
case 'generate_prompts': {
|
|
210
|
+
const genPlan = activePlans.get(msg.planId);
|
|
211
|
+
if (!genPlan) {
|
|
212
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Plan not found' }));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// Set master to working while generating
|
|
216
|
+
ws.send(JSON.stringify({ type: 'state', lizardId: 'master', state: 'working' }));
|
|
217
|
+
try {
|
|
218
|
+
const genCallbacks = {
|
|
219
|
+
onTaskPromptGenerated: (taskId, prompt) => {
|
|
220
|
+
// Update server-side plan
|
|
221
|
+
const task = genPlan.tasks.find(t => t.id === taskId);
|
|
222
|
+
if (task)
|
|
223
|
+
task.prompt = prompt;
|
|
224
|
+
// Notify client
|
|
225
|
+
ws.send(JSON.stringify({
|
|
226
|
+
type: 'prompt_generated',
|
|
227
|
+
planId: msg.planId,
|
|
228
|
+
taskId,
|
|
229
|
+
prompt,
|
|
230
|
+
}));
|
|
231
|
+
},
|
|
232
|
+
onAllPromptsReady: () => {
|
|
233
|
+
genPlan.status = 'prompts_ready';
|
|
234
|
+
ws.send(JSON.stringify({
|
|
235
|
+
type: 'prompts_ready',
|
|
236
|
+
planId: msg.planId,
|
|
237
|
+
}));
|
|
238
|
+
},
|
|
239
|
+
onError: (taskId, error) => {
|
|
240
|
+
ws.send(JSON.stringify({
|
|
241
|
+
type: 'prompt_generated',
|
|
242
|
+
planId: msg.planId,
|
|
243
|
+
taskId,
|
|
244
|
+
prompt: genPlan.tasks.find(t => t.id === taskId)?.description || 'Execute task',
|
|
245
|
+
error,
|
|
246
|
+
}));
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
await generateTaskPrompts(genPlan, getWorkingDir(), genCallbacks);
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
console.error('[Agent] Prompt generation failed:', err);
|
|
253
|
+
ws.send(JSON.stringify({
|
|
254
|
+
type: 'gekto_chat',
|
|
255
|
+
planId: msg.planId,
|
|
256
|
+
message: `Error generating prompts: ${err instanceof Error ? err.message : 'Failed'}`,
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
ws.send(JSON.stringify({ type: 'state', lizardId: 'master', state: 'ready' }));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
202
262
|
case 'execute_plan':
|
|
203
263
|
// Execute an existing plan
|
|
204
264
|
const plan = activePlans.get(msg.planId);
|
|
@@ -217,6 +277,23 @@ export function setupAgentWebSocket(server, path = '/__gekto/agent') {
|
|
|
217
277
|
activePlans.delete(msg.planId);
|
|
218
278
|
}
|
|
219
279
|
return;
|
|
280
|
+
case 'resume_agent': {
|
|
281
|
+
const { lizardId: resumeId, sessionId: resumeSessionId, prompt: resumePrompt } = msg;
|
|
282
|
+
if (!resumeId) {
|
|
283
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Missing lizardId for resume' }));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
// Create session with restored sessionId
|
|
287
|
+
resumeSession(resumeId, resumeSessionId, ws);
|
|
288
|
+
// Send the original prompt to resume work
|
|
289
|
+
try {
|
|
290
|
+
await sendMessage(resumeId, resumePrompt || 'Continue where you left off.', ws);
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
console.error(`[Agent] Resume failed for ${resumeId}:`, err);
|
|
294
|
+
}
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
220
297
|
}
|
|
221
298
|
// Commands that require lizardId
|
|
222
299
|
const lizardId = msg.lizardId;
|
|
@@ -237,6 +314,16 @@ export function setupAgentWebSocket(server, path = '/__gekto/agent') {
|
|
|
237
314
|
resetSession(lizardId);
|
|
238
315
|
ws.send(JSON.stringify({ type: 'state', lizardId, state: 'ready' }));
|
|
239
316
|
break;
|
|
317
|
+
case 'revert_files': {
|
|
318
|
+
const revertResult = revertFiles(msg.filePaths || [], msg.fileChanges || []);
|
|
319
|
+
ws.send(JSON.stringify({
|
|
320
|
+
type: 'files_reverted',
|
|
321
|
+
lizardId,
|
|
322
|
+
reverted: revertResult.reverted,
|
|
323
|
+
failed: revertResult.failed,
|
|
324
|
+
}));
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
240
327
|
case 'kill':
|
|
241
328
|
// For master, abort the persistent Gekto process instead of killing session
|
|
242
329
|
const killed = lizardId === 'master' ? abortGekto() : killSession(lizardId);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Shared Bash safety rules for all agents (Gekto + workers)
|
|
2
|
+
// Mirrors the restrictions in CLAUDE.md and .claude/settings.json
|
|
3
|
+
export const BASH_SAFETY_RULES = `
|
|
4
|
+
BASH SAFETY RULES — You MUST follow these when using Bash:
|
|
5
|
+
|
|
6
|
+
ALLOWED commands:
|
|
7
|
+
- git (status, add, commit, diff, log, branch, checkout, stash)
|
|
8
|
+
- npm/yarn/pnpm/bun (run, test, install, build)
|
|
9
|
+
- npx, node, tsc, python, pip install
|
|
10
|
+
- cat, ls, find, grep, echo, mkdir, cp, mv, head, tail, wc, sort, diff, pwd, which
|
|
11
|
+
|
|
12
|
+
STRICTLY FORBIDDEN — NEVER run these:
|
|
13
|
+
- rm -rf (any path outside the current project)
|
|
14
|
+
- sudo (any command)
|
|
15
|
+
- chmod 777, chown, mkfs, dd
|
|
16
|
+
- curl|bash, curl|sh, wget|bash, wget|sh (piped execution)
|
|
17
|
+
- eval with untrusted input
|
|
18
|
+
- shutdown, reboot, kill -9 1, killall, systemctl, service
|
|
19
|
+
- Any command that modifies system files (/etc, /usr, /var, /bin, /sbin, /boot)
|
|
20
|
+
- Any command that reads /etc/shadow, /etc/passwd
|
|
21
|
+
- Any command that writes to /root, ~/.ssh, ~/.bashrc, ~/.zshrc
|
|
22
|
+
- Fork bombs or disk-wiping commands
|
|
23
|
+
- Reading or writing .env files (they contain secrets)
|
|
24
|
+
|
|
25
|
+
GENERAL RULES:
|
|
26
|
+
1. Only run commands within the current project directory
|
|
27
|
+
2. Never run destructive commands without confirming the paths are project-local
|
|
28
|
+
3. Prefer dedicated tools (Read, Write, Edit, Glob, Grep) over bash equivalents when available
|
|
29
|
+
4. If a task requires operations outside the project, STOP and report it
|
|
30
|
+
`;
|
|
@@ -46,7 +46,7 @@ function spawnOpus() {
|
|
|
46
46
|
'--input-format', 'stream-json',
|
|
47
47
|
'--output-format', 'stream-json',
|
|
48
48
|
'--verbose',
|
|
49
|
-
'--model', 'claude-
|
|
49
|
+
'--model', 'claude-sonnet-4-6',
|
|
50
50
|
'--system-prompt', OPUS_SYSTEM_PROMPT,
|
|
51
51
|
'--dangerously-skip-permissions',
|
|
52
52
|
'--disallowed-tools', 'Bash', 'Task',
|
|
@@ -58,6 +58,8 @@ function spawnOpus() {
|
|
|
58
58
|
env: process.env,
|
|
59
59
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
60
60
|
});
|
|
61
|
+
// Prevent EPIPE crash when writing to stdin of a dead process
|
|
62
|
+
opusProcess.stdin.on('error', () => { });
|
|
61
63
|
opusProcess.on('error', (err) => {
|
|
62
64
|
console.error(`[GektoPersistent] Spawn error:`, err);
|
|
63
65
|
});
|
|
@@ -100,7 +102,7 @@ function spawnOpus() {
|
|
|
100
102
|
});
|
|
101
103
|
// Warm up: send a quick message to trigger ready state
|
|
102
104
|
setTimeout(() => {
|
|
103
|
-
if (opusProcess && !
|
|
105
|
+
if (opusProcess && !opusProcess.killed && opusProcess.stdin.writable) {
|
|
104
106
|
const warmup = { type: 'user', message: { role: 'user', content: 'hi' } };
|
|
105
107
|
opusProcess.stdin.write(JSON.stringify(warmup) + '\n');
|
|
106
108
|
}
|
|
@@ -139,12 +141,15 @@ function handleOpusEvent(event) {
|
|
|
139
141
|
}
|
|
140
142
|
}
|
|
141
143
|
}
|
|
142
|
-
// Text streaming
|
|
144
|
+
// Text streaming (text_delta = response text, thinking_delta = extended thinking)
|
|
143
145
|
if (event.type === 'content_block_delta') {
|
|
144
146
|
const delta = event.delta;
|
|
145
147
|
if (delta?.type === 'text_delta' && delta.text) {
|
|
146
148
|
opusCallbacks?.onText?.(delta.text);
|
|
147
149
|
}
|
|
150
|
+
else if (delta?.type === 'thinking_delta' && delta.thinking) {
|
|
151
|
+
opusCallbacks?.onText?.(delta.thinking);
|
|
152
|
+
}
|
|
148
153
|
}
|
|
149
154
|
// Final result
|
|
150
155
|
if (event.type === 'result' && event.result) {
|
|
@@ -173,7 +178,13 @@ async function sendToOpus(prompt, callbacks) {
|
|
|
173
178
|
type: 'user',
|
|
174
179
|
message: { role: 'user', content: prompt },
|
|
175
180
|
};
|
|
176
|
-
opusProcess.stdin.
|
|
181
|
+
if (opusProcess && !opusProcess.killed && opusProcess.stdin.writable) {
|
|
182
|
+
opusProcess.stdin.write(JSON.stringify(inputMessage) + '\n');
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
opusPendingResolve?.('Process is not available, please try again.');
|
|
186
|
+
opusPendingResolve = null;
|
|
187
|
+
}
|
|
177
188
|
// Timeout after 5 min for complex tasks
|
|
178
189
|
setTimeout(() => {
|
|
179
190
|
if (opusPendingResolve) {
|
|
@@ -183,6 +194,10 @@ async function sendToOpus(prompt, callbacks) {
|
|
|
183
194
|
}, 300000);
|
|
184
195
|
});
|
|
185
196
|
}
|
|
197
|
+
// === Planning API (reuses warm persistent process) ===
|
|
198
|
+
export async function sendPlanningPrompt(prompt, callbacks) {
|
|
199
|
+
return sendToOpus(prompt, callbacks || {});
|
|
200
|
+
}
|
|
186
201
|
// Mode is now passed as parameter - default is 'plan', UI can toggle to 'direct'
|
|
187
202
|
export async function sendToGekto(prompt, mode = 'plan', callbacks) {
|
|
188
203
|
const startTime = Date.now();
|
|
@@ -93,8 +93,13 @@ function runHaiku(prompt, systemPrompt, workingDir) {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
-
else if (event.type === 'content_block_delta'
|
|
97
|
-
|
|
96
|
+
else if (event.type === 'content_block_delta') {
|
|
97
|
+
if (event.delta?.text) {
|
|
98
|
+
resultText += event.delta.text;
|
|
99
|
+
}
|
|
100
|
+
else if (event.delta?.thinking) {
|
|
101
|
+
resultText += event.delta.thinking;
|
|
102
|
+
}
|
|
98
103
|
}
|
|
99
104
|
}
|
|
100
105
|
catch {
|