tide-commander 0.52.0

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.
Files changed (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +364 -0
  3. package/dist/assets/characters/Textures/colormap.png +0 -0
  4. package/dist/assets/characters/character-female-a.glb +0 -0
  5. package/dist/assets/characters/character-female-b.glb +0 -0
  6. package/dist/assets/characters/character-female-c.glb +0 -0
  7. package/dist/assets/characters/character-female-d.glb +0 -0
  8. package/dist/assets/characters/character-female-e.glb +0 -0
  9. package/dist/assets/characters/character-female-f.glb +0 -0
  10. package/dist/assets/characters/character-male-a-processed.gltf +11862 -0
  11. package/dist/assets/characters/character-male-a.glb +0 -0
  12. package/dist/assets/characters/character-male-b.glb +0 -0
  13. package/dist/assets/characters/character-male-c.glb +0 -0
  14. package/dist/assets/characters/character-male-d.glb +0 -0
  15. package/dist/assets/characters/character-male-e.glb +0 -0
  16. package/dist/assets/characters/character-male-f.glb +0 -0
  17. package/dist/assets/icons/icon-192.png +0 -0
  18. package/dist/assets/icons/icon-512.png +0 -0
  19. package/dist/assets/landing-Cc0MDBAK.css +1 -0
  20. package/dist/assets/main-BIpLsrUu.css +1 -0
  21. package/dist/assets/main-DMTRw3br.js +276 -0
  22. package/dist/assets/textures/concrete_floor_worn_001_diff_1k.jpg +0 -0
  23. package/dist/assets/textures/logo-blanco.png +0 -0
  24. package/dist/assets/vendor-react-uS-d4TUT.js +17 -0
  25. package/dist/assets/vendor-three-4iQNXcoo.js +3828 -0
  26. package/dist/assets/web-BZdi2lG9.js +1 -0
  27. package/dist/assets/web-yHsOO1Qb.js +1 -0
  28. package/dist/index.html +38 -0
  29. package/dist/manifest.json +39 -0
  30. package/dist/src/packages/landing/index.html +463 -0
  31. package/dist/src/packages/server/app.js +87 -0
  32. package/dist/src/packages/server/auth/index.js +121 -0
  33. package/dist/src/packages/server/claude/backend.js +578 -0
  34. package/dist/src/packages/server/claude/index.js +8 -0
  35. package/dist/src/packages/server/claude/runner/internal-events.js +22 -0
  36. package/dist/src/packages/server/claude/runner/process-lifecycle.js +208 -0
  37. package/dist/src/packages/server/claude/runner/recovery-store.js +72 -0
  38. package/dist/src/packages/server/claude/runner/resource-monitor.js +51 -0
  39. package/dist/src/packages/server/claude/runner/restart-policy.js +69 -0
  40. package/dist/src/packages/server/claude/runner/stdout-pipeline.js +153 -0
  41. package/dist/src/packages/server/claude/runner/watchdog.js +114 -0
  42. package/dist/src/packages/server/claude/runner.js +310 -0
  43. package/dist/src/packages/server/claude/session-loader.js +898 -0
  44. package/dist/src/packages/server/claude/types.js +5 -0
  45. package/dist/src/packages/server/cli.js +113 -0
  46. package/dist/src/packages/server/codex/backend.js +119 -0
  47. package/dist/src/packages/server/codex/index.js +2 -0
  48. package/dist/src/packages/server/codex/json-event-parser.js +612 -0
  49. package/dist/src/packages/server/data/builtin-skills/bitbucket-pr.js +298 -0
  50. package/dist/src/packages/server/data/builtin-skills/full-notifications.js +49 -0
  51. package/dist/src/packages/server/data/builtin-skills/git-captain.js +304 -0
  52. package/dist/src/packages/server/data/builtin-skills/index.js +61 -0
  53. package/dist/src/packages/server/data/builtin-skills/pm2-logs.js +354 -0
  54. package/dist/src/packages/server/data/builtin-skills/send-message-to-agent.js +51 -0
  55. package/dist/src/packages/server/data/builtin-skills/server-logs.js +124 -0
  56. package/dist/src/packages/server/data/builtin-skills/streaming-exec.js +94 -0
  57. package/dist/src/packages/server/data/builtin-skills/types.js +4 -0
  58. package/dist/src/packages/server/data/builtin-skills.js +6 -0
  59. package/dist/src/packages/server/data/index.js +890 -0
  60. package/dist/src/packages/server/data/snapshots.js +371 -0
  61. package/dist/src/packages/server/index.js +96 -0
  62. package/dist/src/packages/server/prompts/tide-commander.js +13 -0
  63. package/dist/src/packages/server/routes/agents.js +406 -0
  64. package/dist/src/packages/server/routes/config.js +347 -0
  65. package/dist/src/packages/server/routes/custom-models.js +170 -0
  66. package/dist/src/packages/server/routes/exec.js +269 -0
  67. package/dist/src/packages/server/routes/files.js +995 -0
  68. package/dist/src/packages/server/routes/index.js +38 -0
  69. package/dist/src/packages/server/routes/notifications.js +81 -0
  70. package/dist/src/packages/server/routes/permissions.js +115 -0
  71. package/dist/src/packages/server/routes/snapshots.js +224 -0
  72. package/dist/src/packages/server/routes/stt.js +99 -0
  73. package/dist/src/packages/server/routes/tts.js +166 -0
  74. package/dist/src/packages/server/routes/voice-assistant.js +310 -0
  75. package/dist/src/packages/server/runtime/claude-runtime-provider.js +10 -0
  76. package/dist/src/packages/server/runtime/codex-runtime-provider.js +11 -0
  77. package/dist/src/packages/server/runtime/index.js +2 -0
  78. package/dist/src/packages/server/runtime/types.js +6 -0
  79. package/dist/src/packages/server/services/agent-lifecycle-service.js +82 -0
  80. package/dist/src/packages/server/services/agent-service.js +410 -0
  81. package/dist/src/packages/server/services/boss-message-service.js +430 -0
  82. package/dist/src/packages/server/services/boss-service.js +553 -0
  83. package/dist/src/packages/server/services/building-service.js +867 -0
  84. package/dist/src/packages/server/services/claude-service.js +5 -0
  85. package/dist/src/packages/server/services/custom-class-service.js +323 -0
  86. package/dist/src/packages/server/services/database-service.js +914 -0
  87. package/dist/src/packages/server/services/docker-service.js +865 -0
  88. package/dist/src/packages/server/services/fileTracker.js +242 -0
  89. package/dist/src/packages/server/services/index.js +21 -0
  90. package/dist/src/packages/server/services/permission-service.js +258 -0
  91. package/dist/src/packages/server/services/pm2-service.js +435 -0
  92. package/dist/src/packages/server/services/runtime-command-execution.js +168 -0
  93. package/dist/src/packages/server/services/runtime-events.js +357 -0
  94. package/dist/src/packages/server/services/runtime-service.js +308 -0
  95. package/dist/src/packages/server/services/runtime-status-sync.js +104 -0
  96. package/dist/src/packages/server/services/runtime-subagents.js +50 -0
  97. package/dist/src/packages/server/services/runtime-watchdog.js +74 -0
  98. package/dist/src/packages/server/services/secrets-service.js +206 -0
  99. package/dist/src/packages/server/services/skill-service.js +508 -0
  100. package/dist/src/packages/server/services/subordinate-context-service.js +223 -0
  101. package/dist/src/packages/server/services/supervisor-claude.js +132 -0
  102. package/dist/src/packages/server/services/supervisor-prompts.js +80 -0
  103. package/dist/src/packages/server/services/supervisor-service.js +659 -0
  104. package/dist/src/packages/server/services/work-plan-service.js +476 -0
  105. package/dist/src/packages/server/setup.js +86 -0
  106. package/dist/src/packages/server/utils/index.js +4 -0
  107. package/dist/src/packages/server/utils/logger.js +302 -0
  108. package/dist/src/packages/server/utils/string.js +39 -0
  109. package/dist/src/packages/server/utils/tool-formatting.js +139 -0
  110. package/dist/src/packages/server/utils/unicode.js +46 -0
  111. package/dist/src/packages/server/websocket/handler.js +290 -0
  112. package/dist/src/packages/server/websocket/handlers/agent-handler.js +515 -0
  113. package/dist/src/packages/server/websocket/handlers/boss-handler.js +116 -0
  114. package/dist/src/packages/server/websocket/handlers/boss-response-handler.js +250 -0
  115. package/dist/src/packages/server/websocket/handlers/building-handler.js +298 -0
  116. package/dist/src/packages/server/websocket/handlers/command-handler.js +217 -0
  117. package/dist/src/packages/server/websocket/handlers/custom-class-handler.js +68 -0
  118. package/dist/src/packages/server/websocket/handlers/database-handler.js +223 -0
  119. package/dist/src/packages/server/websocket/handlers/notification-handler.js +25 -0
  120. package/dist/src/packages/server/websocket/handlers/permission-handler.js +21 -0
  121. package/dist/src/packages/server/websocket/handlers/secrets-handler.js +61 -0
  122. package/dist/src/packages/server/websocket/handlers/skill-handler.js +148 -0
  123. package/dist/src/packages/server/websocket/handlers/supervisor-handler.js +44 -0
  124. package/dist/src/packages/server/websocket/handlers/sync-handler.js +19 -0
  125. package/dist/src/packages/server/websocket/handlers/types.js +4 -0
  126. package/dist/src/packages/server/websocket/listeners/boss-listeners.js +21 -0
  127. package/dist/src/packages/server/websocket/listeners/index.js +32 -0
  128. package/dist/src/packages/server/websocket/listeners/permission-listeners.js +19 -0
  129. package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +196 -0
  130. package/dist/src/packages/server/websocket/listeners/skill-listeners.js +51 -0
  131. package/dist/src/packages/server/websocket/listeners/supervisor-listeners.js +37 -0
  132. package/dist/src/packages/shared/agent-types.js +54 -0
  133. package/dist/src/packages/shared/building-types.js +43 -0
  134. package/dist/src/packages/shared/common-types.js +1 -0
  135. package/dist/src/packages/shared/database-types.js +8 -0
  136. package/dist/src/packages/shared/types/snapshot.js +7 -0
  137. package/dist/src/packages/shared/types.js +12 -0
  138. package/dist/src/packages/shared/websocket-messages.js +1 -0
  139. package/dist/sw.js +37 -0
  140. package/package.json +90 -0
@@ -0,0 +1,659 @@
1
+ /**
2
+ * Supervisor Service
3
+ * Manages periodic analysis of agent activities via Claude Code
4
+ * Generates human-readable activity narratives
5
+ */
6
+ import * as agentService from './agent-service.js';
7
+ import { loadSession } from '../claude/index.js';
8
+ import { callClaudeForAnalysis, stripCodeFences } from './supervisor-claude.js';
9
+ import { loadSupervisorHistory, saveSupervisorHistory, addSupervisorHistoryEntry, getAgentSupervisorHistory as getAgentHistoryFromStorage, deleteSupervisorHistory, } from '../data/index.js';
10
+ import { logger, sanitizeUnicode, generateId, truncateOrEmpty, formatToolNarrative } from '../utils/index.js';
11
+ import { SINGLE_AGENT_PROMPT, DEFAULT_SUPERVISOR_PROMPT } from './supervisor-prompts.js';
12
+ const log = logger.supervisor;
13
+ // In-memory narrative storage per agent
14
+ const narratives = new Map();
15
+ // Supervisor history storage per agent (persisted to disk)
16
+ let supervisorHistory = new Map();
17
+ // Configuration
18
+ let config = {
19
+ enabled: true,
20
+ intervalMs: 60000, // Not used for timer anymore, kept for compatibility
21
+ maxNarrativesPerAgent: 20,
22
+ autoReportOnComplete: false, // Generate report when agent completes task (disabled by default)
23
+ };
24
+ // Debounce for report generation (avoid generating too many reports in quick succession)
25
+ let reportDebounceTimer = null;
26
+ const REPORT_DEBOUNCE_MS = 3000; // Wait 3 seconds after last event before generating
27
+ // Per-agent debounce timers for single-agent report generation
28
+ const agentReportTimers = new Map();
29
+ const agentReportInProgress = new Set();
30
+ // Track if a full report is currently being generated
31
+ let isGeneratingReport = false;
32
+ // Latest report
33
+ let latestReport = null;
34
+ const listeners = new Set();
35
+ // ============================================================================
36
+ // Initialization
37
+ // ============================================================================
38
+ export function init() {
39
+ // Load persisted supervisor history
40
+ supervisorHistory = loadSupervisorHistory();
41
+ log.log(' Initialized (event-driven mode, using Claude Code)');
42
+ log.log(` Loaded history for ${supervisorHistory.size} agents`);
43
+ }
44
+ export function shutdown() {
45
+ if (reportDebounceTimer) {
46
+ clearTimeout(reportDebounceTimer);
47
+ reportDebounceTimer = null;
48
+ }
49
+ }
50
+ // ============================================================================
51
+ // Event System
52
+ // ============================================================================
53
+ export function subscribe(listener) {
54
+ listeners.add(listener);
55
+ return () => listeners.delete(listener);
56
+ }
57
+ function emit(event, data) {
58
+ listeners.forEach((listener) => listener(event, data));
59
+ }
60
+ // ============================================================================
61
+ // Narrative Generation
62
+ // ============================================================================
63
+ /**
64
+ * Generate a human-readable narrative from a Claude event
65
+ */
66
+ export function generateNarrative(agentId, event) {
67
+ const agent = agentService.getAgent(agentId);
68
+ if (!agent)
69
+ return null;
70
+ let narrative = null;
71
+ let type = 'output';
72
+ let toolName;
73
+ switch (event.type) {
74
+ case 'tool_start':
75
+ type = 'tool_use';
76
+ toolName = event.toolName;
77
+ narrative = formatToolNarrative(event.toolName, event.toolInput);
78
+ break;
79
+ case 'text':
80
+ if (event.text && event.text.length > 10) {
81
+ type = 'output';
82
+ narrative = `Responding: "${truncateOrEmpty(event.text, 100)}"`;
83
+ }
84
+ break;
85
+ case 'thinking':
86
+ if (event.text) {
87
+ type = 'thinking';
88
+ narrative = `Thinking: "${truncateOrEmpty(event.text, 80)}"`;
89
+ }
90
+ break;
91
+ case 'error':
92
+ type = 'error';
93
+ narrative = `Error occurred: ${event.errorMessage || 'Unknown error'}`;
94
+ break;
95
+ case 'step_complete':
96
+ type = 'task_complete';
97
+ narrative = `Completed processing step (${event.tokens?.input || 0} input, ${event.tokens?.output || 0} output tokens)`;
98
+ break;
99
+ }
100
+ if (!narrative)
101
+ return null;
102
+ const activityNarrative = {
103
+ id: generateId(),
104
+ agentId,
105
+ timestamp: Date.now(),
106
+ type,
107
+ narrative,
108
+ toolName,
109
+ };
110
+ // Store narrative
111
+ addNarrative(agentId, activityNarrative);
112
+ // Emit for real-time updates
113
+ emit('narrative', { agentId, narrative: activityNarrative });
114
+ // Trigger single-agent report generation on significant events (task start or complete)
115
+ if (event.type === 'init' || event.type === 'step_complete') {
116
+ log.log(` Event trigger: ${event.type} from agent ${agentId}`);
117
+ scheduleAgentReportGeneration(agentId);
118
+ }
119
+ return activityNarrative;
120
+ }
121
+ /**
122
+ * Schedule a single-agent report generation with debouncing
123
+ * Only updates the specific agent that had activity, not all agents
124
+ */
125
+ function scheduleAgentReportGeneration(agentId) {
126
+ if (!config.enabled) {
127
+ log.log(' Disabled, skipping scheduled agent report');
128
+ return;
129
+ }
130
+ if (!config.autoReportOnComplete) {
131
+ log.log(' Auto-report on complete disabled, skipping scheduled agent report');
132
+ return;
133
+ }
134
+ // Clear any existing timer for this agent
135
+ const existingTimer = agentReportTimers.get(agentId);
136
+ if (existingTimer) {
137
+ clearTimeout(existingTimer);
138
+ }
139
+ log.log(` Scheduled single-agent report for ${agentId} (${REPORT_DEBOUNCE_MS}ms debounce)`);
140
+ // Schedule report generation after debounce period
141
+ const timer = setTimeout(async () => {
142
+ agentReportTimers.delete(agentId);
143
+ if (agentReportInProgress.has(agentId)) {
144
+ log.log(` Report already in progress for ${agentId}, skipping`);
145
+ return;
146
+ }
147
+ const agent = agentService.getAgent(agentId);
148
+ if (!agent) {
149
+ log.log(` Agent ${agentId} not found, skipping report`);
150
+ return;
151
+ }
152
+ try {
153
+ log.log(` Generating single-agent report for ${agent.name}...`);
154
+ await generateSingleAgentReport(agentId);
155
+ }
156
+ catch (err) {
157
+ log.error(` Single-agent report failed for ${agentId}:`, err);
158
+ }
159
+ }, REPORT_DEBOUNCE_MS);
160
+ agentReportTimers.set(agentId, timer);
161
+ }
162
+ // ============================================================================
163
+ // Narrative Storage
164
+ // ============================================================================
165
+ function addNarrative(agentId, narrative) {
166
+ if (!narratives.has(agentId)) {
167
+ narratives.set(agentId, []);
168
+ }
169
+ const agentNarratives = narratives.get(agentId);
170
+ agentNarratives.unshift(narrative);
171
+ // Trim to max
172
+ if (agentNarratives.length > config.maxNarrativesPerAgent) {
173
+ agentNarratives.pop();
174
+ }
175
+ }
176
+ export function getNarratives(agentId) {
177
+ return narratives.get(agentId) || [];
178
+ }
179
+ export function getAllNarratives() {
180
+ return new Map(narratives);
181
+ }
182
+ export function clearNarratives(agentId) {
183
+ narratives.delete(agentId);
184
+ }
185
+ // ============================================================================
186
+ // Report Generation
187
+ // ============================================================================
188
+ export async function generateReport() {
189
+ log.log(' generateReport() called');
190
+ // If already generating, return the latest report (or wait for current one)
191
+ if (isGeneratingReport) {
192
+ log.log(' Report already in progress, returning latest');
193
+ // Return latest report if available, otherwise return a pending status
194
+ if (latestReport) {
195
+ return latestReport;
196
+ }
197
+ // No report yet, return empty one
198
+ return {
199
+ id: generateId(),
200
+ timestamp: Date.now(),
201
+ agentSummaries: [],
202
+ overallStatus: 'healthy',
203
+ insights: ['Report generation in progress...'],
204
+ recommendations: [],
205
+ };
206
+ }
207
+ isGeneratingReport = true;
208
+ log.log(' Starting report generation...');
209
+ try {
210
+ const agents = agentService.getAllAgents();
211
+ if (agents.length === 0) {
212
+ // Return empty report if no agents
213
+ const emptyReport = {
214
+ id: generateId(),
215
+ timestamp: Date.now(),
216
+ agentSummaries: [],
217
+ overallStatus: 'healthy',
218
+ insights: ['No agents currently active'],
219
+ recommendations: [],
220
+ };
221
+ latestReport = emptyReport;
222
+ emit('report', emptyReport);
223
+ return emptyReport;
224
+ }
225
+ // Build agent summaries with session history
226
+ const agentSummaries = await Promise.all(agents.map(async (agent) => {
227
+ // Try to load recent session history if agent has a session
228
+ let sessionNarratives = [];
229
+ if (agent.sessionId) {
230
+ try {
231
+ const history = await loadSession(agent.cwd, agent.sessionId, 20);
232
+ if (history && history.messages.length > 0) {
233
+ // Convert session messages to narratives
234
+ sessionNarratives = history.messages.map((msg, index) => ({
235
+ id: `session-${agent.sessionId}-${index}`,
236
+ agentId: agent.id,
237
+ timestamp: new Date(msg.timestamp).getTime(),
238
+ type: msg.type === 'user' ? 'task_start' :
239
+ msg.type === 'tool_use' ? 'tool_use' :
240
+ msg.type === 'tool_result' ? 'output' : 'output',
241
+ narrative: msg.type === 'user' ? `User asked: "${truncateOrEmpty(msg.content, 150)}"` :
242
+ msg.type === 'assistant' ? `Responded: "${truncateOrEmpty(msg.content, 150)}"` :
243
+ msg.type === 'tool_use' ? `Used tool: ${msg.toolName}` :
244
+ `Tool result received`,
245
+ toolName: msg.toolName,
246
+ }));
247
+ }
248
+ }
249
+ catch (err) {
250
+ console.error(`[SupervisorService] Failed to load session for ${agent.name}:`, err);
251
+ }
252
+ }
253
+ // Combine in-memory narratives with session history, preferring recent in-memory ones
254
+ const inMemoryNarratives = getNarratives(agent.id).slice(0, 10);
255
+ const allNarratives = inMemoryNarratives.length > 0
256
+ ? inMemoryNarratives
257
+ : sessionNarratives.slice(-10);
258
+ return {
259
+ id: agent.id,
260
+ name: agent.name,
261
+ class: agent.class,
262
+ status: agent.status,
263
+ currentTask: agent.currentTask,
264
+ lastAssignedTask: agent.lastAssignedTask,
265
+ lastAssignedTaskTime: agent.lastAssignedTaskTime,
266
+ recentNarratives: allNarratives,
267
+ tokensUsed: agent.tokensUsed,
268
+ contextUsed: agent.contextUsed,
269
+ lastActivityTime: agent.lastActivity,
270
+ };
271
+ }));
272
+ // Call Claude for analysis
273
+ const prompt = await buildSupervisorPrompt(agentSummaries);
274
+ let response;
275
+ try {
276
+ response = await callClaudeForAnalysis(prompt);
277
+ }
278
+ catch (err) {
279
+ log.error(' Claude API call failed:', err);
280
+ // Return fallback report (but still emit it to clients)
281
+ const fallbackReport = createFallbackReport(agentSummaries);
282
+ latestReport = fallbackReport;
283
+ emit('report', fallbackReport);
284
+ return fallbackReport;
285
+ }
286
+ // Parse response
287
+ const report = parseClaudeResponse(response, agentSummaries);
288
+ // Save history entries for each agent in the report
289
+ saveReportToHistory(report);
290
+ latestReport = report;
291
+ log.log(` ✓ Report generated successfully (${report.agentSummaries.length} agents analyzed)`);
292
+ emit('report', report);
293
+ return report;
294
+ }
295
+ finally {
296
+ isGeneratingReport = false;
297
+ }
298
+ }
299
+ /**
300
+ * Generate a supervisor report for a single agent
301
+ * Used when an agent finishes a task - only analyzes that specific agent
302
+ */
303
+ async function generateSingleAgentReport(agentId) {
304
+ const agent = agentService.getAgent(agentId);
305
+ if (!agent)
306
+ return;
307
+ agentReportInProgress.add(agentId);
308
+ try {
309
+ // Build agent summary
310
+ const agentSummary = await buildAgentSummary(agent);
311
+ // Build a simpler prompt for single agent
312
+ const prompt = buildSingleAgentPrompt(agentSummary);
313
+ let response;
314
+ try {
315
+ response = await callClaudeForAnalysis(prompt);
316
+ }
317
+ catch (err) {
318
+ log.error(` Claude API call failed for single agent ${agent.name}:`, err);
319
+ // Create fallback analysis
320
+ const fallbackAnalysis = {
321
+ agentId: agent.id,
322
+ agentName: agent.name,
323
+ statusDescription: `${agent.status} - ${agent.currentTask || 'Task completed'}`,
324
+ progress: agent.status === 'working' ? 'on_track' : 'idle',
325
+ recentWorkSummary: agentSummary.recentNarratives[0]?.narrative || 'No recent activity',
326
+ };
327
+ saveSingleAgentToHistory(fallbackAnalysis);
328
+ emit('agent_analysis', { agentId, analysis: fallbackAnalysis });
329
+ return;
330
+ }
331
+ // Parse response
332
+ const analysis = parseSingleAgentResponse(response, agentSummary);
333
+ // Save to history
334
+ saveSingleAgentToHistory(analysis);
335
+ // Emit the single agent update (not a full report)
336
+ emit('agent_analysis', { agentId, analysis });
337
+ log.log(` ✓ Single-agent report generated for ${agent.name}`);
338
+ }
339
+ finally {
340
+ agentReportInProgress.delete(agentId);
341
+ }
342
+ }
343
+ /**
344
+ * Build summary data for a single agent
345
+ */
346
+ async function buildAgentSummary(agent) {
347
+ let sessionNarratives = [];
348
+ if (agent.sessionId) {
349
+ try {
350
+ const history = await loadSession(agent.cwd, agent.sessionId, 20);
351
+ if (history && history.messages.length > 0) {
352
+ sessionNarratives = history.messages.map((msg, index) => ({
353
+ id: `session-${agent.sessionId}-${index}`,
354
+ agentId: agent.id,
355
+ timestamp: new Date(msg.timestamp).getTime(),
356
+ type: msg.type === 'user' ? 'task_start' :
357
+ msg.type === 'tool_use' ? 'tool_use' :
358
+ msg.type === 'tool_result' ? 'output' : 'output',
359
+ narrative: msg.type === 'user' ? `User asked: "${truncateOrEmpty(msg.content, 150)}"` :
360
+ msg.type === 'assistant' ? `Responded: "${truncateOrEmpty(msg.content, 150)}"` :
361
+ msg.type === 'tool_use' ? `Used tool: ${msg.toolName}` :
362
+ `Tool result received`,
363
+ toolName: msg.toolName,
364
+ }));
365
+ }
366
+ }
367
+ catch (err) {
368
+ log.error(` Failed to load session for ${agent.name}:`, err);
369
+ }
370
+ }
371
+ const inMemoryNarratives = getNarratives(agent.id).slice(0, 10);
372
+ const allNarratives = inMemoryNarratives.length > 0
373
+ ? inMemoryNarratives
374
+ : sessionNarratives.slice(-10);
375
+ return {
376
+ id: agent.id,
377
+ name: agent.name,
378
+ class: agent.class,
379
+ status: agent.status,
380
+ currentTask: agent.currentTask,
381
+ lastAssignedTask: agent.lastAssignedTask,
382
+ lastAssignedTaskTime: agent.lastAssignedTaskTime,
383
+ recentNarratives: allNarratives,
384
+ tokensUsed: agent.tokensUsed,
385
+ contextUsed: agent.contextUsed,
386
+ lastActivityTime: agent.lastActivity,
387
+ };
388
+ }
389
+ /**
390
+ * Build a prompt for analyzing a single agent
391
+ */
392
+ function buildSingleAgentPrompt(summary) {
393
+ const taskAssignedSecondsAgo = summary.lastAssignedTaskTime
394
+ ? Math.round((Date.now() - summary.lastAssignedTaskTime) / 1000)
395
+ : null;
396
+ // Sanitize all string fields to prevent invalid Unicode surrogates
397
+ const agentData = {
398
+ id: summary.id,
399
+ name: sanitizeUnicode(summary.name),
400
+ class: summary.class,
401
+ status: summary.status,
402
+ currentTask: sanitizeUnicode(summary.currentTask || 'None'),
403
+ assignedTask: summary.lastAssignedTask
404
+ ? sanitizeUnicode(truncateOrEmpty(summary.lastAssignedTask, 500))
405
+ : 'No task assigned yet',
406
+ taskAssignedSecondsAgo,
407
+ tokensUsed: summary.tokensUsed,
408
+ contextPercent: Math.round((summary.contextUsed / 200000) * 100),
409
+ timeSinceActivity: Math.round((Date.now() - summary.lastActivityTime) / 1000),
410
+ recentActivities: summary.recentNarratives.map((n) => sanitizeUnicode(n.narrative)).slice(0, 5),
411
+ };
412
+ return SINGLE_AGENT_PROMPT.replace('{{AGENT_DATA}}', JSON.stringify(agentData, null, 2));
413
+ }
414
+ /**
415
+ * Parse Claude's response for a single agent
416
+ */
417
+ function parseSingleAgentResponse(response, summary) {
418
+ try {
419
+ const jsonStr = stripCodeFences(response);
420
+ const parsed = JSON.parse(jsonStr);
421
+ return {
422
+ agentId: parsed.agentId || summary.id,
423
+ agentName: parsed.agentName || summary.name,
424
+ statusDescription: parsed.statusDescription || `${summary.status} - ${summary.currentTask || 'Task completed'}`,
425
+ progress: parsed.progress || (summary.status === 'working' ? 'on_track' : 'idle'),
426
+ recentWorkSummary: parsed.recentWorkSummary || 'No recent activity',
427
+ currentFocus: parsed.currentFocus,
428
+ blockers: parsed.blockers || [],
429
+ suggestions: parsed.suggestions || [],
430
+ filesModified: parsed.filesModified || [],
431
+ concerns: parsed.concerns || [],
432
+ };
433
+ }
434
+ catch (err) {
435
+ log.error(' Failed to parse single agent response:', err);
436
+ return {
437
+ agentId: summary.id,
438
+ agentName: summary.name,
439
+ statusDescription: `${summary.status} - ${summary.currentTask || 'Task completed'}`,
440
+ progress: summary.status === 'working' ? 'on_track' : 'idle',
441
+ recentWorkSummary: summary.recentNarratives[0]?.narrative || 'No recent activity',
442
+ };
443
+ }
444
+ }
445
+ /**
446
+ * Save a single agent's analysis to history
447
+ */
448
+ function saveSingleAgentToHistory(analysis) {
449
+ const entry = {
450
+ id: generateId(),
451
+ timestamp: Date.now(),
452
+ reportId: `single-${generateId()}`,
453
+ analysis,
454
+ };
455
+ addSupervisorHistoryEntry(supervisorHistory, analysis.agentId, entry);
456
+ saveSupervisorHistory(supervisorHistory);
457
+ log.log(` Saved single-agent history entry for ${analysis.agentName}`);
458
+ }
459
+ function buildSupervisorPrompt(summaries) {
460
+ const customPrompt = config.customPrompt || DEFAULT_SUPERVISOR_PROMPT;
461
+ // Sanitize all string fields to prevent invalid Unicode surrogates
462
+ const agentData = summaries.map((s) => {
463
+ // Calculate time since task was assigned
464
+ const taskAssignedSecondsAgo = s.lastAssignedTaskTime
465
+ ? Math.round((Date.now() - s.lastAssignedTaskTime) / 1000)
466
+ : null;
467
+ return {
468
+ id: s.id, // Include ID so we can match response back
469
+ name: sanitizeUnicode(s.name),
470
+ class: s.class,
471
+ status: s.status,
472
+ currentTask: sanitizeUnicode(s.currentTask || 'None'),
473
+ // Include the full assigned task so supervisor knows what the agent was asked to do
474
+ assignedTask: s.lastAssignedTask
475
+ ? sanitizeUnicode(truncateOrEmpty(s.lastAssignedTask, 500))
476
+ : 'No task assigned yet',
477
+ taskAssignedSecondsAgo,
478
+ tokensUsed: s.tokensUsed,
479
+ contextPercent: Math.round((s.contextUsed / 200000) * 100),
480
+ timeSinceActivity: Math.round((Date.now() - s.lastActivityTime) / 1000),
481
+ recentActivities: s.recentNarratives.map((n) => sanitizeUnicode(n.narrative)).slice(0, 5),
482
+ };
483
+ });
484
+ return customPrompt.replace('{{AGENT_DATA}}', JSON.stringify(agentData, null, 2));
485
+ }
486
+ function parseClaudeResponse(response, summaries) {
487
+ try {
488
+ const jsonStr = stripCodeFences(response);
489
+ const parsed = JSON.parse(jsonStr);
490
+ // Map agentAnalyses to agentSummaries (the field name in our type)
491
+ const agentAnalyses = (parsed.agentAnalyses || []).map((a) => ({
492
+ agentId: a.agentId || '',
493
+ agentName: a.agentName || '',
494
+ statusDescription: a.statusDescription || 'Unknown status',
495
+ progress: a.progress || 'idle',
496
+ recentWorkSummary: a.recentWorkSummary || 'No recent activity',
497
+ concerns: a.concerns || [],
498
+ }));
499
+ // Match agent IDs from summaries by name (more reliable than index)
500
+ agentAnalyses.forEach((analysis) => {
501
+ if (!analysis.agentId) {
502
+ // Find matching summary by name
503
+ const matchingSummary = summaries.find(s => s.name === analysis.agentName);
504
+ if (matchingSummary) {
505
+ analysis.agentId = matchingSummary.id;
506
+ }
507
+ }
508
+ });
509
+ return {
510
+ id: generateId(),
511
+ timestamp: Date.now(),
512
+ agentSummaries: agentAnalyses,
513
+ overallStatus: parsed.overallStatus || 'healthy',
514
+ insights: parsed.insights || [],
515
+ recommendations: parsed.recommendations || [],
516
+ rawResponse: response,
517
+ };
518
+ }
519
+ catch (err) {
520
+ log.error(' Failed to parse Claude response:', err);
521
+ log.error(' Raw response:', response);
522
+ // Return fallback report
523
+ return createFallbackReport(summaries);
524
+ }
525
+ }
526
+ function createFallbackReport(summaries) {
527
+ return {
528
+ id: generateId(),
529
+ timestamp: Date.now(),
530
+ agentSummaries: summaries.map((s) => ({
531
+ agentId: s.id,
532
+ agentName: s.name,
533
+ statusDescription: `${s.status} - ${s.currentTask || 'No current task'}`,
534
+ progress: s.status === 'working' ? 'on_track' : 'idle',
535
+ recentWorkSummary: s.recentNarratives[0]?.narrative || 'No recent activity',
536
+ })),
537
+ overallStatus: 'healthy',
538
+ insights: ['Unable to generate detailed analysis - using basic status'],
539
+ recommendations: [],
540
+ };
541
+ }
542
+ // ============================================================================
543
+ // History Management
544
+ // ============================================================================
545
+ /**
546
+ * Save a report's agent analyses to history
547
+ */
548
+ function saveReportToHistory(report) {
549
+ for (const analysis of report.agentSummaries) {
550
+ const entry = {
551
+ id: generateId(),
552
+ timestamp: report.timestamp,
553
+ reportId: report.id,
554
+ analysis,
555
+ };
556
+ addSupervisorHistoryEntry(supervisorHistory, analysis.agentId, entry);
557
+ }
558
+ // Persist to disk
559
+ saveSupervisorHistory(supervisorHistory);
560
+ log.log(` Saved history entries for ${report.agentSummaries.length} agents`);
561
+ }
562
+ /**
563
+ * Get supervisor history for a specific agent
564
+ */
565
+ export function getAgentSupervisorHistory(agentId) {
566
+ return getAgentHistoryFromStorage(supervisorHistory, agentId);
567
+ }
568
+ /**
569
+ * Delete supervisor history for an agent (call when agent is deleted)
570
+ */
571
+ export function deleteAgentHistory(agentId) {
572
+ deleteSupervisorHistory(supervisorHistory, agentId);
573
+ saveSupervisorHistory(supervisorHistory);
574
+ log.log(` Deleted history for agent ${agentId}`);
575
+ }
576
+ // ============================================================================
577
+ // Configuration
578
+ // ============================================================================
579
+ export function getConfig() {
580
+ return { ...config };
581
+ }
582
+ export function setConfig(updates) {
583
+ config = { ...config, ...updates };
584
+ // If disabling, cancel any pending report
585
+ if (!config.enabled && reportDebounceTimer) {
586
+ clearTimeout(reportDebounceTimer);
587
+ reportDebounceTimer = null;
588
+ }
589
+ emit('config_changed', config);
590
+ }
591
+ export function getLatestReport() {
592
+ return latestReport;
593
+ }
594
+ export function getStatus() {
595
+ return {
596
+ enabled: config.enabled,
597
+ autoReportOnComplete: config.autoReportOnComplete === true,
598
+ lastReportTime: latestReport?.timestamp || null,
599
+ // Reports are now event-driven (on task start/complete), not scheduled
600
+ nextReportTime: null,
601
+ };
602
+ }
603
+ // Global usage stats (shared across all sessions since they use same API key)
604
+ let globalUsage = null;
605
+ /**
606
+ * Update global usage stats from a /usage command response
607
+ */
608
+ export function updateGlobalUsage(agentId, agentName, usageData) {
609
+ globalUsage = {
610
+ session: usageData.session,
611
+ weeklyAllModels: usageData.weeklyAllModels,
612
+ weeklySonnet: usageData.weeklySonnet,
613
+ sourceAgentId: agentId,
614
+ sourceAgentName: agentName,
615
+ lastUpdated: Date.now(),
616
+ };
617
+ log.log(`✓ Updated global usage stats from ${agentName}:`);
618
+ log.log(` Session: ${usageData.session.percentUsed}% (resets ${usageData.session.resetTime})`);
619
+ log.log(` Weekly All: ${usageData.weeklyAllModels.percentUsed}% (resets ${usageData.weeklyAllModels.resetTime})`);
620
+ log.log(` Weekly Sonnet: ${usageData.weeklySonnet.percentUsed}% (resets ${usageData.weeklySonnet.resetTime})`);
621
+ // Emit event for real-time updates
622
+ emit('global_usage', globalUsage);
623
+ }
624
+ /**
625
+ * Get current global usage stats
626
+ */
627
+ export function getGlobalUsage() {
628
+ return globalUsage;
629
+ }
630
+ /**
631
+ * Request a usage refresh from any idle agent
632
+ * Returns the agent ID that will provide the data, or null if no agent is available
633
+ */
634
+ export async function requestUsageRefresh() {
635
+ console.log('[Supervisor] requestUsageRefresh called');
636
+ const agents = agentService.getAllAgents();
637
+ console.log('[Supervisor] All agents:', agents.map(a => ({ name: a.name, status: a.status, sessionId: a.sessionId })));
638
+ // Find an idle agent with a session
639
+ const idleAgent = agents.find(a => a.status === 'idle' && a.sessionId);
640
+ console.log('[Supervisor] Idle agent found:', idleAgent ? idleAgent.name : 'none');
641
+ if (!idleAgent) {
642
+ log.log('No idle agent available for usage refresh');
643
+ return null;
644
+ }
645
+ log.log(`Requesting usage refresh from ${idleAgent.name}`);
646
+ // Import dynamically to avoid circular dependency
647
+ const { sendSilentCommand } = await import('./claude-service.js');
648
+ try {
649
+ console.log('[Supervisor] Sending /usage command to', idleAgent.name);
650
+ await sendSilentCommand(idleAgent.id, '/usage');
651
+ console.log('[Supervisor] /usage command sent successfully');
652
+ return idleAgent.id;
653
+ }
654
+ catch (err) {
655
+ console.error('[Supervisor] Failed to send /usage command:', err);
656
+ log.error(`Failed to request usage from ${idleAgent.name}:`, err);
657
+ return null;
658
+ }
659
+ }