@siftd/connect-agent 0.2.18 → 0.2.20

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
@@ -8,110 +8,33 @@ import { startHeartbeat, stopHeartbeat, getHeartbeatState } from './heartbeat.js
8
8
  function stripAnsi(str) {
9
9
  return str.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
10
10
  }
11
- // Lock to prevent concurrent updates
12
- let updateInProgress = false;
13
11
  /**
14
- * Get the installed version of connect-agent
15
- */
16
- function getInstalledVersion() {
17
- try {
18
- const output = execSync('npm list -g @siftd/connect-agent --depth=0 2>/dev/null', {
19
- encoding: 'utf8',
20
- shell: '/bin/bash'
21
- });
22
- const match = output.match(/@siftd\/connect-agent@([\d.]+)/);
23
- return match ? match[1] : 'unknown';
24
- }
25
- catch {
26
- return 'unknown';
27
- }
28
- }
29
- /**
30
- * Get the latest published version from npm
31
- */
32
- function getLatestVersion() {
33
- try {
34
- const output = execSync('npm view @siftd/connect-agent version 2>/dev/null', {
35
- encoding: 'utf8',
36
- shell: '/bin/bash'
37
- });
38
- return output.trim();
39
- }
40
- catch {
41
- return 'unknown';
42
- }
43
- }
44
- /**
45
- * Actually perform a self-update - runs npm install and restarts
12
+ * Self-update: npm install latest and restart
13
+ * Called from webapp banner or update command
46
14
  */
47
15
  async function performSelfUpdate() {
48
- // Prevent concurrent updates
49
- if (updateInProgress) {
50
- return '⏳ Update already in progress. Please wait...';
51
- }
52
- updateInProgress = true;
53
- console.log('[AGENT] Starting self-update...');
16
+ console.log('[AGENT] === SELF-UPDATE STARTING ===');
54
17
  try {
55
- // Get current version
56
- const currentVersion = getInstalledVersion();
57
- console.log('[AGENT] Current version:', currentVersion);
58
- // Check latest version first
59
- const latestVersion = getLatestVersion();
60
- console.log('[AGENT] Latest available:', latestVersion);
61
- if (currentVersion === latestVersion && currentVersion !== 'unknown') {
62
- updateInProgress = false;
63
- return `✅ Already on latest version (${currentVersion})`;
64
- }
18
+ // Just do it - npm install latest
65
19
  console.log('[AGENT] Running: npm install -g @siftd/connect-agent@latest');
66
- // Actually run the npm install with more detailed error capture
67
- let installOutput;
68
- try {
69
- installOutput = execSync('npm install -g @siftd/connect-agent@latest 2>&1', {
70
- encoding: 'utf8',
71
- shell: '/bin/bash',
72
- timeout: 180000, // 3 minute timeout
73
- maxBuffer: 10 * 1024 * 1024 // 10MB buffer
74
- });
75
- }
76
- catch (installError) {
77
- const err = installError;
78
- const output = err.stdout || err.stderr || err.message || 'Unknown install error';
79
- console.error('[AGENT] npm install failed:', output);
80
- updateInProgress = false;
81
- // Parse common npm errors
82
- if (output.includes('EACCES') || output.includes('permission denied')) {
83
- return `❌ Permission denied. Try running:\nsudo npm install -g @siftd/connect-agent@latest`;
84
- }
85
- if (output.includes('ENOTFOUND') || output.includes('network')) {
86
- return `❌ Network error. Check your internet connection and try again.`;
87
- }
88
- if (output.includes('E404')) {
89
- return `❌ Package not found on npm. The package may have been unpublished.`;
90
- }
91
- return `❌ Update failed:\n${output.slice(0, 500)}\n\nRun manually:\nnpm install -g @siftd/connect-agent@latest`;
92
- }
93
- console.log('[AGENT] Install output:', installOutput.slice(0, 500));
94
- // Verify the update succeeded
95
- const newVersion = getInstalledVersion();
96
- console.log('[AGENT] New version:', newVersion);
97
- if (newVersion === currentVersion && currentVersion !== 'unknown') {
98
- updateInProgress = false;
99
- return `⚠️ Version unchanged (${currentVersion}). npm may have used cache.\n\nTry: npm cache clean --force && npm install -g @siftd/connect-agent@latest`;
100
- }
101
- // Schedule restart
102
- console.log('[AGENT] Scheduling restart in 3 seconds...');
20
+ execSync('npm install -g @siftd/connect-agent@latest', {
21
+ encoding: 'utf8',
22
+ shell: '/bin/bash',
23
+ stdio: 'inherit', // Show output in real-time
24
+ timeout: 180000
25
+ });
26
+ console.log('[AGENT] Update installed. Restarting in 2 seconds...');
27
+ // Restart the agent
103
28
  setTimeout(() => {
104
- console.log('[AGENT] Restarting...');
105
- updateInProgress = false;
106
- process.exit(0); // Exit - systemd/pm2/user will restart
107
- }, 3000);
108
- return `✅ Update complete!\n\n${currentVersion} → ${newVersion}\n\nRestarting agent in 3 seconds...`;
29
+ console.log('[AGENT] === RESTARTING ===');
30
+ process.exit(0);
31
+ }, 2000);
32
+ return '✅ Update installed. Restarting...';
109
33
  }
110
34
  catch (error) {
111
- updateInProgress = false;
112
- const errMsg = error instanceof Error ? error.message : String(error);
113
- console.error('[AGENT] Update failed:', errMsg);
114
- return `❌ Update failed: ${errMsg}\n\nYou may need to run manually:\nnpm install -g @siftd/connect-agent@latest`;
35
+ const msg = error instanceof Error ? error.message : String(error);
36
+ console.error('[AGENT] Update failed:', msg);
37
+ return `❌ Update failed: ${msg}`;
115
38
  }
116
39
  }
117
40
  // Conversation history for orchestrator mode
@@ -258,10 +181,9 @@ export async function processMessage(message) {
258
181
  );
259
182
  return response;
260
183
  }
261
- // Handle self-update requests - ACTUALLY run the update, don't just pretend
262
- if (content.includes('update') && content.includes('connect-agent') &&
263
- (content.includes('npm install') || content.includes('latest'))) {
264
- console.log('[AGENT] Self-update request detected - forcing actual execution');
184
+ // System command: force update (sent by webapp banner)
185
+ if (content === '/system-update') {
186
+ console.log('[AGENT] === SYSTEM UPDATE COMMAND ===');
265
187
  return await performSelfUpdate();
266
188
  }
267
189
  try {
@@ -323,16 +245,24 @@ export async function runAgent(pollInterval = 2000) {
323
245
  // Try WebSocket first
324
246
  wsClient = new AgentWebSocket();
325
247
  const wsConnected = await wsClient.connect();
326
- // Always set up worker status callback for progress bars (works with or without WebSocket)
248
+ // Set up worker callbacks
327
249
  if (orchestrator) {
250
+ // Progress bars
328
251
  orchestrator.setWorkerStatusCallback((workers) => {
329
252
  if (wsClient?.connected()) {
330
253
  wsClient.sendWorkersUpdate(workers);
331
254
  }
332
- // Log running workers count for visibility even without WebSocket
333
255
  const running = workers.filter(w => w.status === 'running');
334
256
  if (running.length > 0) {
335
- console.log(`[WORKERS] ${running.length} running: ${running.map(w => `${w.id} (${w.progress}%)`).join(', ')}`);
257
+ console.log(`[WORKERS] ${running.length} running`);
258
+ }
259
+ });
260
+ // Worker results - send to user when workers complete
261
+ orchestrator.setWorkerResultCallback((workerId, result) => {
262
+ console.log(`[WORKER DONE] ${workerId}: ${result.slice(0, 100)}...`);
263
+ if (wsClient?.connected()) {
264
+ // Send as response with worker ID as message ID
265
+ wsClient.sendResponse(workerId, `**Worker completed:**\n\n${result}`);
336
266
  }
337
267
  });
338
268
  }
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.18'; // Should match package.json
13
+ const VERSION = '0.2.20'; // Should match package.json
14
14
  const state = {
15
15
  intervalId: null,
16
16
  runnerId: null,
@@ -101,9 +101,14 @@ export declare class MasterOrchestrator {
101
101
  * Process tool calls
102
102
  */
103
103
  private processToolCalls;
104
+ private workerResultCallback;
104
105
  /**
105
- * Delegate task to Claude Code CLI worker with retry logic
106
- * @param timeoutMs - Timeout in milliseconds (default: 30 minutes, max: 60 minutes)
106
+ * Set callback for when workers complete (for async notification)
107
+ */
108
+ setWorkerResultCallback(callback: ((workerId: string, result: string) => void) | null): void;
109
+ /**
110
+ * Delegate task to Claude Code CLI worker - NON-BLOCKING
111
+ * Returns immediately, sends results via callback when done
107
112
  */
108
113
  private delegateToWorker;
109
114
  /**
@@ -828,197 +828,99 @@ Be specific about what you want done.`,
828
828
  }
829
829
  return results;
830
830
  }
831
+ // Callback for sending results when worker completes
832
+ workerResultCallback = null;
831
833
  /**
832
- * Delegate task to Claude Code CLI worker with retry logic
833
- * @param timeoutMs - Timeout in milliseconds (default: 30 minutes, max: 60 minutes)
834
+ * Set callback for when workers complete (for async notification)
834
835
  */
835
- async delegateToWorker(task, context, workingDir, retryCount = 0, timeoutMs) {
836
- const maxRetries = 2;
837
- // Default 30 min, max 60 min
838
- const workerTimeout = Math.min(timeoutMs || 30 * 60 * 1000, 60 * 60 * 1000);
836
+ setWorkerResultCallback(callback) {
837
+ this.workerResultCallback = callback;
838
+ }
839
+ /**
840
+ * Delegate task to Claude Code CLI worker - NON-BLOCKING
841
+ * Returns immediately, sends results via callback when done
842
+ */
843
+ async delegateToWorker(task, context, workingDir) {
839
844
  const id = `worker_${Date.now()}_${++this.jobCounter}`;
840
845
  const cwd = workingDir || this.workspaceDir;
841
- // Search for relevant memories to inject into worker prompt
842
- let memoryContext = '';
843
- try {
844
- const relevantMemories = await this.memory.search(task, { limit: 5, minImportance: 0.3 });
845
- if (relevantMemories.length > 0) {
846
- memoryContext = '\n\nRELEVANT KNOWLEDGE FROM MEMORY:\n' +
847
- relevantMemories.map(m => `[${m.type}] ${m.content}`).join('\n');
848
- }
849
- }
850
- catch (error) {
851
- // Memory search failed, continue without it
852
- console.log('[ORCHESTRATOR] Memory search failed, continuing without context');
853
- }
854
- // Get shared state context for worker coordination
855
- const sharedContext = this.sharedState.getSummaryForWorker(id);
856
- // Build prompt for worker with memory context and checkpoint instructions
846
+ // Build simple prompt - no bloat
857
847
  let prompt = task;
858
848
  if (context) {
859
849
  prompt = `Context: ${context}\n\nTask: ${task}`;
860
850
  }
861
- if (memoryContext) {
862
- prompt += memoryContext;
863
- }
864
- if (sharedContext) {
865
- prompt += `\n\nWORKER COORDINATION:${sharedContext}`;
866
- }
867
- // Add checkpoint and logging instructions to prevent data loss
868
- const logFile = `/tmp/worker-${id}-log.txt`;
869
- prompt += `
870
-
871
- IMPORTANT - Progress & Logging:
872
- - Output findings as you go, don't wait until the end
873
- - Print discoveries, file paths, and insights immediately as you find them
874
- - Report on each file/step before moving to the next
875
-
876
- REQUIRED - Log Export:
877
- At the END of your work, create a final log file at: ${logFile}
878
- Include: job_id=${id}, timestamp, summary of work done, files modified, key findings.
879
- This ensures nothing is lost even if your output gets truncated.
880
-
881
- LEARNING EXPORT (optional but valuable):
882
- If you discover something important (patterns, user preferences, technical insights),
883
- end your response with a line like:
884
- [MEMORY] type=semantic | content=User prefers X over Y for this type of task
885
- [MEMORY] type=procedural | content=When doing X, always check Y first
886
- This helps me remember and improve for future tasks.
887
-
888
- WORKER COORDINATION (optional):
889
- If you need to share data with other workers or signal completion:
890
- [SHARE] key=myData | value={"result": "something useful"}
891
- [SIGNAL] name=step1_complete | data={"files": ["a.ts", "b.ts"]}
892
- [MESSAGE] to=worker_xyz | content=Please review the changes I made
893
- This enables parallel workers to coordinate.`;
894
- console.log(`[ORCHESTRATOR] Delegating to worker ${id}: ${task.slice(0, 80)}...`);
895
- // Estimate task duration based on content
851
+ // Add brief instruction
852
+ prompt += `\n\nBe concise. Output results directly.`;
853
+ console.log(`[ORCHESTRATOR] Worker ${id} starting: ${task.slice(0, 80)}...`);
854
+ // Estimate task duration
896
855
  const estimatedTime = this.estimateTaskDuration(task);
897
- return new Promise((resolve) => {
898
- const job = {
899
- id,
900
- task: task.slice(0, 200),
901
- status: 'running',
902
- startTime: Date.now(),
903
- output: '',
904
- estimatedTime
905
- };
906
- // Escape single quotes in prompt for shell safety
907
- // Replace ' with '\'' (end quote, escaped quote, start quote)
908
- const escapedPrompt = prompt.replace(/'/g, "'\\''");
909
- // Spawn using /bin/bash -l -c to ensure proper PATH resolution (NVM, etc.)
910
- // Use absolute path to bash to avoid ENOENT errors in restricted environments
911
- const child = spawn('/bin/bash', ['-l', '-c', `claude -p '${escapedPrompt}' --dangerously-skip-permissions`], {
912
- cwd,
913
- env: { ...process.env },
914
- stdio: ['pipe', 'pipe', 'pipe']
915
- });
916
- job.process = child;
917
- this.jobs.set(id, job);
918
- // Configurable timeout (default 30 min, max 60 min)
919
- const timeoutMinutes = Math.round(workerTimeout / 60000);
920
- const timeout = setTimeout(async () => {
921
- if (job.status === 'running') {
922
- job.status = 'timeout';
923
- job.endTime = Date.now();
924
- const duration = Math.round((job.endTime - job.startTime) / 1000);
925
- // Send SIGINT first to allow graceful shutdown and output flush
926
- child.kill('SIGINT');
927
- // Give 5 seconds for graceful shutdown before SIGTERM
928
- setTimeout(() => {
929
- if (!child.killed) {
930
- child.kill('SIGTERM');
931
- }
932
- }, 5000);
933
- // Wait a moment for graceful shutdown to flush logs
934
- await new Promise(r => setTimeout(r, 2000));
935
- const partialOutput = job.output.trim();
936
- // Try to recover from log file
937
- const recoveredLog = this.recoverWorkerLog(id);
938
- // Log failure to memory for learning
939
- await this.logWorkerFailure(id, task, `Timeout after ${timeoutMinutes} minutes`, duration, recoveredLog || undefined);
940
- // Build combined output
941
- let combinedOutput = '';
942
- if (recoveredLog) {
943
- combinedOutput = `[Recovered from log file]\n${recoveredLog}\n\n`;
944
- }
945
- if (partialOutput) {
946
- combinedOutput += `[Partial stdout]\n${partialOutput.slice(-3000)}`;
947
- }
948
- resolve({
949
- success: false,
950
- output: combinedOutput
951
- ? `Worker timed out after ${timeoutMinutes} minutes. Recovered output:\n${combinedOutput}`
952
- : `Worker timed out after ${timeoutMinutes} minutes with no recoverable output.`
953
- });
954
- }
955
- }, workerTimeout);
956
- child.stdout?.on('data', (data) => {
957
- job.output += data.toString();
958
- });
959
- child.stderr?.on('data', (data) => {
960
- // Ignore status messages
961
- const text = data.toString();
962
- if (!text.includes('Checking') && !text.includes('Connected')) {
963
- job.output += text;
964
- }
965
- });
966
- child.on('close', async (code) => {
967
- clearTimeout(timeout);
968
- job.status = code === 0 ? 'completed' : 'failed';
969
- job.endTime = Date.now();
970
- const duration = Math.round((job.endTime - job.startTime) / 1000);
971
- console.log(`[ORCHESTRATOR] Worker ${id} finished in ${duration}s (code: ${code})`);
972
- let finalOutput = job.output.trim();
973
- if (code !== 0 || finalOutput.length === 0) {
974
- console.log(`[ORCHESTRATOR] Worker ${id} output: ${finalOutput.slice(0, 200) || '(empty)'}`);
975
- // Try to recover from log file on failure or empty output
976
- const recoveredLog = this.recoverWorkerLog(id);
977
- if (recoveredLog) {
978
- finalOutput = recoveredLog + (finalOutput ? `\n\n[Additional stdout]\n${finalOutput}` : '');
979
- }
980
- // Log failure to memory
981
- if (code !== 0) {
982
- await this.logWorkerFailure(id, task, `Exit code ${code}`, duration, recoveredLog || undefined);
983
- }
984
- }
985
- // Extract and store memory contributions from worker output
986
- await this.extractWorkerMemories(finalOutput, id);
987
- resolve({
988
- success: code === 0,
989
- output: finalOutput || '(No output)'
990
- });
991
- });
992
- child.on('error', async (err) => {
993
- clearTimeout(timeout);
994
- job.status = 'failed';
856
+ const job = {
857
+ id,
858
+ task: task.slice(0, 200),
859
+ status: 'running',
860
+ startTime: Date.now(),
861
+ output: '',
862
+ estimatedTime
863
+ };
864
+ // Escape single quotes in prompt for shell safety
865
+ const escapedPrompt = prompt.replace(/'/g, "'\\''");
866
+ // Spawn worker
867
+ const child = spawn('/bin/bash', ['-l', '-c', `claude -p '${escapedPrompt}' --dangerously-skip-permissions`], {
868
+ cwd,
869
+ env: { ...process.env },
870
+ stdio: ['pipe', 'pipe', 'pipe']
871
+ });
872
+ job.process = child;
873
+ this.jobs.set(id, job);
874
+ // 5 minute timeout
875
+ const timeout = setTimeout(() => {
876
+ if (job.status === 'running') {
877
+ job.status = 'timeout';
995
878
  job.endTime = Date.now();
996
- const duration = Math.round((job.endTime - job.startTime) / 1000);
997
- console.error(`[ORCHESTRATOR] Worker ${id} spawn error:`, err.message);
998
- // Retryable errors: ENOENT (command not found), EAGAIN (resource busy), ENOMEM (out of memory)
999
- const retryableErrors = ['ENOENT', 'EAGAIN', 'ENOMEM', 'ETIMEDOUT', 'ECONNRESET'];
1000
- const isRetryable = retryableErrors.some(code => err.message.includes(code));
1001
- if (isRetryable && retryCount < maxRetries) {
1002
- console.log(`[ORCHESTRATOR] Retrying worker (attempt ${retryCount + 2}/${maxRetries + 1}) after ${err.message}...`);
1003
- // Exponential backoff: 500ms, 1000ms, 2000ms
1004
- const delay = 500 * Math.pow(2, retryCount);
1005
- await new Promise(r => setTimeout(r, delay));
1006
- const retryResult = await this.delegateToWorker(task, context, workingDir, retryCount + 1, timeoutMs);
1007
- resolve(retryResult);
1008
- return;
879
+ child.kill('SIGTERM');
880
+ console.log(`[ORCHESTRATOR] Worker ${id} timed out`);
881
+ if (this.workerResultCallback) {
882
+ this.workerResultCallback(id, `Worker timed out. Partial output: ${job.output.slice(-1000) || 'none'}`);
1009
883
  }
1010
- // Log failure to memory
1011
- await this.logWorkerFailure(id, task, err.message, duration);
1012
- // Try to recover any partial output from log file
1013
- const recoveredLog = this.recoverWorkerLog(id);
1014
- resolve({
1015
- success: false,
1016
- output: recoveredLog
1017
- ? `Worker error: ${err.message}\n\n[Recovered from log]\n${recoveredLog}`
1018
- : `Worker error: ${err.message}`
1019
- });
1020
- });
884
+ }
885
+ }, 5 * 60 * 1000);
886
+ child.stdout?.on('data', (data) => {
887
+ const text = data.toString();
888
+ job.output += text;
889
+ // Stream output to console
890
+ process.stdout.write(`[${id}] ${text}`);
891
+ });
892
+ child.stderr?.on('data', (data) => {
893
+ const text = data.toString();
894
+ if (!text.includes('Checking') && !text.includes('Connected')) {
895
+ job.output += text;
896
+ }
897
+ });
898
+ child.on('close', async (code) => {
899
+ clearTimeout(timeout);
900
+ job.status = code === 0 ? 'completed' : 'failed';
901
+ job.endTime = Date.now();
902
+ const duration = Math.round((job.endTime - job.startTime) / 1000);
903
+ console.log(`[ORCHESTRATOR] Worker ${id} done in ${duration}s`);
904
+ const result = job.output.trim() || '(No output)';
905
+ // Notify via callback (sends to user via WebSocket)
906
+ if (this.workerResultCallback) {
907
+ this.workerResultCallback(id, result);
908
+ }
1021
909
  });
910
+ child.on('error', (err) => {
911
+ clearTimeout(timeout);
912
+ job.status = 'failed';
913
+ job.endTime = Date.now();
914
+ console.error(`[ORCHESTRATOR] Worker ${id} error:`, err.message);
915
+ if (this.workerResultCallback) {
916
+ this.workerResultCallback(id, `Worker error: ${err.message}`);
917
+ }
918
+ });
919
+ // Return immediately - worker runs in background
920
+ return {
921
+ success: true,
922
+ output: `Worker ${id} started. I'll notify you when it completes.`
923
+ };
1022
924
  }
1023
925
  /**
1024
926
  * Try to recover worker output from log file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siftd/connect-agent",
3
- "version": "0.2.18",
3
+ "version": "0.2.20",
4
4
  "description": "Master orchestrator agent - control Claude Code remotely via web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",