tide-commander 1.134.2 → 1.136.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 (50) hide show
  1. package/dist/assets/{BossLogsModal-DNQC-Ewi.js → BossLogsModal-B5wtQ9Fh.js} +1 -1
  2. package/dist/assets/{BossSpawnModal-DSpPdHfB.js → BossSpawnModal-B8twkNNt.js} +1 -1
  3. package/dist/assets/{ControlsModal-TPM0WTd7.js → ControlsModal-BKUVlO45.js} +1 -1
  4. package/dist/assets/{DockerLogsModal-BsEfK9sY.js → DockerLogsModal-Oa0WYexL.js} +1 -1
  5. package/dist/assets/{EmbeddedEditor-B7pzeHYJ.js → EmbeddedEditor-Cnkfr9vJ.js} +1 -1
  6. package/dist/assets/{GmailOAuthSetup-BD0lLD6-.js → GmailOAuthSetup-Cfsgqs5D.js} +1 -1
  7. package/dist/assets/{GoogleOAuthSetup-q_I-vMvg.js → GoogleOAuthSetup-BZCY7oIc.js} +1 -1
  8. package/dist/assets/{IframeModal-C0-b5Igy.js → IframeModal-BVaOFzOL.js} +1 -1
  9. package/dist/assets/{IntegrationsPanel-BPWcucBR.js → IntegrationsPanel-CdcwTSOY.js} +2 -2
  10. package/dist/assets/{LogViewerModal-D1puFD4z.js → LogViewerModal-U0o-ixDu.js} +1 -1
  11. package/dist/assets/{MonitoringModal-Dy3YmxW7.js → MonitoringModal-CdYBNEBK.js} +1 -1
  12. package/dist/assets/{PM2LogsModal-DFNTztfk.js → PM2LogsModal-cxSSfOTz.js} +1 -1
  13. package/dist/assets/{RestoreArchivedAreaModal-CokHqFKe.js → RestoreArchivedAreaModal-BkzbecLN.js} +1 -1
  14. package/dist/assets/{Scene2DCanvas-Dr1l-tVX.js → Scene2DCanvas-GGc__FVu.js} +1 -1
  15. package/dist/assets/{SceneManager-IcoT7Cdk.js → SceneManager-BWyie0tH.js} +1 -1
  16. package/dist/assets/{SkillsPanel-Cc1zvnfm.js → SkillsPanel-DS7QNt0-.js} +1 -1
  17. package/dist/assets/{SlackMultiInstanceSetup-Cjva8TJX.js → SlackMultiInstanceSetup-CjHaygiR.js} +1 -1
  18. package/dist/assets/{SpawnModal-DHKupuXe.js → SpawnModal-MWySBWkA.js} +1 -1
  19. package/dist/assets/{StatisticsModal-DZXDkn4d.js → StatisticsModal-D08htYhH.js} +1 -1
  20. package/dist/assets/{SubordinateAssignmentModal-DLJNIySK.js → SubordinateAssignmentModal-DULM1xpx.js} +1 -1
  21. package/dist/assets/{TriggerManagerPanel-Bd5ZGBaw.js → TriggerManagerPanel-nB6R7Nk4.js} +1 -1
  22. package/dist/assets/{WorkflowEditorPanel-D1YylR-A.js → WorkflowEditorPanel-CKgW8DhB.js} +1 -1
  23. package/dist/assets/{area-logos-C1EKS2S6.js → area-logos-CuU1qdkU.js} +1 -1
  24. package/dist/assets/{index-_wxXq7jG.js → index-8DzLSviG.js} +1 -1
  25. package/dist/assets/{index-0yPqh1QY.js → index-B6TRH1E2.js} +1 -1
  26. package/dist/assets/{index-7EfWB7v9.js → index-CLmNQzTF.js} +2 -2
  27. package/dist/assets/{index-E4hM-HPm.js → index-CQPpjjTd.js} +1 -1
  28. package/dist/assets/{index-IPKznDm8.js → index-CWYcGGCJ.js} +3 -3
  29. package/dist/assets/{index-BLrXnQWB.js → index-CWiGs6nn.js} +1 -1
  30. package/dist/assets/{index-BmUoFUZP.js → index-Cc_Ys4_9.js} +1 -1
  31. package/dist/assets/{index-C87bCtYJ.js → index-Csnk6THj.js} +1 -1
  32. package/dist/assets/{index-Ch70UsM8.js → index-ih8yuWhL.js} +1 -1
  33. package/dist/assets/{main-DhXGHPKx.js → main-CHE4wC7P.js} +113 -113
  34. package/dist/assets/main-DM0iS4EN.css +1 -0
  35. package/dist/assets/{web-CHTqiViU.js → web-C4yrL2Hp.js} +1 -1
  36. package/dist/assets/{web-g3yRdGTs.js → web-D1Ur_Mtd.js} +1 -1
  37. package/dist/assets/{web-CDTjrhUg.js → web-dmnfXY_q.js} +1 -1
  38. package/dist/index.html +2 -2
  39. package/dist/locales/en/terminal.json +13 -0
  40. package/dist/src/packages/server/claude/interactive/interactive-runner.js +8 -0
  41. package/dist/src/packages/server/claude/runner/recovery-store.js +102 -6
  42. package/dist/src/packages/server/claude/runner.js +30 -10
  43. package/dist/src/packages/server/data/index.js +1 -0
  44. package/dist/src/packages/server/services/auto-collapse-service.js +8 -2
  45. package/dist/src/packages/server/services/collapse-context.js +52 -12
  46. package/dist/src/packages/server/services/subagent-jsonl-watcher.js +112 -82
  47. package/dist/src/packages/server/websocket/handlers/agent-handler.js +3 -0
  48. package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +40 -30
  49. package/package.json +1 -1
  50. package/dist/assets/main-rrlO1Py8.css +0 -1
@@ -19,9 +19,12 @@ import { createLogger } from '../utils/logger.js';
19
19
  import { encodeProjectPath } from '../claude/session-loader.js';
20
20
  const log = createLogger('SubagentJSONL');
21
21
  const MAX_ENTRIES_PER_BROADCAST = 20;
22
- const IDLE_TIMEOUT_MS = 180_000; // Stop after 3 minutes of no file changes (subagents can run long builds)
22
+ const IDLE_TIMEOUT_MS = 600_000; // Stop after 10 minutes of no file changes (long thinking/tool gaps on xhigh effort)
23
23
  const POLL_INTERVAL_MS = 2_000; // Poll file every 2s (fallback if fs.watch misses events)
24
- const MAX_WATCH_DURATION_MS = 900_000; // Hard limit: 15 minutes max per watcher
24
+ const MAX_WATCH_DURATION_MS = 3_600_000; // Hard limit: 60 minutes max per watcher (background agents routinely run 20-30 min)
25
+ const BIND_POLL_INTERVAL_MS = 1_000; // Retry transcript binding every second until found
26
+ const BIND_GIVE_UP_MS = 120_000; // Stop an unbound watcher after 2 minutes (no transcript ever appeared)
27
+ const FALLBACK_BIND_GRACE_MS = 15_000; // Give meta.json time to appear before the newest-file fallback
25
28
  // Key param extraction per tool name
26
29
  const TOOL_KEY_PARAMS = {
27
30
  Bash: 'command',
@@ -48,7 +51,7 @@ export function getSubagentsDir(cwd, sessionId) {
48
51
  /**
49
52
  * Start watching for a subagent's JSONL file
50
53
  */
51
- export function startWatching(toolUseId, parentAgentId, subagentsDir, onBroadcast, onToolResult) {
54
+ export function startWatching(toolUseId, parentAgentId, subagentsDir, onBroadcast, onToolResult, options) {
52
55
  if (activeWatchers.has(toolUseId)) {
53
56
  log.warn(`[Watcher] Already watching for toolUseId=${toolUseId}`);
54
57
  return;
@@ -57,6 +60,8 @@ export function startWatching(toolUseId, parentAgentId, subagentsDir, onBroadcas
57
60
  toolUseId,
58
61
  parentAgentId,
59
62
  subagentsDir,
63
+ startedAt: Date.now(),
64
+ startAtEnd: options?.startAtEnd === true,
60
65
  readPosition: 0,
61
66
  lineBuffer: '',
62
67
  pendingEntries: [],
@@ -76,6 +81,10 @@ export function startWatching(toolUseId, parentAgentId, subagentsDir, onBroadcas
76
81
  // Try to find existing files first, then watch for new ones
77
82
  tryFindAndWatchFile(watcher);
78
83
  }
84
+ /** Whether a live watcher exists for this Task/Agent toolUseId. */
85
+ export function isWatching(toolUseId) {
86
+ return activeWatchers.has(toolUseId);
87
+ }
79
88
  /**
80
89
  * Signal that the subagent's Task tool has completed.
81
90
  * For team agents, the Task returns immediately but the subagent keeps running.
@@ -105,6 +114,7 @@ function doStop(watcher) {
105
114
  }
106
115
  flushEntries(watcher);
107
116
  // Cleanup all timers and watchers
117
+ clearBindTimer(watcher);
108
118
  watcher.dirWatcher?.close();
109
119
  watcher.fileWatcher?.close();
110
120
  if (watcher.broadcastTimer)
@@ -138,91 +148,42 @@ function resetIdleTimer(watcher) {
138
148
  }, IDLE_TIMEOUT_MS);
139
149
  }
140
150
  /**
141
- * Try to find an existing JSONL file or watch the directory for new ones
151
+ * Bind this watcher to its subagent's transcript inside the shared
152
+ * subagents/ directory, retrying until it appears.
153
+ *
154
+ * Exact binding: each subagent writes agent-<id>.meta.json containing the
155
+ * parent Task/Agent toolUseId — match it against ours. The old "newest
156
+ * .jsonl in the dir" heuristic mis-binds when several subagents run
157
+ * concurrently: at tool_start time the newest file usually belongs to a
158
+ * sibling (this subagent hasn't written anything yet), so this subagent's
159
+ * tool_results never bridge and its Bash cards spin forever.
142
160
  */
143
161
  function tryFindAndWatchFile(watcher) {
144
- const { subagentsDir } = watcher;
145
- // Check if directory exists yet
146
- if (!fs.existsSync(subagentsDir)) {
147
- // Directory doesn't exist yet - watch parent for it to appear
148
- const parentDir = path.dirname(subagentsDir);
149
- if (!fs.existsSync(parentDir)) {
150
- // Session directory doesn't exist either - retry periodically
151
- const retryInterval = setInterval(() => {
152
- if (watcher.stopped) {
153
- clearInterval(retryInterval);
154
- return;
155
- }
156
- if (fs.existsSync(subagentsDir)) {
157
- clearInterval(retryInterval);
158
- watchDirectory(watcher);
159
- }
160
- }, 500);
161
- // Give up after 30 seconds
162
- setTimeout(() => clearInterval(retryInterval), 30000);
162
+ if (tryBindTranscript(watcher))
163
+ return;
164
+ // Retry on a timer — also covers the "session/subagents directory doesn't
165
+ // exist yet" case (readdir fails keep polling).
166
+ watcher.bindTimer = setInterval(() => {
167
+ if (watcher.stopped) {
168
+ clearBindTimer(watcher);
163
169
  return;
164
170
  }
165
- // Watch parent directory for subagents/ to appear
166
- try {
167
- const parentWatcher = fs.watch(parentDir, (eventType, filename) => {
168
- if (watcher.stopped)
169
- return;
170
- if (filename === 'subagents' && fs.existsSync(subagentsDir)) {
171
- parentWatcher.close();
172
- watchDirectory(watcher);
173
- }
174
- });
175
- parentWatcher.on('error', () => parentWatcher.close());
176
- }
177
- catch {
178
- log.warn(`[Watcher] Failed to watch parent dir: ${parentDir}`);
179
- }
180
- return;
181
- }
182
- // Directory exists - look for existing files or watch for new ones
183
- watchDirectory(watcher);
184
- }
185
- /**
186
- * Watch the subagents directory for JSONL files
187
- */
188
- function watchDirectory(watcher) {
189
- if (watcher.stopped)
190
- return;
191
- const { subagentsDir } = watcher;
192
- // Check for existing .jsonl files
193
- try {
194
- const files = fs.readdirSync(subagentsDir).filter(f => f.endsWith('.jsonl'));
195
- if (files.length > 0) {
196
- // Pick the most recently modified file
197
- let newest = files[0];
198
- let newestMtime = 0;
199
- for (const f of files) {
200
- try {
201
- const stat = fs.statSync(path.join(subagentsDir, f));
202
- if (stat.mtimeMs > newestMtime) {
203
- newestMtime = stat.mtimeMs;
204
- newest = f;
205
- }
206
- }
207
- catch { /* skip */ }
208
- }
209
- startFileWatch(watcher, path.join(subagentsDir, newest));
171
+ if (tryBindTranscript(watcher)) {
172
+ clearBindTimer(watcher);
210
173
  return;
211
174
  }
212
- }
213
- catch { /* directory may have been removed */ }
214
- // No files yet - watch for new ones
175
+ if (Date.now() - watcher.startedAt > BIND_GIVE_UP_MS) {
176
+ log.warn(`[Watcher] No transcript appeared for toolUseId=${watcher.toolUseId} after ${BIND_GIVE_UP_MS / 1000}s, giving up`);
177
+ doStop(watcher);
178
+ }
179
+ }, BIND_POLL_INTERVAL_MS);
180
+ // React faster than the poll when the directory already exists.
215
181
  try {
216
- watcher.dirWatcher = fs.watch(subagentsDir, (eventType, filename) => {
217
- if (watcher.stopped)
182
+ watcher.dirWatcher = fs.watch(watcher.subagentsDir, () => {
183
+ if (watcher.stopped || watcher.jsonlPath)
218
184
  return;
219
- if (filename && filename.endsWith('.jsonl') && !watcher.jsonlPath) {
220
- const filePath = path.join(subagentsDir, filename);
221
- if (fs.existsSync(filePath)) {
222
- watcher.dirWatcher?.close();
223
- watcher.dirWatcher = undefined;
224
- startFileWatch(watcher, filePath);
225
- }
185
+ if (tryBindTranscript(watcher)) {
186
+ clearBindTimer(watcher);
226
187
  }
227
188
  });
228
189
  watcher.dirWatcher.on('error', () => {
@@ -230,9 +191,69 @@ function watchDirectory(watcher) {
230
191
  watcher.dirWatcher = undefined;
231
192
  });
232
193
  }
194
+ catch { /* directory not created yet — the bind poll covers it */ }
195
+ }
196
+ function clearBindTimer(watcher) {
197
+ if (watcher.bindTimer) {
198
+ clearInterval(watcher.bindTimer);
199
+ watcher.bindTimer = undefined;
200
+ }
201
+ }
202
+ /**
203
+ * One binding attempt. Returns true when a file watch was started.
204
+ *
205
+ * Fallback for CLI layouts that don't write meta files: after a grace
206
+ * period, bind to the newest .jsonl that has no .meta.json sibling. Files
207
+ * WITH a meta sibling are never eligible — a non-matching meta means the
208
+ * file belongs to a different Task/Agent call.
209
+ */
210
+ function tryBindTranscript(watcher) {
211
+ const { subagentsDir } = watcher;
212
+ let files;
213
+ try {
214
+ files = fs.readdirSync(subagentsDir);
215
+ }
233
216
  catch {
234
- log.warn(`[Watcher] Failed to watch directory: ${subagentsDir}`);
217
+ return false; // directory doesn't exist yet
218
+ }
219
+ // Exact match via meta.json (agent-<id>.meta.json → agent-<id>.jsonl)
220
+ for (const f of files) {
221
+ if (!f.endsWith('.meta.json'))
222
+ continue;
223
+ try {
224
+ const meta = JSON.parse(fs.readFileSync(path.join(subagentsDir, f), 'utf8'));
225
+ if (meta?.toolUseId !== watcher.toolUseId)
226
+ continue;
227
+ const jsonl = f.replace(/\.meta\.json$/, '.jsonl');
228
+ if (files.includes(jsonl)) {
229
+ startFileWatch(watcher, path.join(subagentsDir, jsonl));
230
+ return true;
231
+ }
232
+ return false; // our meta exists but the transcript doesn't yet — keep waiting
233
+ }
234
+ catch { /* unreadable/partially-written meta — skip */ }
235
+ }
236
+ // Fallback: newest orphan .jsonl (no meta sibling), after a grace period
237
+ // that gives our meta.json time to appear.
238
+ if (Date.now() - watcher.startedAt < FALLBACK_BIND_GRACE_MS)
239
+ return false;
240
+ const orphans = files.filter((f) => f.endsWith('.jsonl') && !files.includes(f.replace(/\.jsonl$/, '.meta.json')));
241
+ if (orphans.length === 0)
242
+ return false;
243
+ let newest = orphans[0];
244
+ let newestMtime = 0;
245
+ for (const f of orphans) {
246
+ try {
247
+ const stat = fs.statSync(path.join(subagentsDir, f));
248
+ if (stat.mtimeMs > newestMtime) {
249
+ newestMtime = stat.mtimeMs;
250
+ newest = f;
251
+ }
252
+ }
253
+ catch { /* skip */ }
235
254
  }
255
+ startFileWatch(watcher, path.join(subagentsDir, newest));
256
+ return true;
236
257
  }
237
258
  /**
238
259
  * Start watching a specific JSONL file for new content
@@ -240,8 +261,17 @@ function watchDirectory(watcher) {
240
261
  function startFileWatch(watcher, filePath) {
241
262
  if (watcher.stopped)
242
263
  return;
264
+ // Binding done — the directory watcher is no longer needed.
265
+ watcher.dirWatcher?.close();
266
+ watcher.dirWatcher = undefined;
243
267
  watcher.jsonlPath = filePath;
244
- log.log(`[Watcher] Found JSONL file: ${filePath} for toolUseId=${watcher.toolUseId}`);
268
+ log.log(`[Watcher] Found JSONL file: ${filePath} for toolUseId=${watcher.toolUseId}${watcher.startAtEnd ? ' (tail from EOF)' : ''}`);
269
+ if (watcher.startAtEnd) {
270
+ try {
271
+ watcher.readPosition = fs.statSync(filePath).size;
272
+ }
273
+ catch { /* file vanished — readNewLines will retry from 0 */ }
274
+ }
245
275
  // Read existing content
246
276
  readNewLines(watcher);
247
277
  // Start idle timer
@@ -910,6 +910,9 @@ export async function handleUpdateAgentProperties(ctx, payload) {
910
910
  if (updates.autoCollapseTz !== undefined) {
911
911
  agentUpdates.autoCollapseTz = updates.autoCollapseTz || undefined;
912
912
  }
913
+ if (updates.autoCollapsePrompt !== undefined) {
914
+ agentUpdates.autoCollapsePrompt = updates.autoCollapsePrompt || undefined;
915
+ }
913
916
  // Apply agent property updates if any
914
917
  // agentService.updateAgent tracks pending property changes for notification on next command
915
918
  if (Object.keys(agentUpdates).length > 0) {
@@ -6,7 +6,7 @@ import { parseAllFormats } from '../handlers/agent-handler.js';
6
6
  import { agentService, runtimeService } from '../../services/index.js';
7
7
  import { logger, formatToolActivity } from '../../utils/index.js';
8
8
  import { parseBossDelegation, parseBossSpawn, getBossForSubordinate, clearDelegation } from '../handlers/boss-response-handler.js';
9
- import { startWatching as startJsonlWatching, stopWatching as stopJsonlWatching, getSubagentsDir } from '../../services/subagent-jsonl-watcher.js';
9
+ import { startWatching as startJsonlWatching, stopWatching as stopJsonlWatching, isWatching as isJsonlWatching, getSubagentsDir } from '../../services/subagent-jsonl-watcher.js';
10
10
  const log = logger.ws;
11
11
  const MAX_SYNTHETIC_DIFF_FILE_BYTES = 256 * 1024;
12
12
  function sanitizeParsedContextStats(stats) {
@@ -31,6 +31,38 @@ export function setupRuntimeListeners(ctx) {
31
31
  const bossAccumulatedText = new Map();
32
32
  // Track agents currently in compacting state so we can clear it when they resume
33
33
  const compactingAgents = new Set();
34
+ function armSubagentJsonlWatcher(agentId, toolUseId, options) {
35
+ const parentAgent = agentService.getAgent(agentId);
36
+ if (!parentAgent?.sessionId)
37
+ return;
38
+ const subagentsDir = getSubagentsDir(parentAgent.cwd, parentAgent.sessionId);
39
+ startJsonlWatching(toolUseId, agentId, subagentsDir, (watchedToolUseId, parentAgentId, entries) => {
40
+ ctx.broadcast({
41
+ type: 'subagent_stream',
42
+ payload: { toolUseId: watchedToolUseId, parentAgentId, entries },
43
+ });
44
+ }, (watchedToolUseId, parentAgentId, result) => {
45
+ // The parent CLI stream never echoes subagent tool_result events
46
+ // (only tool_use), so without this bridge the subagent Bash
47
+ // cards in the terminal keep their "running" spinner forever.
48
+ // Mirror the parent-agent behavior: only Bash results get a card.
49
+ if (result.toolName !== 'Bash')
50
+ return;
51
+ ctx.broadcast({
52
+ type: 'event',
53
+ payload: {
54
+ agentId: parentAgentId,
55
+ type: 'tool_result',
56
+ toolName: 'Bash',
57
+ toolOutput: result.output,
58
+ parentToolUseId: watchedToolUseId,
59
+ // Subagent-internal tool_use id — matches the uuid of the
60
+ // corresponding tool_start card so the client pairs exactly.
61
+ uuid: result.toolUseId,
62
+ },
63
+ });
64
+ }, options);
65
+ }
34
66
  runtimeService.on('event', (agentId, event) => {
35
67
  // Clear compacting state when agent resumes with new output after compaction
36
68
  if (compactingAgents.has(agentId) && event.type !== 'compacting') {
@@ -100,35 +132,7 @@ export function setupRuntimeListeners(ctx) {
100
132
  ctx.sendActivity(agentId, `Spawned subagent: ${subagent.name} (${subagent.subagentType})`);
101
133
  log.log(`[Subagent] Broadcast subagent_started: ${subagent.name} (${subagent.id})`);
102
134
  // Start streaming JSONL file for this subagent
103
- if (parentAgent?.sessionId) {
104
- const subagentsDir = getSubagentsDir(parentAgent.cwd, parentAgent.sessionId);
105
- startJsonlWatching(event.toolUseId, agentId, subagentsDir, (toolUseId, parentAgentId, entries) => {
106
- ctx.broadcast({
107
- type: 'subagent_stream',
108
- payload: { toolUseId, parentAgentId, entries },
109
- });
110
- }, (toolUseId, parentAgentId, result) => {
111
- // The parent CLI stream never echoes subagent tool_result events
112
- // (only tool_use), so without this bridge the subagent Bash
113
- // cards in the terminal keep their "running" spinner forever.
114
- // Mirror the parent-agent behavior: only Bash results get a card.
115
- if (result.toolName !== 'Bash')
116
- return;
117
- ctx.broadcast({
118
- type: 'event',
119
- payload: {
120
- agentId: parentAgentId,
121
- type: 'tool_result',
122
- toolName: 'Bash',
123
- toolOutput: result.output,
124
- parentToolUseId: toolUseId,
125
- // Subagent-internal tool_use id — matches the uuid of the
126
- // corresponding tool_start card so the client pairs exactly.
127
- uuid: result.toolUseId,
128
- },
129
- });
130
- });
131
- }
135
+ armSubagentJsonlWatcher(agentId, event.toolUseId);
132
136
  }
133
137
  }
134
138
  }
@@ -171,6 +175,12 @@ export function setupRuntimeListeners(ctx) {
171
175
  }
172
176
  // Forward subagent internal tool activity to client (events with parentToolUseId)
173
177
  if (event.parentToolUseId && event.type === 'tool_start' && event.toolName !== 'Task' && event.toolName !== 'Agent') {
178
+ // Self-heal: if this subagent's watcher is gone (idle/max timeout hit,
179
+ // server restarted mid-run) or was never armed, re-arm it tailing from
180
+ // EOF so this tool's result can still resolve its card's spinner.
181
+ if (!isJsonlWatching(event.parentToolUseId)) {
182
+ armSubagentJsonlWatcher(agentId, event.parentToolUseId, { startAtEnd: true });
183
+ }
174
184
  const toolDesc = formatToolActivity(event.toolName, event.toolInput);
175
185
  ctx.broadcast({
176
186
  type: 'subagent_output',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tide-commander",
3
- "version": "1.134.2",
3
+ "version": "1.136.0",
4
4
  "description": "Visual multi-agent orchestrator and manager for Claude Code with 3D/2D interface",
5
5
  "repository": {
6
6
  "type": "git",