@siftd/connect-agent 0.2.56 → 0.2.58

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.
@@ -75,6 +75,8 @@ export declare class MasterOrchestrator {
75
75
  private initialized;
76
76
  private currentFileScope;
77
77
  private forceTeamWorkingDir;
78
+ private recentFileWrites;
79
+ private recentWorkerActivity;
78
80
  private bashTool;
79
81
  private webTools;
80
82
  private workerTools;
@@ -210,7 +212,14 @@ export declare class MasterOrchestrator {
210
212
  private getFileScopeOverrides;
211
213
  private rewriteFilesAlias;
212
214
  private resolveFilesWritePath;
215
+ private toFilesVirtualPath;
216
+ private finalizeResponse;
213
217
  private getFileScopeSystemNote;
218
+ private recordWorkerActivity;
219
+ private formatRelativeTime;
220
+ private getRecentWorkerSummary;
221
+ private isStatusInquiry;
222
+ private buildStatusReply;
214
223
  /**
215
224
  * Check if verbose mode is enabled
216
225
  */
@@ -258,6 +258,8 @@ export class MasterOrchestrator {
258
258
  initialized = false;
259
259
  currentFileScope = 'personal';
260
260
  forceTeamWorkingDir = false;
261
+ recentFileWrites = [];
262
+ recentWorkerActivity = [];
261
263
  // New tools from whatsapp-claude
262
264
  bashTool;
263
265
  webTools;
@@ -937,6 +939,52 @@ export class MasterOrchestrator {
937
939
  }
938
940
  return resolved;
939
941
  }
942
+ toFilesVirtualPath(rawPath, resolvedPath) {
943
+ const trimmed = rawPath.trim();
944
+ if (!trimmed)
945
+ return '/files';
946
+ const lower = trimmed.toLowerCase();
947
+ if (lower.startsWith('/files')) {
948
+ const suffix = trimmed.slice('/files'.length).replace(/\\/g, '/');
949
+ return `/files${suffix}`;
950
+ }
951
+ if (lower.startsWith('files/')) {
952
+ const suffix = trimmed.slice('files'.length).replace(/\\/g, '/');
953
+ return `/files${suffix}`;
954
+ }
955
+ if (trimmed.startsWith('/')) {
956
+ const teamDir = this.currentFileScope === 'team' ? this.getTeamFilesDir() : null;
957
+ const baseDir = teamDir || getSharedOutputPath();
958
+ try {
959
+ const resolved = resolve(resolvedPath || trimmed);
960
+ const baseResolved = resolve(baseDir);
961
+ if (resolved === baseResolved)
962
+ return '/files';
963
+ if (resolved.startsWith(`${baseResolved}${sep}`)) {
964
+ const rel = resolved.slice(baseResolved.length + 1).replace(/\\/g, '/');
965
+ return `/files/${rel}`;
966
+ }
967
+ }
968
+ catch {
969
+ return '/files';
970
+ }
971
+ }
972
+ const cleaned = trimmed.replace(/^\/+/, '').replace(/^files\/?/i, '');
973
+ return cleaned ? `/files/${cleaned}` : '/files';
974
+ }
975
+ finalizeResponse(text) {
976
+ const trimmed = text.trim();
977
+ if (this.recentFileWrites.length === 0)
978
+ return trimmed || text;
979
+ const uniquePaths = Array.from(new Set(this.recentFileWrites));
980
+ const confirmation = uniquePaths.length === 1
981
+ ? `Saved ${uniquePaths[0]}.`
982
+ : `Saved files:\n${uniquePaths.map((path) => `- ${path}`).join('\n')}`;
983
+ if (!trimmed)
984
+ return confirmation;
985
+ const mentionsPath = uniquePaths.some((path) => trimmed.includes(path)) || /\/files\b/i.test(trimmed);
986
+ return mentionsPath ? trimmed : `${trimmed}\n\n${confirmation}`;
987
+ }
940
988
  getFileScopeSystemNote() {
941
989
  if (this.currentFileScope !== 'team')
942
990
  return null;
@@ -945,6 +993,98 @@ export class MasterOrchestrator {
945
993
  return null;
946
994
  return `TEAM FILES DIRECTORY:\n- ${teamDir}\nUse this path for files meant to be shared with the team.`;
947
995
  }
996
+ recordWorkerActivity(event) {
997
+ const normalized = {
998
+ ...event,
999
+ task: event.task.slice(0, 140),
1000
+ note: event.note?.slice(0, 200)
1001
+ };
1002
+ this.recentWorkerActivity = [normalized, ...this.recentWorkerActivity].slice(0, 12);
1003
+ }
1004
+ formatRelativeTime(timestamp) {
1005
+ const deltaMs = Date.now() - timestamp;
1006
+ if (deltaMs < 1000)
1007
+ return 'just now';
1008
+ const seconds = Math.floor(deltaMs / 1000);
1009
+ if (seconds < 60)
1010
+ return `${seconds}s ago`;
1011
+ const minutes = Math.floor(seconds / 60);
1012
+ if (minutes < 60)
1013
+ return `${minutes}m ago`;
1014
+ const hours = Math.floor(minutes / 60);
1015
+ if (hours < 24)
1016
+ return `${hours}h ago`;
1017
+ const days = Math.floor(hours / 24);
1018
+ return `${days}d ago`;
1019
+ }
1020
+ getRecentWorkerSummary(limit = 4) {
1021
+ if (this.recentWorkerActivity.length === 0)
1022
+ return null;
1023
+ const seen = new Set();
1024
+ const lines = [];
1025
+ for (const event of this.recentWorkerActivity) {
1026
+ if (seen.has(event.id))
1027
+ continue;
1028
+ seen.add(event.id);
1029
+ const when = this.formatRelativeTime(event.endedAt ?? event.startedAt);
1030
+ const note = event.note ? ` — ${event.note}` : '';
1031
+ lines.push(`- ${event.task} (${event.status}, ${when})${note}`);
1032
+ if (lines.length >= limit)
1033
+ break;
1034
+ }
1035
+ return lines.length > 0 ? lines.join('\n') : null;
1036
+ }
1037
+ isStatusInquiry(message) {
1038
+ const lower = message.toLowerCase();
1039
+ if (!lower)
1040
+ return false;
1041
+ const patterns = [
1042
+ /\b(status|progress|queue|queued|pending)\b/,
1043
+ /\bwhat (are|were) you (doing|working on)\b/,
1044
+ /\bwhat happened\b/,
1045
+ /\bstuck\b/,
1046
+ /\btaking (so long|forever|too long)\b/,
1047
+ /\bwhy (is|are) (this|it) (taking|hung|stuck)\b/,
1048
+ ];
1049
+ return patterns.some((pattern) => pattern.test(lower));
1050
+ }
1051
+ buildStatusReply() {
1052
+ const queueStatus = this.taskQueue.getStatus();
1053
+ const runningWorkers = this.getWorkerStatus().filter((worker) => worker.status === 'running');
1054
+ const recentWorkers = this.getRecentWorkerSummary(4);
1055
+ const recentTasks = this.taskQueue.getRecentTasks(3)
1056
+ .filter((task) => task.status !== 'pending')
1057
+ .map((task) => `- ${task.content.slice(0, 80)} (${task.status})`);
1058
+ const lines = [];
1059
+ if (runningWorkers.length > 0) {
1060
+ lines.push(`Active workers: ${runningWorkers.length}`);
1061
+ for (const worker of runningWorkers.slice(0, 3)) {
1062
+ lines.push(`- ${worker.task} (${worker.progress}% · ${worker.elapsed}s)`);
1063
+ }
1064
+ }
1065
+ else {
1066
+ lines.push('Active workers: none');
1067
+ }
1068
+ if (queueStatus.isProcessing || queueStatus.pendingCount > 0) {
1069
+ const current = queueStatus.currentTask?.content;
1070
+ lines.push(`Task queue: ${queueStatus.pendingCount} pending${queueStatus.isProcessing ? ' · processing' : ''}`);
1071
+ if (current) {
1072
+ lines.push(`- Current: ${current.slice(0, 100)}`);
1073
+ }
1074
+ }
1075
+ else {
1076
+ lines.push('Task queue: empty');
1077
+ }
1078
+ if (recentWorkers) {
1079
+ lines.push('Recent worker activity:');
1080
+ lines.push(recentWorkers);
1081
+ }
1082
+ if (recentTasks.length > 0) {
1083
+ lines.push('Recent task results:');
1084
+ lines.push(recentTasks.join('\n'));
1085
+ }
1086
+ return lines.join('\n');
1087
+ }
948
1088
  /**
949
1089
  * Check if verbose mode is enabled
950
1090
  */
@@ -963,6 +1103,9 @@ export class MasterOrchestrator {
963
1103
  return slashResponse;
964
1104
  }
965
1105
  this.updateFileScope(cleanMessage);
1106
+ if (this.isStatusInquiry(cleanMessage)) {
1107
+ return this.buildStatusReply();
1108
+ }
966
1109
  const wantsTodoOrCal = this.hasTodoMutation(cleanMessage) || this.hasCalendarMutation(cleanMessage);
967
1110
  if (wantsTodoOrCal) {
968
1111
  this.attachmentContext = this.extractAttachmentContext(message);
@@ -1019,6 +1162,10 @@ ${hubContextStr}
1019
1162
  if (fileScopeNote) {
1020
1163
  systemWithContext += `\n\n${fileScopeNote}`;
1021
1164
  }
1165
+ const recentWorkerSummary = this.getRecentWorkerSummary();
1166
+ if (recentWorkerSummary) {
1167
+ systemWithContext += `\n\nRECENT WORKER ACTIVITY (for accurate status updates):\n${recentWorkerSummary}`;
1168
+ }
1022
1169
  // Add user message
1023
1170
  const messages = [
1024
1171
  ...conversationHistory,
@@ -1454,6 +1601,7 @@ ${hubContextStr}
1454
1601
  const forcedToolChoice = this.getToolChoice(currentMessages);
1455
1602
  let retriedForcedTool = false;
1456
1603
  let retriedTodoCal = false;
1604
+ this.recentFileWrites = [];
1457
1605
  while (iterations < maxIterations) {
1458
1606
  iterations++;
1459
1607
  const toolChoice = forcedToolChoice ?? this.getToolChoice(currentMessages);
@@ -1516,7 +1664,7 @@ ${hubContextStr}
1516
1664
  ];
1517
1665
  continue;
1518
1666
  }
1519
- return this.extractText(response.content);
1667
+ return this.finalizeResponse(this.extractText(response.content));
1520
1668
  }
1521
1669
  const toolUseBlocks = response.content.filter((block) => block.type === 'tool_use');
1522
1670
  const toolNames = toolUseBlocks.map((block) => block.name);
@@ -1554,7 +1702,7 @@ ${hubContextStr}
1554
1702
  { role: 'user', content: toolResults }
1555
1703
  ];
1556
1704
  }
1557
- return 'Maximum iterations reached.';
1705
+ return this.finalizeResponse('Maximum iterations reached.');
1558
1706
  }
1559
1707
  /**
1560
1708
  * Get tool definitions for the orchestrator
@@ -2258,7 +2406,9 @@ Unlike lia_plan (internal only), this creates a VISIBLE todo list that appears i
2258
2406
  const targetPath = this.resolveFilesWritePath(pathValue);
2259
2407
  mkdirSync(dirname(targetPath), { recursive: true });
2260
2408
  writeFileSync(targetPath, content, 'utf8');
2261
- result = { success: true, output: 'Saved file to /files.' };
2409
+ const virtualPath = this.toFilesVirtualPath(pathValue, targetPath);
2410
+ this.recentFileWrites.push(virtualPath);
2411
+ result = { success: true, output: `Saved file to ${virtualPath}.` };
2262
2412
  }
2263
2413
  catch (error) {
2264
2414
  const message = error instanceof Error ? error.message : String(error);
@@ -2286,6 +2436,17 @@ Unlike lia_plan (internal only), this creates a VISIBLE todo list that appears i
2286
2436
  priority: input.priority,
2287
2437
  workingDirectory: input.working_directory || fileScope.workingDir
2288
2438
  });
2439
+ if (result.success && result.output) {
2440
+ const match = result.output.match(/Job ID:\s*(\S+)/i);
2441
+ if (match) {
2442
+ this.recordWorkerActivity({
2443
+ id: match[1],
2444
+ task: normalizedTask.slice(0, 200),
2445
+ status: 'running',
2446
+ startedAt: Date.now(),
2447
+ });
2448
+ }
2449
+ }
2289
2450
  break;
2290
2451
  }
2291
2452
  case 'check_worker':
@@ -2299,6 +2460,16 @@ Unlike lia_plan (internal only), this creates a VISIBLE todo list that appears i
2299
2460
  break;
2300
2461
  case 'cancel_worker':
2301
2462
  result = await this.workerTools.cancelWorker(input.job_id);
2463
+ if (result.success) {
2464
+ this.recordWorkerActivity({
2465
+ id: String(input.job_id || 'unknown'),
2466
+ task: 'Worker cancelled',
2467
+ status: 'cancelled',
2468
+ startedAt: Date.now(),
2469
+ endedAt: Date.now(),
2470
+ note: 'Cancelled by request'
2471
+ });
2472
+ }
2302
2473
  break;
2303
2474
  // Legacy delegate tool
2304
2475
  case 'delegate_to_worker': {
@@ -2459,6 +2630,12 @@ Unlike lia_plan (internal only), this creates a VISIBLE todo list that appears i
2459
2630
  estimatedTime,
2460
2631
  workingDir: cwd
2461
2632
  };
2633
+ this.recordWorkerActivity({
2634
+ id,
2635
+ task: job.task,
2636
+ status: 'running',
2637
+ startedAt: job.startTime
2638
+ });
2462
2639
  // Escape single quotes in prompt for shell safety
2463
2640
  const escapedPrompt = prompt.replace(/'/g, "'\\''");
2464
2641
  const budget = resolveClaudeBudgetUsd();
@@ -2499,6 +2676,14 @@ Unlike lia_plan (internal only), this creates a VISIBLE todo list that appears i
2499
2676
  job.endTime = Date.now();
2500
2677
  child.kill('SIGTERM');
2501
2678
  console.log(`[ORCHESTRATOR] Worker ${id} timed out`);
2679
+ this.recordWorkerActivity({
2680
+ id,
2681
+ task: job.task,
2682
+ status: 'timeout',
2683
+ startedAt: job.startTime,
2684
+ endedAt: job.endTime,
2685
+ note: 'Timed out after 5 minutes'
2686
+ });
2502
2687
  if (this.workerResultCallback) {
2503
2688
  this.workerResultCallback(id, `Worker timed out. Partial output: ${job.output.slice(-1000) || 'none'}`);
2504
2689
  }
@@ -2536,6 +2721,14 @@ Unlike lia_plan (internal only), this creates a VISIBLE todo list that appears i
2536
2721
  const status = code === 0 ? 'completed' : 'failed';
2537
2722
  const filesCreated = assets.map(a => a.name);
2538
2723
  logWorker(id, job.task, status, duration, filesCreated.length > 0 ? filesCreated : undefined);
2724
+ this.recordWorkerActivity({
2725
+ id,
2726
+ task: job.task,
2727
+ status,
2728
+ startedAt: job.startTime,
2729
+ endedAt: job.endTime,
2730
+ note: code === 0 ? undefined : `Exit code ${code ?? 'unknown'}`
2731
+ });
2539
2732
  if (assets.length > 0) {
2540
2733
  console.log(`[ORCHESTRATOR] Worker ${id} created ${assets.length} files: ${assets.map(a => a.name).join(', ')}`);
2541
2734
  // Store in memory for gallery queries
@@ -2563,6 +2756,14 @@ Unlike lia_plan (internal only), this creates a VISIBLE todo list that appears i
2563
2756
  job.status = 'failed';
2564
2757
  job.endTime = Date.now();
2565
2758
  console.error(`[ORCHESTRATOR] Worker ${id} error:`, err.message);
2759
+ this.recordWorkerActivity({
2760
+ id,
2761
+ task: job.task,
2762
+ status: 'failed',
2763
+ startedAt: job.startTime,
2764
+ endedAt: job.endTime,
2765
+ note: err.message
2766
+ });
2566
2767
  if (this.workerResultCallback) {
2567
2768
  this.workerResultCallback(id, `Worker error: ${err.message}`);
2568
2769
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siftd/connect-agent",
3
- "version": "0.2.56",
3
+ "version": "0.2.58",
4
4
  "description": "Master orchestrator agent - control Claude Code remotely via web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",