@siftd/connect-agent 0.2.14 → 0.2.16

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 CHANGED
@@ -211,6 +211,19 @@ export async function runAgent(pollInterval = 2000) {
211
211
  // Try WebSocket first
212
212
  wsClient = new AgentWebSocket();
213
213
  const wsConnected = await wsClient.connect();
214
+ // Always set up worker status callback for progress bars (works with or without WebSocket)
215
+ if (orchestrator) {
216
+ orchestrator.setWorkerStatusCallback((workers) => {
217
+ if (wsClient?.connected()) {
218
+ wsClient.sendWorkersUpdate(workers);
219
+ }
220
+ // Log running workers count for visibility even without WebSocket
221
+ const running = workers.filter(w => w.status === 'running');
222
+ if (running.length > 0) {
223
+ console.log(`[WORKERS] ${running.length} running: ${running.map(w => `${w.id} (${w.progress}%)`).join(', ')}`);
224
+ }
225
+ });
226
+ }
214
227
  if (wsConnected) {
215
228
  console.log('[AGENT] Using WebSocket for real-time communication\n');
216
229
  // Handle messages via WebSocket
package/dist/heartbeat.js CHANGED
@@ -10,7 +10,7 @@ import { hostname } from 'os';
10
10
  import { createHash } from 'crypto';
11
11
  import { getServerUrl, getAgentToken, getUserId, isCloudMode } from './config.js';
12
12
  const HEARTBEAT_INTERVAL = 10000; // 10 seconds
13
- const VERSION = '0.2.14'; // Should match package.json
13
+ const VERSION = '0.2.16'; // Should match package.json
14
14
  const state = {
15
15
  intervalId: null,
16
16
  runnerId: null,
@@ -6,6 +6,15 @@
6
6
  */
7
7
  import type { MessageParam } from '@anthropic-ai/sdk/resources/messages';
8
8
  export type MessageSender = (message: string) => Promise<void>;
9
+ export interface WorkerStatus {
10
+ id: string;
11
+ task: string;
12
+ status: 'running' | 'completed' | 'failed' | 'timeout';
13
+ progress: number;
14
+ elapsed: number;
15
+ estimated: number;
16
+ }
17
+ export type WorkerStatusCallback = (workers: WorkerStatus[]) => void;
9
18
  export declare class MasterOrchestrator {
10
19
  private client;
11
20
  private model;
@@ -15,6 +24,8 @@ export declare class MasterOrchestrator {
15
24
  private indexer;
16
25
  private jobs;
17
26
  private jobCounter;
27
+ private workerStatusCallback;
28
+ private workerStatusInterval;
18
29
  private userId;
19
30
  private workspaceDir;
20
31
  private claudePath;
@@ -36,6 +47,22 @@ export declare class MasterOrchestrator {
36
47
  * Initialize the orchestrator - indexes filesystem on first call
37
48
  */
38
49
  initialize(): Promise<void>;
50
+ /**
51
+ * Set callback for worker status updates (for UI progress bars)
52
+ */
53
+ setWorkerStatusCallback(callback: WorkerStatusCallback | null): void;
54
+ /**
55
+ * Get current status of all workers (from both delegateToWorker and spawn_worker)
56
+ */
57
+ getWorkerStatus(): WorkerStatus[];
58
+ /**
59
+ * Broadcast worker status to callback
60
+ */
61
+ private broadcastWorkerStatus;
62
+ /**
63
+ * Estimate task duration based on content
64
+ */
65
+ private estimateTaskDuration;
39
66
  /**
40
67
  * Find the claude binary path
41
68
  */
@@ -76,6 +103,7 @@ export declare class MasterOrchestrator {
76
103
  private processToolCalls;
77
104
  /**
78
105
  * Delegate task to Claude Code CLI worker with retry logic
106
+ * @param timeoutMs - Timeout in milliseconds (default: 30 minutes, max: 60 minutes)
79
107
  */
80
108
  private delegateToWorker;
81
109
  /**
@@ -16,60 +16,63 @@ import { WebTools } from './tools/web.js';
16
16
  import { WorkerTools } from './tools/worker.js';
17
17
  import { SharedState } from './workers/shared-state.js';
18
18
  import { getKnowledgeForPrompt } from './genesis/index.js';
19
- 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.
19
+ const SYSTEM_PROMPT = `You are a MASTER ORCHESTRATOR - NOT a worker. You delegate ALL file/code work to Claude Code CLI workers.
20
20
 
21
- CRITICAL - READ THIS:
22
- The user is on a WEB BROWSER. They CANNOT run terminal commands. They CANNOT access a shell.
23
- YOU are their hands. When something needs to be done, YOU do it with your tools.
24
- NEVER say "run this command" or "you'll need to..." - the user CAN'T do that.
21
+ CRITICAL IDENTITY:
22
+ - You are the BRAIN, not the hands
23
+ - You ORCHESTRATE workers - you don't do the work yourself
24
+ - You REMEMBER everything using your memory tools
25
+ - You PLAN and COORDINATE, then delegate execution
25
26
 
26
- YOUR TOOLS (use them!):
27
- - bash: Run any shell command directly
28
- - web_search: Search the internet for information
29
- - fetch_url: Fetch and read web page content
30
- - spawn_worker: Spawn background Claude Code workers for complex multi-step tasks
31
- - delegate_to_worker: Delegate complex tasks that need full CLI autonomy
32
- - remember/search_memory: Persistent memory across conversations
33
- - start_local_server/open_browser: Serve and preview web projects
27
+ STRICT TOOL USAGE RULES:
34
28
 
35
- WHEN TO USE WHAT:
36
- - Simple commands (ls, cat, git status, npm install) use bash directly
37
- - Web research use web_search + fetch_url
38
- - Complex multi-file changes, builds, deployments → spawn_worker or delegate_to_worker
39
- - Quick checks or reads → bash
40
- - Long-running background tasks → spawn_worker (non-blocking)
29
+ USE bash ONLY for READ-ONLY operations:
30
+ - ls, pwd, cat, head, tail, find, which, echo, date
31
+ - git status, git log, git diff (viewing only)
32
+ - Checking if files/directories exist
41
33
 
42
- FILESYSTEM AWARENESS:
43
- You know the user's filesystem. Their projects, directories, and files are indexed in your memory.
44
- When they mention "that project", "the connect app", "downloads folder", etc. - SEARCH YOUR MEMORY.
45
- Use search_memory to find the actual paths before working.
34
+ NEVER use bash for:
35
+ - Creating files (echo >, touch, mkdir)
36
+ - Editing files (sed, awk, tee)
37
+ - Moving/copying/deleting (mv, cp, rm)
38
+ - Installing packages (npm install, pip install)
39
+ - Running builds or tests
40
+ - ANY file modification whatsoever
46
41
 
47
- WHO YOU ARE:
48
- 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.
42
+ USE delegate_to_worker or spawn_worker for:
43
+ - Creating, editing, or deleting ANY files
44
+ - Running npm/pip/cargo install
45
+ - Building, testing, deploying
46
+ - Complex multi-step tasks
47
+ - ANY filesystem modification
49
48
 
50
- WHAT YOU CAN DO:
51
- - Run shell commands directly with bash
52
- - Search the web and fetch URLs
53
- - Dispatch Claude Code workers for complex work
54
- - Start local servers and open browsers
55
- - Remember things across conversations
56
- - Schedule tasks for later
57
- - Have real conversations
49
+ MEMORY IS MANDATORY:
50
+ - ALWAYS use search_memory before starting work
51
+ - ALWAYS use remember after significant actions
52
+ - Log your orchestration decisions to memory
53
+ - Learn from failures - remember what went wrong
58
54
 
59
- HOW TO TALK:
60
- - Be yourself - warm, direct, helpful
61
- - Keep it conversational
62
- - Show you remember them
63
- - Celebrate progress - "Nice, that's done!" not "Task completed successfully."
64
- - Be honest about what you're doing
65
- - If something fails, try a different approach or explain what happened
55
+ WORKER ORCHESTRATION:
56
+ - spawn_worker: For parallel/background tasks (non-blocking)
57
+ - delegate_to_worker: For sequential tasks needing full autonomy
58
+ - ALWAYS include logging instructions in worker prompts
59
+ - Tell workers to save their logs to /tmp/worker-{job_id}.log
60
+
61
+ AFTER EVERY SIGNIFICANT ACTION:
62
+ 1. Log it to memory with remember
63
+ 2. Note what worked or failed
64
+ 3. Update your understanding of the user's system
65
+
66
+ WHO YOU ARE:
67
+ Warm, helpful orchestrator. You coordinate, remember, and delegate. You're the persistent layer that makes AI feel personal - but you do it by managing workers, not by doing tasks yourself.
66
68
 
67
69
  SLASH COMMANDS:
68
70
  - /reset - Clear conversation and memory context
69
- - /verbose - Toggle showing tool execution details
71
+ - /mode - Show current mode and capabilities
72
+ - /memory - Show memory stats
70
73
  - /help - Show available commands
71
74
 
72
- You're the persistent layer that makes AI feel personal. You ACT on behalf of the user - you don't give them homework.`;
75
+ You ACT through workers. You REMEMBER through memory. You NEVER do file operations directly.`;
73
76
  export class MasterOrchestrator {
74
77
  client;
75
78
  model;
@@ -79,6 +82,8 @@ export class MasterOrchestrator {
79
82
  indexer;
80
83
  jobs = new Map();
81
84
  jobCounter = 0;
85
+ workerStatusCallback = null;
86
+ workerStatusInterval = null;
82
87
  userId;
83
88
  workspaceDir;
84
89
  claudePath;
@@ -140,6 +145,128 @@ export class MasterOrchestrator {
140
145
  }
141
146
  this.initialized = true;
142
147
  }
148
+ /**
149
+ * Set callback for worker status updates (for UI progress bars)
150
+ */
151
+ setWorkerStatusCallback(callback) {
152
+ this.workerStatusCallback = callback;
153
+ if (callback) {
154
+ // Start broadcasting status every 500ms when workers are running
155
+ if (!this.workerStatusInterval) {
156
+ this.workerStatusInterval = setInterval(() => {
157
+ this.broadcastWorkerStatus();
158
+ }, 500);
159
+ }
160
+ }
161
+ else {
162
+ // Stop broadcasting
163
+ if (this.workerStatusInterval) {
164
+ clearInterval(this.workerStatusInterval);
165
+ this.workerStatusInterval = null;
166
+ }
167
+ }
168
+ }
169
+ /**
170
+ * Get current status of all workers (from both delegateToWorker and spawn_worker)
171
+ */
172
+ getWorkerStatus() {
173
+ const now = Date.now();
174
+ const workers = [];
175
+ // Include jobs from delegateToWorker (this.jobs)
176
+ for (const [id, job] of this.jobs) {
177
+ const elapsed = (now - job.startTime) / 1000;
178
+ const estimated = job.estimatedTime;
179
+ // Calculate progress: cap at 90% while running, 100% when done
180
+ let progress;
181
+ if (job.status === 'completed') {
182
+ progress = 100;
183
+ }
184
+ else if (job.status === 'failed' || job.status === 'timeout') {
185
+ progress = 100; // Show as complete even on failure
186
+ }
187
+ else {
188
+ progress = Math.min(90, (elapsed / estimated) * 100);
189
+ }
190
+ workers.push({
191
+ id,
192
+ task: job.task.slice(0, 60),
193
+ status: job.status,
194
+ progress: Math.round(progress),
195
+ elapsed: Math.round(elapsed),
196
+ estimated: Math.round(estimated)
197
+ });
198
+ }
199
+ // Also include jobs from spawn_worker (workerTools)
200
+ try {
201
+ const spawnedWorkers = this.workerTools.getRunningWorkers();
202
+ for (const worker of spawnedWorkers) {
203
+ // Don't duplicate if already in this.jobs
204
+ if (workers.some(w => w.id === worker.id))
205
+ continue;
206
+ const elapsed = (now - worker.startTime) / 1000;
207
+ const estimated = worker.estimatedTime;
208
+ const progress = Math.min(90, (elapsed / estimated) * 100);
209
+ workers.push({
210
+ id: worker.id,
211
+ task: worker.task.slice(0, 60),
212
+ status: worker.status,
213
+ progress: Math.round(progress),
214
+ elapsed: Math.round(elapsed),
215
+ estimated: Math.round(estimated)
216
+ });
217
+ }
218
+ }
219
+ catch (error) {
220
+ // WorkerTools not available, skip
221
+ }
222
+ return workers;
223
+ }
224
+ /**
225
+ * Broadcast worker status to callback
226
+ */
227
+ broadcastWorkerStatus() {
228
+ if (!this.workerStatusCallback)
229
+ return;
230
+ const workers = this.getWorkerStatus();
231
+ // Only broadcast if there are running workers
232
+ const hasRunning = workers.some(w => w.status === 'running');
233
+ if (hasRunning || workers.length > 0) {
234
+ this.workerStatusCallback(workers);
235
+ }
236
+ // Clean up completed jobs after 3 seconds
237
+ const now = Date.now();
238
+ for (const [id, job] of this.jobs) {
239
+ if (job.status !== 'running' && job.endTime && (now - job.endTime) > 3000) {
240
+ this.jobs.delete(id);
241
+ }
242
+ }
243
+ }
244
+ /**
245
+ * Estimate task duration based on content
246
+ */
247
+ estimateTaskDuration(task) {
248
+ const lower = task.toLowerCase();
249
+ // Complex tasks: 3 minutes
250
+ if (lower.includes('refactor') || lower.includes('implement') ||
251
+ lower.includes('create') || lower.includes('build') ||
252
+ lower.includes('write tests') || lower.includes('add tests')) {
253
+ return 180;
254
+ }
255
+ // Medium tasks: 90 seconds
256
+ if (lower.includes('fix') || lower.includes('update') ||
257
+ lower.includes('modify') || lower.includes('change') ||
258
+ lower.includes('add') || lower.includes('install')) {
259
+ return 90;
260
+ }
261
+ // Simple tasks: 45 seconds
262
+ if (lower.includes('find') || lower.includes('search') ||
263
+ lower.includes('read') || lower.includes('list') ||
264
+ lower.includes('check') || lower.includes('show')) {
265
+ return 45;
266
+ }
267
+ // Default: 60 seconds
268
+ return 60;
269
+ }
143
270
  /**
144
271
  * Find the claude binary path
145
272
  */
@@ -301,16 +428,21 @@ export class MasterOrchestrator {
301
428
  */
302
429
  getToolDefinitions() {
303
430
  return [
304
- // Direct bash execution - for simple commands
431
+ // Direct bash execution - READ-ONLY OPERATIONS ONLY
305
432
  {
306
433
  name: 'bash',
307
- 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.',
434
+ description: `Execute a READ-ONLY shell command. ONLY use for viewing/checking - NEVER for modifications.
435
+
436
+ ALLOWED: ls, pwd, cat, head, tail, find, which, echo, date, git status, git log, git diff, test -f, test -d
437
+ FORBIDDEN: touch, mkdir, rm, mv, cp, echo >, tee, sed -i, npm install, pip install, ANY file creation/modification
438
+
439
+ For ANY file creation, editing, or system modification, use delegate_to_worker instead.`,
308
440
  input_schema: {
309
441
  type: 'object',
310
442
  properties: {
311
443
  command: {
312
444
  type: 'string',
313
- description: 'The bash command to execute'
445
+ description: 'READ-ONLY bash command (no file modifications allowed)'
314
446
  },
315
447
  timeout: {
316
448
  type: 'number',
@@ -698,9 +830,12 @@ Be specific about what you want done.`,
698
830
  }
699
831
  /**
700
832
  * Delegate task to Claude Code CLI worker with retry logic
833
+ * @param timeoutMs - Timeout in milliseconds (default: 30 minutes, max: 60 minutes)
701
834
  */
702
- async delegateToWorker(task, context, workingDir, retryCount = 0) {
835
+ async delegateToWorker(task, context, workingDir, retryCount = 0, timeoutMs) {
703
836
  const maxRetries = 2;
837
+ // Default 30 min, max 60 min
838
+ const workerTimeout = Math.min(timeoutMs || 30 * 60 * 1000, 60 * 60 * 1000);
704
839
  const id = `worker_${Date.now()}_${++this.jobCounter}`;
705
840
  const cwd = workingDir || this.workspaceDir;
706
841
  // Search for relevant memories to inject into worker prompt
@@ -757,13 +892,16 @@ If you need to share data with other workers or signal completion:
757
892
  [MESSAGE] to=worker_xyz | content=Please review the changes I made
758
893
  This enables parallel workers to coordinate.`;
759
894
  console.log(`[ORCHESTRATOR] Delegating to worker ${id}: ${task.slice(0, 80)}...`);
895
+ // Estimate task duration based on content
896
+ const estimatedTime = this.estimateTaskDuration(task);
760
897
  return new Promise((resolve) => {
761
898
  const job = {
762
899
  id,
763
900
  task: task.slice(0, 200),
764
901
  status: 'running',
765
902
  startTime: Date.now(),
766
- output: ''
903
+ output: '',
904
+ estimatedTime
767
905
  };
768
906
  // Escape single quotes in prompt for shell safety
769
907
  // Replace ' with '\'' (end quote, escaped quote, start quote)
@@ -777,8 +915,8 @@ This enables parallel workers to coordinate.`;
777
915
  });
778
916
  job.process = child;
779
917
  this.jobs.set(id, job);
780
- // Timeout after 10 minutes (increased from 5 for complex tasks)
781
- const WORKER_TIMEOUT = 10 * 60 * 1000;
918
+ // Configurable timeout (default 30 min, max 60 min)
919
+ const timeoutMinutes = Math.round(workerTimeout / 60000);
782
920
  const timeout = setTimeout(() => {
783
921
  if (job.status === 'running') {
784
922
  job.status = 'timeout';
@@ -792,14 +930,16 @@ This enables parallel workers to coordinate.`;
792
930
  }
793
931
  }, 5000);
794
932
  const partialOutput = job.output.trim();
933
+ // Check for log file that worker should have created
934
+ const logFile = `/tmp/worker-${id}-log.txt`;
795
935
  resolve({
796
936
  success: false,
797
937
  output: partialOutput
798
- ? `Worker timed out after 10 minutes. Partial findings:\n${partialOutput.slice(-2000)}`
799
- : 'Worker timed out after 10 minutes with no output. The task may be too complex - try breaking it into smaller steps.'
938
+ ? `Worker timed out after ${timeoutMinutes} minutes. Check ${logFile} for full logs. Partial findings:\n${partialOutput.slice(-3000)}`
939
+ : `Worker timed out after ${timeoutMinutes} minutes with no output. Check ${logFile} for any saved progress.`
800
940
  });
801
941
  }
802
- }, WORKER_TIMEOUT);
942
+ }, workerTimeout);
803
943
  child.stdout?.on('data', (data) => {
804
944
  job.output += data.toString();
805
945
  });
@@ -1141,6 +1281,11 @@ This enables parallel workers to coordinate.`;
1141
1281
  * Shutdown cleanly
1142
1282
  */
1143
1283
  async shutdown() {
1284
+ // Stop worker status broadcasting
1285
+ if (this.workerStatusInterval) {
1286
+ clearInterval(this.workerStatusInterval);
1287
+ this.workerStatusInterval = null;
1288
+ }
1144
1289
  this.scheduler.shutdown();
1145
1290
  if (this.memory instanceof PostgresMemoryStore) {
1146
1291
  await this.memory.close();
@@ -33,4 +33,18 @@ export declare class WorkerTools {
33
33
  * Clean up old jobs
34
34
  */
35
35
  cleanupWorkers(): Promise<ToolResult>;
36
+ /**
37
+ * Get all running workers for progress tracking
38
+ */
39
+ getRunningWorkers(): Array<{
40
+ id: string;
41
+ task: string;
42
+ status: string;
43
+ startTime: number;
44
+ estimatedTime: number;
45
+ }>;
46
+ /**
47
+ * Estimate task duration based on content
48
+ */
49
+ private estimateTaskDuration;
36
50
  }
@@ -146,4 +146,37 @@ export class WorkerTools {
146
146
  output: `Cleaned up ${cleaned} old job(s).`
147
147
  };
148
148
  }
149
+ /**
150
+ * Get all running workers for progress tracking
151
+ */
152
+ getRunningWorkers() {
153
+ const jobs = this.manager.list({ status: 'running' });
154
+ return jobs.map(job => ({
155
+ id: job.id,
156
+ task: job.task,
157
+ status: job.status,
158
+ startTime: job.started ? new Date(job.started).getTime() : Date.now(),
159
+ // Estimate based on task content
160
+ estimatedTime: this.estimateTaskDuration(job.task)
161
+ }));
162
+ }
163
+ /**
164
+ * Estimate task duration based on content
165
+ */
166
+ estimateTaskDuration(task) {
167
+ const lower = task.toLowerCase();
168
+ if (lower.includes('refactor') || lower.includes('implement') ||
169
+ lower.includes('create') || lower.includes('build')) {
170
+ return 180; // 3 minutes
171
+ }
172
+ if (lower.includes('fix') || lower.includes('update') ||
173
+ lower.includes('modify') || lower.includes('add')) {
174
+ return 90; // 90 seconds
175
+ }
176
+ if (lower.includes('find') || lower.includes('search') ||
177
+ lower.includes('read') || lower.includes('list')) {
178
+ return 45; // 45 seconds
179
+ }
180
+ return 60; // default 1 minute
181
+ }
149
182
  }
@@ -6,6 +6,7 @@
6
6
  * - Streams responses as they're generated
7
7
  * - Supports interruption and progress updates
8
8
  */
9
+ import type { WorkerStatus } from './orchestrator.js';
9
10
  export type MessageHandler = (message: WebSocketMessage) => Promise<void>;
10
11
  export type StreamHandler = (chunk: string) => void;
11
12
  export interface WebSocketMessage {
@@ -57,6 +58,10 @@ export declare class AgentWebSocket {
57
58
  * Send typing indicator
58
59
  */
59
60
  sendTyping(isTyping: boolean): void;
61
+ /**
62
+ * Send workers status update for progress bars
63
+ */
64
+ sendWorkersUpdate(workers: WorkerStatus[]): void;
60
65
  /**
61
66
  * Check if connected
62
67
  */
package/dist/websocket.js CHANGED
@@ -159,6 +159,15 @@ export class AgentWebSocket {
159
159
  isTyping
160
160
  });
161
161
  }
162
+ /**
163
+ * Send workers status update for progress bars
164
+ */
165
+ sendWorkersUpdate(workers) {
166
+ this.sendToServer({
167
+ type: 'workers_update',
168
+ workers
169
+ });
170
+ }
162
171
  /**
163
172
  * Check if connected
164
173
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siftd/connect-agent",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
4
4
  "description": "Master orchestrator agent - control Claude Code remotely via web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",