@siftd/connect-agent 0.1.0 → 0.2.2
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/README.md +106 -0
- package/dist/agent.js +202 -23
- package/dist/api.d.ts +1 -0
- package/dist/cli.js +21 -2
- package/dist/config.d.ts +14 -0
- package/dist/config.js +36 -3
- package/dist/core/memory-advanced.d.ts +91 -0
- package/dist/core/memory-advanced.js +571 -0
- package/dist/core/scheduler.d.ts +101 -0
- package/dist/core/scheduler.js +311 -0
- package/dist/core/system-indexer.d.ts +65 -0
- package/dist/core/system-indexer.js +354 -0
- package/dist/genesis/index.d.ts +56 -0
- package/dist/genesis/index.js +71 -0
- package/dist/genesis/system-knowledge.json +62 -0
- package/dist/genesis/tool-patterns.json +88 -0
- package/dist/heartbeat.d.ts +32 -0
- package/dist/heartbeat.js +166 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/orchestrator.d.ts +122 -0
- package/dist/orchestrator.js +1001 -0
- package/dist/tools/bash.d.ts +18 -0
- package/dist/tools/bash.js +85 -0
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.js +7 -0
- package/dist/tools/web.d.ts +38 -0
- package/dist/tools/web.js +284 -0
- package/dist/tools/worker.d.ts +36 -0
- package/dist/tools/worker.js +149 -0
- package/dist/websocket.d.ts +73 -0
- package/dist/websocket.js +240 -0
- package/dist/workers/manager.d.ts +62 -0
- package/dist/workers/manager.js +270 -0
- package/dist/workers/types.d.ts +31 -0
- package/dist/workers/types.js +10 -0
- package/package.json +15 -5
|
@@ -0,0 +1,1001 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Master Orchestrator Agent
|
|
3
|
+
*
|
|
4
|
+
* This is the BRAIN - it orchestrates, remembers, and delegates.
|
|
5
|
+
* It does NOT do the work itself. Claude Code CLI workers do the work.
|
|
6
|
+
*/
|
|
7
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
8
|
+
import { spawn, execSync } from 'child_process';
|
|
9
|
+
import { existsSync } from 'fs';
|
|
10
|
+
import { AdvancedMemoryStore } from './core/memory-advanced.js';
|
|
11
|
+
import { TaskScheduler } from './core/scheduler.js';
|
|
12
|
+
import { SystemIndexer } from './core/system-indexer.js';
|
|
13
|
+
import { BashTool } from './tools/bash.js';
|
|
14
|
+
import { WebTools } from './tools/web.js';
|
|
15
|
+
import { WorkerTools } from './tools/worker.js';
|
|
16
|
+
import { getKnowledgeForPrompt } from './genesis/index.js';
|
|
17
|
+
const SYSTEM_PROMPT = `You're the user's personal assistant - the friendly brain behind their Connect app. You chat with them from any browser, remember everything about them, and dispatch Claude Code workers to do things on their machine.
|
|
18
|
+
|
|
19
|
+
CRITICAL - READ THIS:
|
|
20
|
+
The user is on a WEB BROWSER. They CANNOT run terminal commands. They CANNOT access a shell.
|
|
21
|
+
YOU are their hands. When something needs to be done, YOU do it with your tools.
|
|
22
|
+
NEVER say "run this command" or "you'll need to..." - the user CAN'T do that.
|
|
23
|
+
|
|
24
|
+
YOUR TOOLS (use them!):
|
|
25
|
+
- bash: Run any shell command directly
|
|
26
|
+
- web_search: Search the internet for information
|
|
27
|
+
- fetch_url: Fetch and read web page content
|
|
28
|
+
- spawn_worker: Spawn background Claude Code workers for complex multi-step tasks
|
|
29
|
+
- delegate_to_worker: Delegate complex tasks that need full CLI autonomy
|
|
30
|
+
- remember/search_memory: Persistent memory across conversations
|
|
31
|
+
- start_local_server/open_browser: Serve and preview web projects
|
|
32
|
+
|
|
33
|
+
WHEN TO USE WHAT:
|
|
34
|
+
- Simple commands (ls, cat, git status, npm install) → use bash directly
|
|
35
|
+
- Web research → use web_search + fetch_url
|
|
36
|
+
- Complex multi-file changes, builds, deployments → spawn_worker or delegate_to_worker
|
|
37
|
+
- Quick checks or reads → bash
|
|
38
|
+
- Long-running background tasks → spawn_worker (non-blocking)
|
|
39
|
+
|
|
40
|
+
FILESYSTEM AWARENESS:
|
|
41
|
+
You know the user's filesystem. Their projects, directories, and files are indexed in your memory.
|
|
42
|
+
When they mention "that project", "the connect app", "downloads folder", etc. - SEARCH YOUR MEMORY.
|
|
43
|
+
Use search_memory to find the actual paths before working.
|
|
44
|
+
|
|
45
|
+
WHO YOU ARE:
|
|
46
|
+
Warm, helpful, and genuinely interested in what the user is working on. Like a knowledgeable friend with superpowers - you remember their preferences, track their projects, and make things happen.
|
|
47
|
+
|
|
48
|
+
WHAT YOU CAN DO:
|
|
49
|
+
- Run shell commands directly with bash
|
|
50
|
+
- Search the web and fetch URLs
|
|
51
|
+
- Dispatch Claude Code workers for complex work
|
|
52
|
+
- Start local servers and open browsers
|
|
53
|
+
- Remember things across conversations
|
|
54
|
+
- Schedule tasks for later
|
|
55
|
+
- Have real conversations
|
|
56
|
+
|
|
57
|
+
HOW TO TALK:
|
|
58
|
+
- Be yourself - warm, direct, helpful
|
|
59
|
+
- Keep it conversational
|
|
60
|
+
- Show you remember them
|
|
61
|
+
- Celebrate progress - "Nice, that's done!" not "Task completed successfully."
|
|
62
|
+
- Be honest about what you're doing
|
|
63
|
+
- If something fails, try a different approach or explain what happened
|
|
64
|
+
|
|
65
|
+
SLASH COMMANDS:
|
|
66
|
+
- /reset - Clear conversation and memory context
|
|
67
|
+
- /verbose - Toggle showing tool execution details
|
|
68
|
+
- /help - Show available commands
|
|
69
|
+
|
|
70
|
+
You're the persistent layer that makes AI feel personal. You ACT on behalf of the user - you don't give them homework.`;
|
|
71
|
+
export class MasterOrchestrator {
|
|
72
|
+
client;
|
|
73
|
+
model;
|
|
74
|
+
maxTokens;
|
|
75
|
+
memory;
|
|
76
|
+
scheduler;
|
|
77
|
+
indexer;
|
|
78
|
+
jobs = new Map();
|
|
79
|
+
jobCounter = 0;
|
|
80
|
+
userId;
|
|
81
|
+
workspaceDir;
|
|
82
|
+
claudePath;
|
|
83
|
+
initialized = false;
|
|
84
|
+
// New tools from whatsapp-claude
|
|
85
|
+
bashTool;
|
|
86
|
+
webTools;
|
|
87
|
+
workerTools;
|
|
88
|
+
verboseMode = new Map(); // per-user verbose mode
|
|
89
|
+
constructor(options) {
|
|
90
|
+
this.client = new Anthropic({ apiKey: options.apiKey });
|
|
91
|
+
this.model = options.model || 'claude-sonnet-4-20250514';
|
|
92
|
+
this.maxTokens = options.maxTokens || 4096;
|
|
93
|
+
this.userId = options.userId;
|
|
94
|
+
this.workspaceDir = options.workspaceDir || process.env.HOME || '/tmp';
|
|
95
|
+
// Find claude binary - critical for worker delegation
|
|
96
|
+
this.claudePath = this.findClaudeBinary();
|
|
97
|
+
console.log(`[ORCHESTRATOR] Claude binary: ${this.claudePath}`);
|
|
98
|
+
// Initialize memory with user-specific paths
|
|
99
|
+
this.memory = new AdvancedMemoryStore({
|
|
100
|
+
dbPath: `memories_${options.userId}.db`,
|
|
101
|
+
vectorPath: `./memory_vectors_${options.userId}`,
|
|
102
|
+
voyageApiKey: options.voyageApiKey
|
|
103
|
+
});
|
|
104
|
+
this.scheduler = new TaskScheduler(`scheduled_${options.userId}.json`);
|
|
105
|
+
// Create indexer (will index on first initialize call)
|
|
106
|
+
this.indexer = new SystemIndexer(this.memory);
|
|
107
|
+
// Initialize new tools
|
|
108
|
+
this.bashTool = new BashTool(this.workspaceDir);
|
|
109
|
+
this.webTools = new WebTools();
|
|
110
|
+
this.workerTools = new WorkerTools(this.workspaceDir);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Initialize the orchestrator - indexes filesystem on first call
|
|
114
|
+
*/
|
|
115
|
+
async initialize() {
|
|
116
|
+
if (this.initialized)
|
|
117
|
+
return;
|
|
118
|
+
console.log('[ORCHESTRATOR] Initializing...');
|
|
119
|
+
// Index the filesystem into memory
|
|
120
|
+
try {
|
|
121
|
+
const result = await this.indexer.indexHome();
|
|
122
|
+
console.log(`[ORCHESTRATOR] Filesystem indexed: ${result.projects} projects, ${result.directories} directories`);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
console.error('[ORCHESTRATOR] Filesystem indexing failed:', error);
|
|
126
|
+
// Continue anyway - indexing is a nice-to-have
|
|
127
|
+
}
|
|
128
|
+
this.initialized = true;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Find the claude binary path
|
|
132
|
+
*/
|
|
133
|
+
findClaudeBinary() {
|
|
134
|
+
// Since we spawn with shell: true, just use 'claude' command
|
|
135
|
+
// Shell will resolve PATH correctly (including NVM)
|
|
136
|
+
// Verify it exists by running 'which claude'
|
|
137
|
+
try {
|
|
138
|
+
const path = execSync('which claude', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
139
|
+
if (path) {
|
|
140
|
+
console.log(`[ORCHESTRATOR] Claude found at: ${path}`);
|
|
141
|
+
return 'claude'; // Use command name, not path (shell resolves it)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
console.warn('[ORCHESTRATOR] Warning: claude not found in PATH');
|
|
146
|
+
}
|
|
147
|
+
return 'claude';
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Handle slash commands
|
|
151
|
+
*/
|
|
152
|
+
handleSlashCommand(message) {
|
|
153
|
+
const lower = message.trim().toLowerCase();
|
|
154
|
+
if (lower === '/help') {
|
|
155
|
+
return `**Available Commands:**
|
|
156
|
+
- \`/reset\` - Clear conversation context
|
|
157
|
+
- \`/verbose\` - Toggle verbose mode (show tool execution details)
|
|
158
|
+
- \`/help\` - Show this help message
|
|
159
|
+
|
|
160
|
+
**What I can do:**
|
|
161
|
+
- Run shell commands, search the web, fetch URLs
|
|
162
|
+
- Spawn Claude Code workers for complex tasks
|
|
163
|
+
- Remember things across our conversations
|
|
164
|
+
- Schedule tasks for later
|
|
165
|
+
- Start local servers and open your browser`;
|
|
166
|
+
}
|
|
167
|
+
if (lower === '/verbose') {
|
|
168
|
+
const current = this.verboseMode.get(this.userId) || false;
|
|
169
|
+
this.verboseMode.set(this.userId, !current);
|
|
170
|
+
return `Verbose mode ${!current ? 'enabled' : 'disabled'}. ${!current ? 'I\'ll show you what tools I\'m using.' : 'Back to concise mode.'}`;
|
|
171
|
+
}
|
|
172
|
+
if (lower === '/reset') {
|
|
173
|
+
// Note: actual conversation reset happens in the agent.ts caller
|
|
174
|
+
return 'Conversation reset. Fresh start!';
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Check if verbose mode is enabled
|
|
180
|
+
*/
|
|
181
|
+
isVerbose() {
|
|
182
|
+
return this.verboseMode.get(this.userId) || false;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Process a user message
|
|
186
|
+
* @param apiKey Optional per-request API key (overrides default)
|
|
187
|
+
*/
|
|
188
|
+
async processMessage(message, conversationHistory = [], sendMessage, apiKey) {
|
|
189
|
+
// Handle slash commands first
|
|
190
|
+
const slashResponse = this.handleSlashCommand(message);
|
|
191
|
+
if (slashResponse) {
|
|
192
|
+
return slashResponse;
|
|
193
|
+
}
|
|
194
|
+
// Build context from memory
|
|
195
|
+
const memoryContext = await this.getMemoryContext(message);
|
|
196
|
+
// Build system prompt with genesis knowledge and memory context
|
|
197
|
+
const genesisKnowledge = getKnowledgeForPrompt();
|
|
198
|
+
let systemWithContext = SYSTEM_PROMPT + genesisKnowledge;
|
|
199
|
+
if (memoryContext) {
|
|
200
|
+
systemWithContext += `\n\nRELEVANT MEMORIES:\n${memoryContext}`;
|
|
201
|
+
}
|
|
202
|
+
// Add user message
|
|
203
|
+
const messages = [
|
|
204
|
+
...conversationHistory,
|
|
205
|
+
{ role: 'user', content: message }
|
|
206
|
+
];
|
|
207
|
+
try {
|
|
208
|
+
const response = await this.runAgentLoop(messages, systemWithContext, sendMessage, apiKey);
|
|
209
|
+
// Auto-remember important things from the conversation
|
|
210
|
+
await this.autoRemember(message, response);
|
|
211
|
+
return response;
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
215
|
+
return `Error: ${errorMessage}`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Get relevant memory context for a message
|
|
220
|
+
*/
|
|
221
|
+
async getMemoryContext(message) {
|
|
222
|
+
try {
|
|
223
|
+
const memories = await this.memory.search(message, { limit: 5 });
|
|
224
|
+
if (memories.length === 0)
|
|
225
|
+
return null;
|
|
226
|
+
return memories.map(m => `[${m.type}] ${m.content}`).join('\n');
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Auto-remember important information from conversations
|
|
234
|
+
*/
|
|
235
|
+
async autoRemember(userMessage, response) {
|
|
236
|
+
// Look for preference indicators
|
|
237
|
+
const prefIndicators = ['i prefer', 'i like', 'i always', 'i never', 'i use', 'my favorite'];
|
|
238
|
+
const lower = userMessage.toLowerCase();
|
|
239
|
+
for (const indicator of prefIndicators) {
|
|
240
|
+
if (lower.includes(indicator)) {
|
|
241
|
+
await this.memory.remember(userMessage, {
|
|
242
|
+
type: 'semantic',
|
|
243
|
+
source: 'auto-extract',
|
|
244
|
+
importance: 0.7,
|
|
245
|
+
tags: ['preference', 'user']
|
|
246
|
+
});
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Run the agentic loop
|
|
253
|
+
* @param apiKey Optional per-request API key (creates temporary client)
|
|
254
|
+
*/
|
|
255
|
+
async runAgentLoop(messages, system, sendMessage, apiKey) {
|
|
256
|
+
const tools = this.getToolDefinitions();
|
|
257
|
+
let currentMessages = [...messages];
|
|
258
|
+
let iterations = 0;
|
|
259
|
+
const maxIterations = 30; // Increased for complex multi-tool tasks
|
|
260
|
+
// Use per-request client if apiKey provided, otherwise use default
|
|
261
|
+
const client = apiKey ? new Anthropic({ apiKey }) : this.client;
|
|
262
|
+
while (iterations < maxIterations) {
|
|
263
|
+
iterations++;
|
|
264
|
+
const response = await client.messages.create({
|
|
265
|
+
model: this.model,
|
|
266
|
+
max_tokens: this.maxTokens,
|
|
267
|
+
system,
|
|
268
|
+
tools,
|
|
269
|
+
messages: currentMessages
|
|
270
|
+
});
|
|
271
|
+
// Check if done
|
|
272
|
+
if (response.stop_reason === 'end_turn' || !this.hasToolUse(response.content)) {
|
|
273
|
+
return this.extractText(response.content);
|
|
274
|
+
}
|
|
275
|
+
// Process tool calls
|
|
276
|
+
const toolResults = await this.processToolCalls(response.content, sendMessage);
|
|
277
|
+
// Continue conversation
|
|
278
|
+
currentMessages = [
|
|
279
|
+
...currentMessages,
|
|
280
|
+
{ role: 'assistant', content: response.content },
|
|
281
|
+
{ role: 'user', content: toolResults }
|
|
282
|
+
];
|
|
283
|
+
}
|
|
284
|
+
return 'Maximum iterations reached.';
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Get tool definitions for the orchestrator
|
|
288
|
+
*/
|
|
289
|
+
getToolDefinitions() {
|
|
290
|
+
return [
|
|
291
|
+
// Direct bash execution - for simple commands
|
|
292
|
+
{
|
|
293
|
+
name: 'bash',
|
|
294
|
+
description: 'Execute a shell command directly. Use for simple commands like ls, cat, git status, npm install, etc. For complex multi-step tasks, use spawn_worker instead.',
|
|
295
|
+
input_schema: {
|
|
296
|
+
type: 'object',
|
|
297
|
+
properties: {
|
|
298
|
+
command: {
|
|
299
|
+
type: 'string',
|
|
300
|
+
description: 'The bash command to execute'
|
|
301
|
+
},
|
|
302
|
+
timeout: {
|
|
303
|
+
type: 'number',
|
|
304
|
+
description: 'Timeout in milliseconds (default: 30000, max: 120000)'
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
required: ['command']
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
// Web tools
|
|
311
|
+
{
|
|
312
|
+
name: 'web_search',
|
|
313
|
+
description: 'Search the web for information. Returns titles, URLs, and snippets from search results.',
|
|
314
|
+
input_schema: {
|
|
315
|
+
type: 'object',
|
|
316
|
+
properties: {
|
|
317
|
+
query: {
|
|
318
|
+
type: 'string',
|
|
319
|
+
description: 'The search query'
|
|
320
|
+
},
|
|
321
|
+
num_results: {
|
|
322
|
+
type: 'number',
|
|
323
|
+
description: 'Number of results to return (default: 5, max: 10)'
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
required: ['query']
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: 'fetch_url',
|
|
331
|
+
description: 'Fetch and parse content from a URL. Returns extracted text content by default.',
|
|
332
|
+
input_schema: {
|
|
333
|
+
type: 'object',
|
|
334
|
+
properties: {
|
|
335
|
+
url: {
|
|
336
|
+
type: 'string',
|
|
337
|
+
description: 'The URL to fetch'
|
|
338
|
+
},
|
|
339
|
+
format: {
|
|
340
|
+
type: 'string',
|
|
341
|
+
enum: ['text', 'html', 'json'],
|
|
342
|
+
description: 'Output format (default: text)'
|
|
343
|
+
},
|
|
344
|
+
timeout: {
|
|
345
|
+
type: 'number',
|
|
346
|
+
description: 'Timeout in milliseconds (default: 10000)'
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
required: ['url']
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
// Worker tools - for parallel/background tasks
|
|
353
|
+
{
|
|
354
|
+
name: 'spawn_worker',
|
|
355
|
+
description: 'Spawn a Claude Code worker for a complex task. Non-blocking - returns immediately with a job ID. Use check_worker to monitor progress.',
|
|
356
|
+
input_schema: {
|
|
357
|
+
type: 'object',
|
|
358
|
+
properties: {
|
|
359
|
+
task: {
|
|
360
|
+
type: 'string',
|
|
361
|
+
description: 'The task description for the worker'
|
|
362
|
+
},
|
|
363
|
+
timeout: {
|
|
364
|
+
type: 'number',
|
|
365
|
+
description: 'Timeout in milliseconds (default: 300000 / 5 min)'
|
|
366
|
+
},
|
|
367
|
+
priority: {
|
|
368
|
+
type: 'string',
|
|
369
|
+
enum: ['low', 'normal', 'high', 'critical'],
|
|
370
|
+
description: 'Task priority'
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
required: ['task']
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
name: 'check_worker',
|
|
378
|
+
description: 'Check the status and result of a spawned worker.',
|
|
379
|
+
input_schema: {
|
|
380
|
+
type: 'object',
|
|
381
|
+
properties: {
|
|
382
|
+
job_id: {
|
|
383
|
+
type: 'string',
|
|
384
|
+
description: 'The job ID from spawn_worker'
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
required: ['job_id']
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
name: 'wait_worker',
|
|
392
|
+
description: 'Wait for a worker to complete and return its result. Blocks until done.',
|
|
393
|
+
input_schema: {
|
|
394
|
+
type: 'object',
|
|
395
|
+
properties: {
|
|
396
|
+
job_id: {
|
|
397
|
+
type: 'string',
|
|
398
|
+
description: 'The job ID to wait for'
|
|
399
|
+
},
|
|
400
|
+
max_wait: {
|
|
401
|
+
type: 'number',
|
|
402
|
+
description: 'Maximum wait time in milliseconds'
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
required: ['job_id']
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
name: 'list_workers',
|
|
410
|
+
description: 'List all worker jobs with their status.',
|
|
411
|
+
input_schema: {
|
|
412
|
+
type: 'object',
|
|
413
|
+
properties: {
|
|
414
|
+
status: {
|
|
415
|
+
type: 'string',
|
|
416
|
+
description: 'Filter by status (running, completed, failed)'
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
required: []
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
name: 'cancel_worker',
|
|
424
|
+
description: 'Cancel a running worker job.',
|
|
425
|
+
input_schema: {
|
|
426
|
+
type: 'object',
|
|
427
|
+
properties: {
|
|
428
|
+
job_id: {
|
|
429
|
+
type: 'string',
|
|
430
|
+
description: 'The job ID to cancel'
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
required: ['job_id']
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
// Delegate tool - for full CLI autonomy
|
|
437
|
+
{
|
|
438
|
+
name: 'delegate_to_worker',
|
|
439
|
+
description: `Delegate a task to Claude Code CLI worker. Use for complex multi-step tasks that need full CLI autonomy.
|
|
440
|
+
|
|
441
|
+
Use this for ANY task that involves:
|
|
442
|
+
- Reading, writing, or editing files
|
|
443
|
+
- Running shell commands
|
|
444
|
+
- Exploring codebases
|
|
445
|
+
- Making code changes
|
|
446
|
+
- Building, testing, deploying
|
|
447
|
+
- ANY filesystem or system operation
|
|
448
|
+
|
|
449
|
+
The worker will execute the task autonomously and return results.
|
|
450
|
+
Be specific about what you want done.`,
|
|
451
|
+
input_schema: {
|
|
452
|
+
type: 'object',
|
|
453
|
+
properties: {
|
|
454
|
+
task: {
|
|
455
|
+
type: 'string',
|
|
456
|
+
description: 'Detailed description of what the worker should do'
|
|
457
|
+
},
|
|
458
|
+
context: {
|
|
459
|
+
type: 'string',
|
|
460
|
+
description: 'Additional context (user preferences, project info, etc)'
|
|
461
|
+
},
|
|
462
|
+
working_directory: {
|
|
463
|
+
type: 'string',
|
|
464
|
+
description: 'Directory to work in (defaults to user home)'
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
required: ['task']
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
name: 'remember',
|
|
472
|
+
description: 'Store something in persistent memory. Use for user preferences, project info, important facts.',
|
|
473
|
+
input_schema: {
|
|
474
|
+
type: 'object',
|
|
475
|
+
properties: {
|
|
476
|
+
content: {
|
|
477
|
+
type: 'string',
|
|
478
|
+
description: 'What to remember'
|
|
479
|
+
},
|
|
480
|
+
type: {
|
|
481
|
+
type: 'string',
|
|
482
|
+
enum: ['episodic', 'semantic', 'procedural'],
|
|
483
|
+
description: 'Memory type: episodic (events), semantic (facts), procedural (how-to)'
|
|
484
|
+
},
|
|
485
|
+
importance: {
|
|
486
|
+
type: 'number',
|
|
487
|
+
description: 'Importance 0-1 (higher = remembered longer)'
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
required: ['content']
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
name: 'search_memory',
|
|
495
|
+
description: 'Search your memory for relevant information. Use before delegating to include user context.',
|
|
496
|
+
input_schema: {
|
|
497
|
+
type: 'object',
|
|
498
|
+
properties: {
|
|
499
|
+
query: {
|
|
500
|
+
type: 'string',
|
|
501
|
+
description: 'What to search for'
|
|
502
|
+
},
|
|
503
|
+
type: {
|
|
504
|
+
type: 'string',
|
|
505
|
+
enum: ['episodic', 'semantic', 'procedural'],
|
|
506
|
+
description: 'Filter by memory type'
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
required: ['query']
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
name: 'schedule_task',
|
|
514
|
+
description: 'Schedule a task for future execution.',
|
|
515
|
+
input_schema: {
|
|
516
|
+
type: 'object',
|
|
517
|
+
properties: {
|
|
518
|
+
task: {
|
|
519
|
+
type: 'string',
|
|
520
|
+
description: 'The task to execute later'
|
|
521
|
+
},
|
|
522
|
+
when: {
|
|
523
|
+
type: 'string',
|
|
524
|
+
description: 'When to run (e.g., "in 30 minutes", "daily at 9:00", "tomorrow at 14:00")'
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
required: ['task', 'when']
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
name: 'list_scheduled',
|
|
532
|
+
description: 'List all scheduled tasks.',
|
|
533
|
+
input_schema: {
|
|
534
|
+
type: 'object',
|
|
535
|
+
properties: {},
|
|
536
|
+
required: []
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
name: 'memory_stats',
|
|
541
|
+
description: 'Get statistics about your memory system.',
|
|
542
|
+
input_schema: {
|
|
543
|
+
type: 'object',
|
|
544
|
+
properties: {},
|
|
545
|
+
required: []
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
name: 'open_browser',
|
|
550
|
+
description: 'Open a URL or file in the user\'s default browser. Use for opening localhost servers, web pages, or local HTML files.',
|
|
551
|
+
input_schema: {
|
|
552
|
+
type: 'object',
|
|
553
|
+
properties: {
|
|
554
|
+
url: {
|
|
555
|
+
type: 'string',
|
|
556
|
+
description: 'URL to open (e.g., http://localhost:8080, https://example.com, or file path)'
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
required: ['url']
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
name: 'start_local_server',
|
|
564
|
+
description: 'Start a persistent HTTP server to serve a web project. Server runs in background and survives after this call. Use before open_browser for local web projects.',
|
|
565
|
+
input_schema: {
|
|
566
|
+
type: 'object',
|
|
567
|
+
properties: {
|
|
568
|
+
directory: {
|
|
569
|
+
type: 'string',
|
|
570
|
+
description: 'Directory to serve (absolute path)'
|
|
571
|
+
},
|
|
572
|
+
port: {
|
|
573
|
+
type: 'number',
|
|
574
|
+
description: 'Port to serve on (default: 8080)'
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
required: ['directory']
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
name: 'stop_local_server',
|
|
582
|
+
description: 'Stop a running local server on a specific port.',
|
|
583
|
+
input_schema: {
|
|
584
|
+
type: 'object',
|
|
585
|
+
properties: {
|
|
586
|
+
port: {
|
|
587
|
+
type: 'number',
|
|
588
|
+
description: 'Port of the server to stop'
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
required: ['port']
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
];
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Process tool calls
|
|
598
|
+
*/
|
|
599
|
+
async processToolCalls(content, sendMessage) {
|
|
600
|
+
const toolUseBlocks = content.filter((block) => block.type === 'tool_use');
|
|
601
|
+
const results = [];
|
|
602
|
+
for (const toolUse of toolUseBlocks) {
|
|
603
|
+
const input = toolUse.input;
|
|
604
|
+
let result;
|
|
605
|
+
// Notify user of action
|
|
606
|
+
if (sendMessage) {
|
|
607
|
+
const preview = this.formatToolPreview(toolUse.name, input);
|
|
608
|
+
if (preview)
|
|
609
|
+
await sendMessage(preview);
|
|
610
|
+
}
|
|
611
|
+
switch (toolUse.name) {
|
|
612
|
+
// New direct tools
|
|
613
|
+
case 'bash':
|
|
614
|
+
result = await this.bashTool.execute(input.command, input.timeout);
|
|
615
|
+
break;
|
|
616
|
+
case 'web_search':
|
|
617
|
+
result = await this.webTools.webSearch(input.query, { numResults: input.num_results });
|
|
618
|
+
break;
|
|
619
|
+
case 'fetch_url':
|
|
620
|
+
result = await this.webTools.fetchUrl(input.url, {
|
|
621
|
+
format: input.format,
|
|
622
|
+
timeout: input.timeout
|
|
623
|
+
});
|
|
624
|
+
break;
|
|
625
|
+
// Worker tools
|
|
626
|
+
case 'spawn_worker':
|
|
627
|
+
result = await this.workerTools.spawnWorker(input.task, {
|
|
628
|
+
timeout: input.timeout,
|
|
629
|
+
priority: input.priority
|
|
630
|
+
});
|
|
631
|
+
break;
|
|
632
|
+
case 'check_worker':
|
|
633
|
+
result = await this.workerTools.checkWorker(input.job_id);
|
|
634
|
+
break;
|
|
635
|
+
case 'wait_worker':
|
|
636
|
+
result = await this.workerTools.waitWorker(input.job_id, input.max_wait);
|
|
637
|
+
break;
|
|
638
|
+
case 'list_workers':
|
|
639
|
+
result = await this.workerTools.listWorkers(input.status);
|
|
640
|
+
break;
|
|
641
|
+
case 'cancel_worker':
|
|
642
|
+
result = await this.workerTools.cancelWorker(input.job_id);
|
|
643
|
+
break;
|
|
644
|
+
// Legacy delegate tool
|
|
645
|
+
case 'delegate_to_worker':
|
|
646
|
+
result = await this.delegateToWorker(input.task, input.context, input.working_directory);
|
|
647
|
+
break;
|
|
648
|
+
case 'remember':
|
|
649
|
+
result = await this.executeRemember(input.content, input.type, input.importance);
|
|
650
|
+
break;
|
|
651
|
+
case 'search_memory':
|
|
652
|
+
result = await this.executeSearchMemory(input.query, input.type);
|
|
653
|
+
break;
|
|
654
|
+
case 'schedule_task':
|
|
655
|
+
result = await this.executeScheduleTask(input.task, input.when);
|
|
656
|
+
break;
|
|
657
|
+
case 'list_scheduled':
|
|
658
|
+
result = await this.executeListScheduled();
|
|
659
|
+
break;
|
|
660
|
+
case 'memory_stats':
|
|
661
|
+
result = await this.executeMemoryStats();
|
|
662
|
+
break;
|
|
663
|
+
case 'open_browser':
|
|
664
|
+
result = await this.executeOpenBrowser(input.url);
|
|
665
|
+
break;
|
|
666
|
+
case 'start_local_server':
|
|
667
|
+
result = await this.executeStartServer(input.directory, input.port);
|
|
668
|
+
break;
|
|
669
|
+
case 'stop_local_server':
|
|
670
|
+
result = await this.executeStopServer(input.port);
|
|
671
|
+
break;
|
|
672
|
+
default:
|
|
673
|
+
result = { success: false, output: `Unknown tool: ${toolUse.name}` };
|
|
674
|
+
}
|
|
675
|
+
// Anthropic API requires content to be non-empty when is_error is true
|
|
676
|
+
const content = result.output || result.error || 'Unknown error';
|
|
677
|
+
results.push({
|
|
678
|
+
type: 'tool_result',
|
|
679
|
+
tool_use_id: toolUse.id,
|
|
680
|
+
content,
|
|
681
|
+
is_error: !result.success
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
return results;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Delegate task to Claude Code CLI worker with retry logic
|
|
688
|
+
*/
|
|
689
|
+
async delegateToWorker(task, context, workingDir, retryCount = 0) {
|
|
690
|
+
const maxRetries = 2;
|
|
691
|
+
const id = `worker_${Date.now()}_${++this.jobCounter}`;
|
|
692
|
+
const cwd = workingDir || this.workspaceDir;
|
|
693
|
+
// Build prompt for worker
|
|
694
|
+
let prompt = task;
|
|
695
|
+
if (context) {
|
|
696
|
+
prompt = `Context: ${context}\n\nTask: ${task}`;
|
|
697
|
+
}
|
|
698
|
+
prompt += '\n\nKeep your response concise and focused on results.';
|
|
699
|
+
console.log(`[ORCHESTRATOR] Delegating to worker ${id}: ${task.slice(0, 80)}...`);
|
|
700
|
+
return new Promise((resolve) => {
|
|
701
|
+
const job = {
|
|
702
|
+
id,
|
|
703
|
+
task: task.slice(0, 200),
|
|
704
|
+
status: 'running',
|
|
705
|
+
startTime: Date.now(),
|
|
706
|
+
output: ''
|
|
707
|
+
};
|
|
708
|
+
// Escape single quotes in prompt for shell safety
|
|
709
|
+
// Replace ' with '\'' (end quote, escaped quote, start quote)
|
|
710
|
+
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
711
|
+
// Spawn using bash -l -c to ensure proper PATH resolution (NVM, etc.)
|
|
712
|
+
// This is more reliable than shell: true which has quote escaping issues
|
|
713
|
+
const child = spawn('bash', ['-l', '-c', `claude -p '${escapedPrompt}' --dangerously-skip-permissions`], {
|
|
714
|
+
cwd,
|
|
715
|
+
env: { ...process.env },
|
|
716
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
717
|
+
});
|
|
718
|
+
job.process = child;
|
|
719
|
+
this.jobs.set(id, job);
|
|
720
|
+
// Timeout after 5 minutes
|
|
721
|
+
const timeout = setTimeout(() => {
|
|
722
|
+
if (job.status === 'running') {
|
|
723
|
+
job.status = 'timeout';
|
|
724
|
+
job.endTime = Date.now();
|
|
725
|
+
child.kill('SIGTERM');
|
|
726
|
+
resolve({
|
|
727
|
+
success: false,
|
|
728
|
+
output: `Worker timed out after 5 minutes.\nPartial output:\n${job.output.slice(-1000)}`
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
}, 5 * 60 * 1000);
|
|
732
|
+
child.stdout?.on('data', (data) => {
|
|
733
|
+
job.output += data.toString();
|
|
734
|
+
});
|
|
735
|
+
child.stderr?.on('data', (data) => {
|
|
736
|
+
// Ignore status messages
|
|
737
|
+
const text = data.toString();
|
|
738
|
+
if (!text.includes('Checking') && !text.includes('Connected')) {
|
|
739
|
+
job.output += text;
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
child.on('close', (code) => {
|
|
743
|
+
clearTimeout(timeout);
|
|
744
|
+
job.status = code === 0 ? 'completed' : 'failed';
|
|
745
|
+
job.endTime = Date.now();
|
|
746
|
+
const duration = ((job.endTime - job.startTime) / 1000).toFixed(1);
|
|
747
|
+
console.log(`[ORCHESTRATOR] Worker ${id} finished in ${duration}s (code: ${code})`);
|
|
748
|
+
if (code !== 0 || job.output.length === 0) {
|
|
749
|
+
console.log(`[ORCHESTRATOR] Worker ${id} output: ${job.output.slice(0, 200) || '(empty)'}`);
|
|
750
|
+
}
|
|
751
|
+
resolve({
|
|
752
|
+
success: code === 0,
|
|
753
|
+
output: job.output.trim() || '(No output)'
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
child.on('error', async (err) => {
|
|
757
|
+
clearTimeout(timeout);
|
|
758
|
+
job.status = 'failed';
|
|
759
|
+
job.endTime = Date.now();
|
|
760
|
+
console.error(`[ORCHESTRATOR] Worker ${id} spawn error:`, err.message);
|
|
761
|
+
// Retry on ENOENT (intermittent spawn failures)
|
|
762
|
+
if (err.message.includes('ENOENT') && retryCount < maxRetries) {
|
|
763
|
+
console.log(`[ORCHESTRATOR] Retrying worker (attempt ${retryCount + 2}/${maxRetries + 1})...`);
|
|
764
|
+
await new Promise(r => setTimeout(r, 500)); // Small delay before retry
|
|
765
|
+
const retryResult = await this.delegateToWorker(task, context, workingDir, retryCount + 1);
|
|
766
|
+
resolve(retryResult);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
resolve({
|
|
770
|
+
success: false,
|
|
771
|
+
output: `Worker error: ${err.message}`
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Execute remember tool
|
|
778
|
+
*/
|
|
779
|
+
async executeRemember(content, type, importance) {
|
|
780
|
+
try {
|
|
781
|
+
const id = await this.memory.remember(content, {
|
|
782
|
+
type: type || undefined,
|
|
783
|
+
importance
|
|
784
|
+
});
|
|
785
|
+
return { success: true, output: `Remembered (${id})` };
|
|
786
|
+
}
|
|
787
|
+
catch (error) {
|
|
788
|
+
return { success: false, output: `Failed to remember: ${error}` };
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Execute search memory tool
|
|
793
|
+
*/
|
|
794
|
+
async executeSearchMemory(query, type) {
|
|
795
|
+
try {
|
|
796
|
+
const memories = await this.memory.search(query, {
|
|
797
|
+
type: type || undefined,
|
|
798
|
+
limit: 10
|
|
799
|
+
});
|
|
800
|
+
if (memories.length === 0) {
|
|
801
|
+
return { success: true, output: 'No relevant memories found.' };
|
|
802
|
+
}
|
|
803
|
+
const output = memories.map(m => `[${m.type}] (${(m.importance * 100).toFixed(0)}%) ${m.content}`).join('\n\n');
|
|
804
|
+
return { success: true, output };
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
return { success: false, output: `Memory search failed: ${error}` };
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Execute schedule task tool
|
|
812
|
+
*/
|
|
813
|
+
async executeScheduleTask(task, when) {
|
|
814
|
+
try {
|
|
815
|
+
const taskId = this.scheduler.schedule(task, when, this.userId);
|
|
816
|
+
const scheduled = this.scheduler.get(taskId);
|
|
817
|
+
return {
|
|
818
|
+
success: true,
|
|
819
|
+
output: `Scheduled: ${taskId}\nWhen: ${scheduled ? this.scheduler.formatSchedule(scheduled.schedule) : when}`
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
catch (error) {
|
|
823
|
+
return { success: false, output: `Scheduling failed: ${error}` };
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Execute list scheduled tool
|
|
828
|
+
*/
|
|
829
|
+
async executeListScheduled() {
|
|
830
|
+
const tasks = this.scheduler.list();
|
|
831
|
+
if (tasks.length === 0) {
|
|
832
|
+
return { success: true, output: 'No scheduled tasks.' };
|
|
833
|
+
}
|
|
834
|
+
const output = tasks.map(t => `${t.enabled ? '✓' : '⏸'} ${t.id}: ${t.task.slice(0, 50)}... (${this.scheduler.formatSchedule(t.schedule)})`).join('\n');
|
|
835
|
+
return { success: true, output };
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Execute memory stats tool
|
|
839
|
+
*/
|
|
840
|
+
async executeMemoryStats() {
|
|
841
|
+
const stats = this.memory.stats();
|
|
842
|
+
return {
|
|
843
|
+
success: true,
|
|
844
|
+
output: `Memories: ${stats.total} (episodic: ${stats.byType.episodic}, semantic: ${stats.byType.semantic}, procedural: ${stats.byType.procedural})\nAvg importance: ${(stats.avgImportance * 100).toFixed(0)}%\nAssociations: ${stats.totalAssociations}`
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Open a URL in the user's default browser
|
|
849
|
+
*/
|
|
850
|
+
async executeOpenBrowser(url) {
|
|
851
|
+
try {
|
|
852
|
+
// Use 'open' on macOS, 'xdg-open' on Linux, 'start' on Windows
|
|
853
|
+
const platform = process.platform;
|
|
854
|
+
let command;
|
|
855
|
+
if (platform === 'darwin') {
|
|
856
|
+
command = 'open';
|
|
857
|
+
}
|
|
858
|
+
else if (platform === 'win32') {
|
|
859
|
+
command = 'start';
|
|
860
|
+
}
|
|
861
|
+
else {
|
|
862
|
+
command = 'xdg-open';
|
|
863
|
+
}
|
|
864
|
+
execSync(`${command} "${url}"`, { stdio: 'ignore' });
|
|
865
|
+
console.log(`[ORCHESTRATOR] Opened browser: ${url}`);
|
|
866
|
+
return { success: true, output: `Opened ${url} in browser` };
|
|
867
|
+
}
|
|
868
|
+
catch (error) {
|
|
869
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
870
|
+
return { success: false, output: `Failed to open browser: ${msg}` };
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Start a persistent HTTP server
|
|
875
|
+
*/
|
|
876
|
+
async executeStartServer(directory, port = 8080) {
|
|
877
|
+
try {
|
|
878
|
+
// First, check if something is already running on this port
|
|
879
|
+
try {
|
|
880
|
+
const existing = execSync(`lsof -i :${port} -t`, { encoding: 'utf8' }).trim();
|
|
881
|
+
if (existing) {
|
|
882
|
+
return {
|
|
883
|
+
success: true,
|
|
884
|
+
output: `Server already running on port ${port}. URL: http://localhost:${port}`
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
catch {
|
|
889
|
+
// No process on port, good to start
|
|
890
|
+
}
|
|
891
|
+
// Verify directory exists
|
|
892
|
+
if (!existsSync(directory)) {
|
|
893
|
+
return { success: false, output: `Directory not found: ${directory}` };
|
|
894
|
+
}
|
|
895
|
+
// Start server with nohup so it persists
|
|
896
|
+
const cmd = `cd "${directory}" && nohup python3 -m http.server ${port} > /dev/null 2>&1 &`;
|
|
897
|
+
execSync(cmd, { shell: '/bin/bash' });
|
|
898
|
+
// Wait a moment for server to start
|
|
899
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
900
|
+
// Verify it started
|
|
901
|
+
try {
|
|
902
|
+
execSync(`lsof -i :${port} -t`, { encoding: 'utf8' });
|
|
903
|
+
console.log(`[ORCHESTRATOR] Started server on port ${port} for ${directory}`);
|
|
904
|
+
return {
|
|
905
|
+
success: true,
|
|
906
|
+
output: `Started HTTP server on http://localhost:${port} serving ${directory}`
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
catch {
|
|
910
|
+
return { success: false, output: `Server failed to start on port ${port}` };
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
catch (error) {
|
|
914
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
915
|
+
return { success: false, output: `Failed to start server: ${msg}` };
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Stop a running server on a port
|
|
920
|
+
*/
|
|
921
|
+
async executeStopServer(port) {
|
|
922
|
+
try {
|
|
923
|
+
const pid = execSync(`lsof -i :${port} -t`, { encoding: 'utf8' }).trim();
|
|
924
|
+
if (pid) {
|
|
925
|
+
execSync(`kill ${pid}`);
|
|
926
|
+
console.log(`[ORCHESTRATOR] Stopped server on port ${port} (PID: ${pid})`);
|
|
927
|
+
return { success: true, output: `Stopped server on port ${port}` };
|
|
928
|
+
}
|
|
929
|
+
return { success: false, output: `No server running on port ${port}` };
|
|
930
|
+
}
|
|
931
|
+
catch {
|
|
932
|
+
return { success: false, output: `No server running on port ${port}` };
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Format tool preview for user
|
|
937
|
+
*/
|
|
938
|
+
formatToolPreview(name, input) {
|
|
939
|
+
// Only show previews in verbose mode
|
|
940
|
+
if (!this.isVerbose()) {
|
|
941
|
+
// Always show these regardless of verbose mode
|
|
942
|
+
if (name === 'delegate_to_worker' || name === 'spawn_worker') {
|
|
943
|
+
return `Working on: ${(input.task || '').slice(0, 60)}...`;
|
|
944
|
+
}
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
switch (name) {
|
|
948
|
+
case 'bash':
|
|
949
|
+
return `Running: ${input.command.slice(0, 60)}`;
|
|
950
|
+
case 'web_search':
|
|
951
|
+
return `Searching web: ${input.query}`;
|
|
952
|
+
case 'fetch_url':
|
|
953
|
+
return `Fetching: ${input.url}`;
|
|
954
|
+
case 'spawn_worker':
|
|
955
|
+
return `Spawning worker: ${input.task.slice(0, 60)}...`;
|
|
956
|
+
case 'check_worker':
|
|
957
|
+
return `Checking worker: ${input.job_id}`;
|
|
958
|
+
case 'wait_worker':
|
|
959
|
+
return `Waiting for worker: ${input.job_id}`;
|
|
960
|
+
case 'list_workers':
|
|
961
|
+
return 'Listing workers...';
|
|
962
|
+
case 'cancel_worker':
|
|
963
|
+
return `Canceling worker: ${input.job_id}`;
|
|
964
|
+
case 'delegate_to_worker':
|
|
965
|
+
return `Delegating: ${input.task.slice(0, 60)}...`;
|
|
966
|
+
case 'remember':
|
|
967
|
+
return `Remembering: ${input.content.slice(0, 50)}...`;
|
|
968
|
+
case 'search_memory':
|
|
969
|
+
return `Searching memory: ${input.query}`;
|
|
970
|
+
case 'schedule_task':
|
|
971
|
+
return `Scheduling: ${input.task} for ${input.when}`;
|
|
972
|
+
case 'open_browser':
|
|
973
|
+
return `Opening browser: ${input.url}`;
|
|
974
|
+
case 'start_local_server':
|
|
975
|
+
return `Starting server on port ${input.port || 8080}...`;
|
|
976
|
+
case 'stop_local_server':
|
|
977
|
+
return `Stopping server on port ${input.port}...`;
|
|
978
|
+
default:
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
hasToolUse(content) {
|
|
983
|
+
return content.some(block => block.type === 'tool_use');
|
|
984
|
+
}
|
|
985
|
+
extractText(content) {
|
|
986
|
+
const text = content
|
|
987
|
+
.filter(block => block.type === 'text')
|
|
988
|
+
.map(block => block.text)
|
|
989
|
+
.join('\n')
|
|
990
|
+
.trim();
|
|
991
|
+
// Never return empty - provide fallback
|
|
992
|
+
return text || 'Task completed.';
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Shutdown cleanly
|
|
996
|
+
*/
|
|
997
|
+
shutdown() {
|
|
998
|
+
this.scheduler.shutdown();
|
|
999
|
+
this.memory.close();
|
|
1000
|
+
}
|
|
1001
|
+
}
|