@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.
@@ -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
- const SYSTEM_PROMPT = `You are the MASTER ORCHESTRATOR - the user's intelligent assistant accessed via web browser.
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 IDENTITY:
14
- You are NOT a coding assistant. You are NOT a worker. You are the ORCHESTRATOR.
15
- Your job is to UNDERSTAND, REMEMBER, DELEGATE, and SYNTHESIZE.
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
- You have a powerful worker at your disposal: Claude Code CLI. It can:
18
- - Read, write, edit any file on the user's machine
19
- - Run any shell command
20
- - Explore codebases
21
- - Make multi-file changes
22
- - Run builds, tests, deployments
23
- - Do complex development work
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
- YOUR ROLE vs WORKER ROLE:
26
- ┌─────────────────────────────────────────────────────────────────┐
27
- YOU (Master Orchestrator) │ WORKER (Claude Code CLI) │
28
- ├────────────────────────────────────┼────────────────────────────┤
29
- Understand user intent │ Execute file operations │
30
- Remember user preferences │ Run shell commands │
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
- DELEGATION STRATEGY:
40
- - ALWAYS delegate: file operations, code changes, builds, tests, deployments
41
- - ALWAYS delegate: multi-step tasks, codebase exploration
42
- - ALWAYS delegate: anything requiring filesystem or shell access
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
- NEVER TRY TO DO THE WORK YOURSELF. You don't have filesystem access. You don't have shell access.
46
- Your only tools are: delegate_to_worker, remember, search_memory, schedule_task.
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
- WHEN YOU DELEGATE:
49
- 1. Be specific about what you want done
50
- 2. Include context about user preferences from memory
51
- 3. Tell the user you're delegating ("Let me have my worker handle that...")
52
- 4. Wait for results and synthesize them into a clear response
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
- MEMORY SYSTEM:
55
- You have persistent memory. Use it to:
56
- - Remember user preferences ("I prefer TypeScript", "I use pnpm")
57
- - Track ongoing projects and their status
58
- - Note user's communication style
59
- - Store important facts about the user's system/environment
60
- - Remember past conversations and decisions
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
- COMMUNICATION STYLE:
63
- - Be concise - this is a chat interface, not documentation
64
- - Be direct - say what you're doing
65
- - Be helpful - anticipate needs based on memory
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 are the user's intelligent agent - you remember them, understand their projects,
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 systemWithMemory = memoryContext
103
- ? `${SYSTEM_PROMPT}\n\nRELEVANT MEMORIES:\n${memoryContext}`
104
- : SYSTEM_PROMPT;
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, systemWithMemory, sendMessage);
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 = 15;
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 this.client.messages.create({
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. This is your PRIMARY tool.
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: result.output,
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
- // Spawn Claude Code CLI - use -p flag and pipe prompt to stdin
372
- const child = spawn('claude', ['-p'], {
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
- shell: false
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 to worker: ${input.task.slice(0, 80)}...`;
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
- return content
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