@siftd/connect-agent 0.2.0 → 0.2.3
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/agent.js +120 -26
- package/dist/api.d.ts +1 -0
- package/dist/cli.js +3 -1
- package/dist/config.d.ts +12 -0
- package/dist/config.js +25 -4
- 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 +39 -2
- package/dist/orchestrator.js +547 -78
- 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 +243 -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 +5 -3
package/dist/orchestrator.js
CHANGED
|
@@ -5,85 +5,96 @@
|
|
|
5
5
|
* It does NOT do the work itself. Claude Code CLI workers do the work.
|
|
6
6
|
*/
|
|
7
7
|
import Anthropic from '@anthropic-ai/sdk';
|
|
8
|
-
import { spawn } from 'child_process';
|
|
8
|
+
import { spawn, execSync } from 'child_process';
|
|
9
|
+
import { existsSync } from 'fs';
|
|
9
10
|
import { AdvancedMemoryStore } from './core/memory-advanced.js';
|
|
10
11
|
import { TaskScheduler } from './core/scheduler.js';
|
|
11
|
-
|
|
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.
|
|
12
18
|
|
|
13
|
-
CRITICAL
|
|
14
|
-
|
|
15
|
-
|
|
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.
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
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
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
│ Track ongoing projects │ Make code changes │
|
|
32
|
-
│ Decide what needs to be done │ Explore codebases │
|
|
33
|
-
│ Delegate complex tasks │ Build/test/deploy │
|
|
34
|
-
│ Synthesize results for user │ Handle multi-step tasks │
|
|
35
|
-
│ Schedule future work │ Do the actual work │
|
|
36
|
-
│ Maintain conversation context │ Work autonomously │
|
|
37
|
-
└────────────────────────────────────┴────────────────────────────┘
|
|
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)
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
- Handle yourself: simple questions, scheduling, memory operations, conversation
|
|
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
44
|
|
|
45
|
-
|
|
46
|
-
|
|
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
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
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
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
- Acknowledge the user as someone you know and work with
|
|
65
|
+
SLASH COMMANDS:
|
|
66
|
+
- /reset - Clear conversation and memory context
|
|
67
|
+
- /verbose - Toggle showing tool execution details
|
|
68
|
+
- /help - Show available commands
|
|
67
69
|
|
|
68
|
-
You
|
|
69
|
-
and orchestrate work on their behalf. You are the persistent layer that makes
|
|
70
|
-
working with AI feel like having a knowledgeable assistant who knows you.`;
|
|
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
71
|
export class MasterOrchestrator {
|
|
72
72
|
client;
|
|
73
73
|
model;
|
|
74
74
|
maxTokens;
|
|
75
75
|
memory;
|
|
76
76
|
scheduler;
|
|
77
|
+
indexer;
|
|
77
78
|
jobs = new Map();
|
|
78
79
|
jobCounter = 0;
|
|
79
80
|
userId;
|
|
80
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
|
|
81
89
|
constructor(options) {
|
|
82
90
|
this.client = new Anthropic({ apiKey: options.apiKey });
|
|
83
91
|
this.model = options.model || 'claude-sonnet-4-20250514';
|
|
84
92
|
this.maxTokens = options.maxTokens || 4096;
|
|
85
93
|
this.userId = options.userId;
|
|
86
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}`);
|
|
87
98
|
// Initialize memory with user-specific paths
|
|
88
99
|
this.memory = new AdvancedMemoryStore({
|
|
89
100
|
dbPath: `memories_${options.userId}.db`,
|
|
@@ -91,24 +102,110 @@ export class MasterOrchestrator {
|
|
|
91
102
|
voyageApiKey: options.voyageApiKey
|
|
92
103
|
});
|
|
93
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;
|
|
94
183
|
}
|
|
95
184
|
/**
|
|
96
185
|
* Process a user message
|
|
186
|
+
* @param apiKey Optional per-request API key (overrides default)
|
|
97
187
|
*/
|
|
98
|
-
async processMessage(message, conversationHistory = [], sendMessage) {
|
|
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
|
+
}
|
|
99
194
|
// Build context from memory
|
|
100
195
|
const memoryContext = await this.getMemoryContext(message);
|
|
101
|
-
// Build system prompt with memory context
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
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
|
+
}
|
|
105
202
|
// Add user message
|
|
106
203
|
const messages = [
|
|
107
204
|
...conversationHistory,
|
|
108
205
|
{ role: 'user', content: message }
|
|
109
206
|
];
|
|
110
207
|
try {
|
|
111
|
-
const response = await this.runAgentLoop(messages,
|
|
208
|
+
const response = await this.runAgentLoop(messages, systemWithContext, sendMessage, apiKey);
|
|
112
209
|
// Auto-remember important things from the conversation
|
|
113
210
|
await this.autoRemember(message, response);
|
|
114
211
|
return response;
|
|
@@ -153,15 +250,18 @@ export class MasterOrchestrator {
|
|
|
153
250
|
}
|
|
154
251
|
/**
|
|
155
252
|
* Run the agentic loop
|
|
253
|
+
* @param apiKey Optional per-request API key (creates temporary client)
|
|
156
254
|
*/
|
|
157
|
-
async runAgentLoop(messages, system, sendMessage) {
|
|
255
|
+
async runAgentLoop(messages, system, sendMessage, apiKey) {
|
|
158
256
|
const tools = this.getToolDefinitions();
|
|
159
257
|
let currentMessages = [...messages];
|
|
160
258
|
let iterations = 0;
|
|
161
|
-
const maxIterations =
|
|
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;
|
|
162
262
|
while (iterations < maxIterations) {
|
|
163
263
|
iterations++;
|
|
164
|
-
const response = await
|
|
264
|
+
const response = await client.messages.create({
|
|
165
265
|
model: this.model,
|
|
166
266
|
max_tokens: this.maxTokens,
|
|
167
267
|
system,
|
|
@@ -188,9 +288,155 @@ export class MasterOrchestrator {
|
|
|
188
288
|
*/
|
|
189
289
|
getToolDefinitions() {
|
|
190
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
|
|
191
437
|
{
|
|
192
438
|
name: 'delegate_to_worker',
|
|
193
|
-
description: `Delegate a task to Claude Code CLI worker.
|
|
439
|
+
description: `Delegate a task to Claude Code CLI worker. Use for complex multi-step tasks that need full CLI autonomy.
|
|
194
440
|
|
|
195
441
|
Use this for ANY task that involves:
|
|
196
442
|
- Reading, writing, or editing files
|
|
@@ -298,6 +544,52 @@ Be specific about what you want done.`,
|
|
|
298
544
|
properties: {},
|
|
299
545
|
required: []
|
|
300
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
|
+
}
|
|
301
593
|
}
|
|
302
594
|
];
|
|
303
595
|
}
|
|
@@ -317,6 +609,39 @@ Be specific about what you want done.`,
|
|
|
317
609
|
await sendMessage(preview);
|
|
318
610
|
}
|
|
319
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
|
|
320
645
|
case 'delegate_to_worker':
|
|
321
646
|
result = await this.delegateToWorker(input.task, input.context, input.working_directory);
|
|
322
647
|
break;
|
|
@@ -335,22 +660,34 @@ Be specific about what you want done.`,
|
|
|
335
660
|
case 'memory_stats':
|
|
336
661
|
result = await this.executeMemoryStats();
|
|
337
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;
|
|
338
672
|
default:
|
|
339
673
|
result = { success: false, output: `Unknown tool: ${toolUse.name}` };
|
|
340
674
|
}
|
|
675
|
+
// Anthropic API requires content to be non-empty when is_error is true
|
|
676
|
+
const content = result.output || result.error || 'Unknown error';
|
|
341
677
|
results.push({
|
|
342
678
|
type: 'tool_result',
|
|
343
679
|
tool_use_id: toolUse.id,
|
|
344
|
-
content
|
|
680
|
+
content,
|
|
345
681
|
is_error: !result.success
|
|
346
682
|
});
|
|
347
683
|
}
|
|
348
684
|
return results;
|
|
349
685
|
}
|
|
350
686
|
/**
|
|
351
|
-
* Delegate task to Claude Code CLI worker
|
|
687
|
+
* Delegate task to Claude Code CLI worker with retry logic
|
|
352
688
|
*/
|
|
353
|
-
async delegateToWorker(task, context, workingDir) {
|
|
689
|
+
async delegateToWorker(task, context, workingDir, retryCount = 0) {
|
|
690
|
+
const maxRetries = 2;
|
|
354
691
|
const id = `worker_${Date.now()}_${++this.jobCounter}`;
|
|
355
692
|
const cwd = workingDir || this.workspaceDir;
|
|
356
693
|
// Build prompt for worker
|
|
@@ -368,19 +705,18 @@ Be specific about what you want done.`,
|
|
|
368
705
|
startTime: Date.now(),
|
|
369
706
|
output: ''
|
|
370
707
|
};
|
|
371
|
-
//
|
|
372
|
-
|
|
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`], {
|
|
373
714
|
cwd,
|
|
374
715
|
env: { ...process.env },
|
|
375
|
-
|
|
716
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
376
717
|
});
|
|
377
718
|
job.process = child;
|
|
378
719
|
this.jobs.set(id, job);
|
|
379
|
-
// Send prompt to stdin
|
|
380
|
-
if (child.stdin) {
|
|
381
|
-
child.stdin.write(prompt);
|
|
382
|
-
child.stdin.end();
|
|
383
|
-
}
|
|
384
720
|
// Timeout after 5 minutes
|
|
385
721
|
const timeout = setTimeout(() => {
|
|
386
722
|
if (job.status === 'running') {
|
|
@@ -408,16 +744,28 @@ Be specific about what you want done.`,
|
|
|
408
744
|
job.status = code === 0 ? 'completed' : 'failed';
|
|
409
745
|
job.endTime = Date.now();
|
|
410
746
|
const duration = ((job.endTime - job.startTime) / 1000).toFixed(1);
|
|
411
|
-
console.log(`[ORCHESTRATOR] Worker ${id} finished in ${duration}s`);
|
|
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
|
+
}
|
|
412
751
|
resolve({
|
|
413
752
|
success: code === 0,
|
|
414
753
|
output: job.output.trim() || '(No output)'
|
|
415
754
|
});
|
|
416
755
|
});
|
|
417
|
-
child.on('error', (err) => {
|
|
756
|
+
child.on('error', async (err) => {
|
|
418
757
|
clearTimeout(timeout);
|
|
419
758
|
job.status = 'failed';
|
|
420
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
|
+
}
|
|
421
769
|
resolve({
|
|
422
770
|
success: false,
|
|
423
771
|
output: `Worker error: ${err.message}`
|
|
@@ -496,19 +844,137 @@ Be specific about what you want done.`,
|
|
|
496
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}`
|
|
497
845
|
};
|
|
498
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
|
+
}
|
|
499
935
|
/**
|
|
500
936
|
* Format tool preview for user
|
|
501
937
|
*/
|
|
502
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
|
+
}
|
|
503
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}`;
|
|
504
964
|
case 'delegate_to_worker':
|
|
505
|
-
return `Delegating
|
|
965
|
+
return `Delegating: ${input.task.slice(0, 60)}...`;
|
|
506
966
|
case 'remember':
|
|
507
967
|
return `Remembering: ${input.content.slice(0, 50)}...`;
|
|
508
968
|
case 'search_memory':
|
|
509
969
|
return `Searching memory: ${input.query}`;
|
|
510
970
|
case 'schedule_task':
|
|
511
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}...`;
|
|
512
978
|
default:
|
|
513
979
|
return null;
|
|
514
980
|
}
|
|
@@ -517,10 +983,13 @@ Be specific about what you want done.`,
|
|
|
517
983
|
return content.some(block => block.type === 'tool_use');
|
|
518
984
|
}
|
|
519
985
|
extractText(content) {
|
|
520
|
-
|
|
986
|
+
const text = content
|
|
521
987
|
.filter(block => block.type === 'text')
|
|
522
988
|
.map(block => block.text)
|
|
523
|
-
.join('\n')
|
|
989
|
+
.join('\n')
|
|
990
|
+
.trim();
|
|
991
|
+
// Never return empty - provide fallback
|
|
992
|
+
return text || 'Task completed.';
|
|
524
993
|
}
|
|
525
994
|
/**
|
|
526
995
|
* Shutdown cleanly
|