recoder-code 2.5.2 → 2.5.3

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 (44) hide show
  1. package/dist/index.js +0 -0
  2. package/dist/src/commands/context/index.js +2 -2
  3. package/dist/src/commands/mcp/marketplace.d.ts +6 -0
  4. package/dist/src/commands/mcp/marketplace.js +448 -0
  5. package/dist/src/commands/mcp.js +2 -0
  6. package/dist/src/commands/parallel.d.ts +20 -0
  7. package/dist/src/commands/parallel.js +133 -0
  8. package/dist/src/commands/recoderWeb.js +184 -5
  9. package/dist/src/commands/web/diff.d.ts +13 -0
  10. package/dist/src/commands/web/diff.js +235 -0
  11. package/dist/src/commands/web/link.d.ts +11 -0
  12. package/dist/src/commands/web/link.js +96 -0
  13. package/dist/src/commands/web/pull.d.ts +13 -0
  14. package/dist/src/commands/web/pull.js +203 -0
  15. package/dist/src/commands/web/status.d.ts +10 -0
  16. package/dist/src/commands/web/status.js +104 -0
  17. package/dist/src/commands/web/unlink.d.ts +10 -0
  18. package/dist/src/commands/web/unlink.js +45 -0
  19. package/dist/src/commands/web/watch.d.ts +14 -0
  20. package/dist/src/commands/web/watch.js +360 -0
  21. package/dist/src/commands/web.js +12 -0
  22. package/dist/src/config/config.js +6 -2
  23. package/dist/src/config/defaultMcpServers.d.ts +1 -0
  24. package/dist/src/config/defaultMcpServers.js +46 -0
  25. package/dist/src/gemini.js +10 -0
  26. package/dist/src/parallel/git-utils.d.ts +42 -0
  27. package/dist/src/parallel/git-utils.js +161 -0
  28. package/dist/src/parallel/index.d.ts +14 -0
  29. package/dist/src/parallel/index.js +14 -0
  30. package/dist/src/parallel/parallel-mode.d.ts +48 -0
  31. package/dist/src/parallel/parallel-mode.js +224 -0
  32. package/dist/src/services/AgentBridgeService.d.ts +61 -0
  33. package/dist/src/services/AgentBridgeService.js +253 -0
  34. package/dist/src/services/BuiltinCommandLoader.js +7 -0
  35. package/dist/src/services/PlatformSyncService.d.ts +154 -0
  36. package/dist/src/services/PlatformSyncService.js +588 -0
  37. package/dist/src/ui/commands/workflowCommands.d.ts +16 -0
  38. package/dist/src/ui/commands/workflowCommands.js +291 -0
  39. package/dist/src/ui/commands/workspaceCommand.d.ts +11 -0
  40. package/dist/src/ui/commands/workspaceCommand.js +329 -0
  41. package/dist/src/zed-integration/schema.d.ts +30 -30
  42. package/package.json +29 -10
  43. package/src/postinstall.cjs +3 -2
  44. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Workflow & utility CLI commands for Recoder Code
3
+ *
4
+ * Slash commands:
5
+ * /workflow list - List workflows for current project
6
+ * /workflow run <id> - Execute a workflow
7
+ * /workflow status <exec-id> - Check execution status
8
+ * /analytics - Show agent analytics summary
9
+ * /cost - Show cost summary
10
+ * /whoami - Show current user info
11
+ */
12
+ import { CommandKind } from './types.js';
13
+ import { RecoderAuthService } from '../../services/RecoderAuthService.js';
14
+ const BACKEND_URL = process.env['RECODER_BACKEND_URL'] || process.env['DOCKER_BACKEND_URL'] || 'https://api.recoder.xyz';
15
+ const authService = new RecoderAuthService();
16
+ // ---------------------------------------------------------------------------
17
+ // Helpers
18
+ // ---------------------------------------------------------------------------
19
+ async function authenticatedGet(path) {
20
+ const token = await authService.getAccessToken();
21
+ if (!token) {
22
+ throw new Error('Not authenticated. Run /recoder-login first.');
23
+ }
24
+ return fetch(`${BACKEND_URL}${path}`, {
25
+ method: 'GET',
26
+ headers: {
27
+ 'Authorization': `Bearer ${token}`,
28
+ 'Content-Type': 'application/json',
29
+ },
30
+ });
31
+ }
32
+ async function authenticatedPost(path, body) {
33
+ const token = await authService.getAccessToken();
34
+ if (!token) {
35
+ throw new Error('Not authenticated. Run /recoder-login first.');
36
+ }
37
+ return fetch(`${BACKEND_URL}${path}`, {
38
+ method: 'POST',
39
+ headers: {
40
+ 'Authorization': `Bearer ${token}`,
41
+ 'Content-Type': 'application/json',
42
+ },
43
+ body: body ? JSON.stringify(body) : undefined,
44
+ });
45
+ }
46
+ function errorMessage(err) {
47
+ return err instanceof Error ? err.message : 'Unknown error';
48
+ }
49
+ // ---------------------------------------------------------------------------
50
+ // /workflow
51
+ // ---------------------------------------------------------------------------
52
+ export const workflowCommand = {
53
+ name: 'workflow',
54
+ description: 'Manage and run workflows',
55
+ kind: CommandKind.BUILT_IN,
56
+ subCommands: [
57
+ // /workflow list
58
+ {
59
+ name: 'list',
60
+ description: 'List workflows for an agent (e.g., /workflow list <agent-id>)',
61
+ kind: CommandKind.BUILT_IN,
62
+ action: async (_context, args) => {
63
+ const agentId = args?.trim();
64
+ if (!agentId) {
65
+ return {
66
+ type: 'message',
67
+ messageType: 'info',
68
+ content: 'Usage: /workflow list <agent-id>',
69
+ };
70
+ }
71
+ try {
72
+ const res = await authenticatedGet(`/api/workflows/agent/${encodeURIComponent(agentId)}`);
73
+ if (!res.ok) {
74
+ return { type: 'message', messageType: 'error', content: `Failed (${res.status}): ${await res.text()}` };
75
+ }
76
+ const workflows = await res.json();
77
+ const list = Array.isArray(workflows) ? workflows : workflows.data || [];
78
+ if (list.length === 0) {
79
+ return { type: 'message', messageType: 'info', content: 'No workflows found for this agent.' };
80
+ }
81
+ const lines = [
82
+ '┌──────────────────────────────────────────────────┐',
83
+ '│ Workflows │',
84
+ '├──────────────────────────────────────────────────┤',
85
+ ];
86
+ for (const wf of list) {
87
+ lines.push(`│ ${(wf.name || 'Untitled').padEnd(47)}│`);
88
+ lines.push(`│ ID: ${String(wf.id).padEnd(41)}│`);
89
+ lines.push(`│ Status: ${String(wf.status || 'draft').padEnd(37)}│`);
90
+ lines.push('│ │');
91
+ }
92
+ lines.push(`├──────────────────────────────────────────────────┤`);
93
+ lines.push(`│ Total: ${String(list.length).padEnd(40)}│`);
94
+ lines.push('│ Run with: /workflow run <id> │');
95
+ lines.push('└──────────────────────────────────────────────────┘');
96
+ return { type: 'message', messageType: 'info', content: lines.join('\n') };
97
+ }
98
+ catch (err) {
99
+ return { type: 'message', messageType: 'error', content: `Error listing workflows: ${errorMessage(err)}` };
100
+ }
101
+ },
102
+ },
103
+ // /workflow run <id>
104
+ {
105
+ name: 'run',
106
+ description: 'Execute a workflow (e.g., /workflow run <workflow-id>)',
107
+ kind: CommandKind.BUILT_IN,
108
+ action: async (_context, args) => {
109
+ const workflowId = args?.trim();
110
+ if (!workflowId) {
111
+ return { type: 'message', messageType: 'info', content: 'Usage: /workflow run <workflow-id>' };
112
+ }
113
+ try {
114
+ const res = await authenticatedPost(`/api/workflows/${encodeURIComponent(workflowId)}/execute`);
115
+ if (!res.ok) {
116
+ return { type: 'message', messageType: 'error', content: `Failed (${res.status}): ${await res.text()}` };
117
+ }
118
+ const execution = await res.json();
119
+ return {
120
+ type: 'message',
121
+ messageType: 'info',
122
+ content: [
123
+ '┌──────────────────────────────────────────────────┐',
124
+ '│ Workflow Execution Started │',
125
+ '├──────────────────────────────────────────────────┤',
126
+ `│ Execution ID: ${String(execution.id || 'unknown').padEnd(33)}│`,
127
+ `│ Status: ${String(execution.status || 'pending').padEnd(33)}│`,
128
+ '│ │',
129
+ '│ Check progress: /workflow status <exec-id> │',
130
+ '└──────────────────────────────────────────────────┘',
131
+ ].join('\n'),
132
+ };
133
+ }
134
+ catch (err) {
135
+ return { type: 'message', messageType: 'error', content: `Error executing workflow: ${errorMessage(err)}` };
136
+ }
137
+ },
138
+ },
139
+ // /workflow status <execution-id>
140
+ {
141
+ name: 'status',
142
+ description: 'Check execution status (e.g., /workflow status <execution-id>)',
143
+ kind: CommandKind.BUILT_IN,
144
+ action: async (_context, args) => {
145
+ const executionId = args?.trim();
146
+ if (!executionId) {
147
+ return { type: 'message', messageType: 'info', content: 'Usage: /workflow status <execution-id>' };
148
+ }
149
+ try {
150
+ const res = await authenticatedGet(`/api/workflows/executions/${encodeURIComponent(executionId)}`);
151
+ if (!res.ok) {
152
+ return { type: 'message', messageType: 'error', content: `Failed (${res.status}): ${await res.text()}` };
153
+ }
154
+ const exec = await res.json();
155
+ const duration = exec.duration_seconds != null ? `${exec.duration_seconds}s` : 'n/a';
156
+ const cost = exec.total_cost != null ? `$${Number(exec.total_cost).toFixed(4)}` : 'n/a';
157
+ return {
158
+ type: 'message',
159
+ messageType: 'info',
160
+ content: [
161
+ '┌──────────────────────────────────────────────────┐',
162
+ '│ Execution Status │',
163
+ '├──────────────────────────────────────────────────┤',
164
+ `│ ID: ${String(exec.id).padEnd(37)}│`,
165
+ `│ Status: ${String(exec.status).padEnd(37)}│`,
166
+ `│ Duration: ${duration.padEnd(37)}│`,
167
+ `│ Cost: ${cost.padEnd(37)}│`,
168
+ `│ Started: ${String(exec.started_at || 'n/a').substring(0, 37).padEnd(37)}│`,
169
+ '└──────────────────────────────────────────────────┘',
170
+ ].join('\n'),
171
+ };
172
+ }
173
+ catch (err) {
174
+ return { type: 'message', messageType: 'error', content: `Error fetching status: ${errorMessage(err)}` };
175
+ }
176
+ },
177
+ },
178
+ ],
179
+ // Default: show help
180
+ action: async () => ({
181
+ type: 'message',
182
+ messageType: 'info',
183
+ content: [
184
+ 'Workflow Commands',
185
+ '',
186
+ ' /workflow list <agent-id> List workflows for an agent',
187
+ ' /workflow run <workflow-id> Execute a workflow',
188
+ ' /workflow status <execution-id> Check execution status',
189
+ ].join('\n'),
190
+ }),
191
+ };
192
+ // ---------------------------------------------------------------------------
193
+ // /analytics
194
+ // ---------------------------------------------------------------------------
195
+ export const analyticsCommand = {
196
+ name: 'analytics',
197
+ description: 'Show agent analytics summary',
198
+ kind: CommandKind.BUILT_IN,
199
+ action: async () => {
200
+ try {
201
+ const res = await authenticatedGet('/api/analytics/summary');
202
+ if (!res.ok) {
203
+ return { type: 'message', messageType: 'error', content: `Failed (${res.status}): ${await res.text()}` };
204
+ }
205
+ const data = await res.json();
206
+ const summary = data.data || data;
207
+ const lines = [
208
+ '┌──────────────────────────────────────────────────┐',
209
+ '│ Agent Analytics Summary │',
210
+ '├──────────────────────────────────────────────────┤',
211
+ `│ Total runs: ${String(summary.total_runs ?? 'n/a').padEnd(29)}│`,
212
+ `│ Successful: ${String(summary.successful_runs ?? 'n/a').padEnd(29)}│`,
213
+ `│ Failed: ${String(summary.failed_runs ?? 'n/a').padEnd(29)}│`,
214
+ `│ Avg duration: ${String(summary.avg_duration ? `${summary.avg_duration}s` : 'n/a').padEnd(29)}│`,
215
+ `│ Total cost: ${String(summary.total_cost ? `$${Number(summary.total_cost).toFixed(4)}` : 'n/a').padEnd(29)}│`,
216
+ `│ Active agents: ${String(summary.active_agents ?? 'n/a').padEnd(29)}│`,
217
+ '└──────────────────────────────────────────────────┘',
218
+ ];
219
+ return { type: 'message', messageType: 'info', content: lines.join('\n') };
220
+ }
221
+ catch (err) {
222
+ return { type: 'message', messageType: 'error', content: `Error fetching analytics: ${errorMessage(err)}` };
223
+ }
224
+ },
225
+ };
226
+ // ---------------------------------------------------------------------------
227
+ // /cost
228
+ // ---------------------------------------------------------------------------
229
+ export const costCommand = {
230
+ name: 'cost',
231
+ description: 'Show cost summary for the current billing period',
232
+ kind: CommandKind.BUILT_IN,
233
+ action: async () => {
234
+ try {
235
+ const res = await authenticatedGet('/api/billing/usage');
236
+ if (!res.ok) {
237
+ return { type: 'message', messageType: 'error', content: `Failed (${res.status}): ${await res.text()}` };
238
+ }
239
+ const data = await res.json();
240
+ const usage = data.data || data;
241
+ const lines = [
242
+ '┌──────────────────────────────────────────────────┐',
243
+ '│ Cost Summary │',
244
+ '├──────────────────────────────────────────────────┤',
245
+ `│ Period: ${String(usage.period || 'current').padEnd(29)}│`,
246
+ `│ Total spent: ${String(usage.total_cost ? `$${Number(usage.total_cost).toFixed(4)}` : '$0.00').padEnd(29)}│`,
247
+ `│ Budget limit: ${String(usage.budget_limit ? `$${Number(usage.budget_limit).toFixed(2)}` : 'none').padEnd(29)}│`,
248
+ `│ Budget remaining: ${String(usage.budget_remaining ? `$${Number(usage.budget_remaining).toFixed(2)}` : 'n/a').padEnd(29)}│`,
249
+ `│ API calls: ${String(usage.api_call_count ?? 'n/a').padEnd(29)}│`,
250
+ `│ Tokens used: ${String(usage.total_tokens ?? 'n/a').padEnd(29)}│`,
251
+ '└──────────────────────────────────────────────────┘',
252
+ ];
253
+ return { type: 'message', messageType: 'info', content: lines.join('\n') };
254
+ }
255
+ catch (err) {
256
+ return { type: 'message', messageType: 'error', content: `Error fetching cost data: ${errorMessage(err)}` };
257
+ }
258
+ },
259
+ };
260
+ // ---------------------------------------------------------------------------
261
+ // /whoami
262
+ // ---------------------------------------------------------------------------
263
+ export const whoamiCommand = {
264
+ name: 'whoami',
265
+ description: 'Show current authenticated user info',
266
+ kind: CommandKind.BUILT_IN,
267
+ action: async () => {
268
+ try {
269
+ const res = await authenticatedGet('/api/user/profile');
270
+ if (!res.ok) {
271
+ return { type: 'message', messageType: 'error', content: `Failed (${res.status}): ${await res.text()}` };
272
+ }
273
+ const data = await res.json();
274
+ const user = data.data || data;
275
+ const lines = [
276
+ '┌──────────────────────────────────────────────────┐',
277
+ '│ Current User │',
278
+ '├──────────────────────────────────────────────────┤',
279
+ `│ Name: ${String(user.name || user.display_name || 'n/a').padEnd(39)}│`,
280
+ `│ Email: ${String(user.email || 'n/a').padEnd(39)}│`,
281
+ `│ Plan: ${String(user.plan || user.tier || 'free').padEnd(39)}│`,
282
+ `│ ID: ${String(user.id || 'n/a').substring(0, 39).padEnd(39)}│`,
283
+ '└──────────────────────────────────────────────────┘',
284
+ ];
285
+ return { type: 'message', messageType: 'info', content: lines.join('\n') };
286
+ }
287
+ catch (err) {
288
+ return { type: 'message', messageType: 'error', content: `Error fetching user info: ${errorMessage(err)}` };
289
+ }
290
+ },
291
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * /workspace slash command - Manage A2A workspace connections
3
+ *
4
+ * Subcommands:
5
+ * /workspace list - List available workspaces
6
+ * /workspace join <id> - Connect to a workspace via AgentBridgeService
7
+ * /workspace leave - Disconnect from current workspace
8
+ * /workspace status - Show connection status and online agents
9
+ */
10
+ import type { SlashCommand } from './types.js';
11
+ export declare const workspaceCommand: SlashCommand;
@@ -0,0 +1,329 @@
1
+ /**
2
+ * /workspace slash command - Manage A2A workspace connections
3
+ *
4
+ * Subcommands:
5
+ * /workspace list - List available workspaces
6
+ * /workspace join <id> - Connect to a workspace via AgentBridgeService
7
+ * /workspace leave - Disconnect from current workspace
8
+ * /workspace status - Show connection status and online agents
9
+ */
10
+ import { CommandKind } from './types.js';
11
+ import { RecoderAuthService } from '../../services/RecoderAuthService.js';
12
+ import { AgentBridgeService } from '../../services/AgentBridgeService.js';
13
+ const DOCKER_BACKEND_URL = process.env['DOCKER_BACKEND_URL'] || 'http://localhost:3100';
14
+ // Singleton instances
15
+ const authService = new RecoderAuthService();
16
+ let bridgeInstance = null;
17
+ function getBridge() {
18
+ if (!bridgeInstance) {
19
+ bridgeInstance = new AgentBridgeService(() => authService.getAccessToken());
20
+ }
21
+ return bridgeInstance;
22
+ }
23
+ export const workspaceCommand = {
24
+ name: 'workspace',
25
+ description: 'Manage A2A workspace connections',
26
+ kind: CommandKind.BUILT_IN,
27
+ subCommands: [
28
+ // -----------------------------------------------------------------------
29
+ // /workspace list
30
+ // -----------------------------------------------------------------------
31
+ {
32
+ name: 'list',
33
+ description: 'List available workspaces',
34
+ kind: CommandKind.BUILT_IN,
35
+ action: async () => {
36
+ const token = await authService.getAccessToken();
37
+ if (!token) {
38
+ return {
39
+ type: 'message',
40
+ messageType: 'error',
41
+ content: 'Not authenticated. Please run /recoder-login first.',
42
+ };
43
+ }
44
+ try {
45
+ const response = await fetch(`${DOCKER_BACKEND_URL}/api/a2a/workspaces`, {
46
+ method: 'GET',
47
+ headers: {
48
+ 'Authorization': `Bearer ${token}`,
49
+ 'Content-Type': 'application/json',
50
+ },
51
+ });
52
+ if (!response.ok) {
53
+ const errText = await response.text().catch(() => 'Unknown error');
54
+ return {
55
+ type: 'message',
56
+ messageType: 'error',
57
+ content: `Failed to fetch workspaces (${response.status}): ${errText}`,
58
+ };
59
+ }
60
+ const data = await response.json();
61
+ const workspaces = data.workspaces || data.data || data || [];
62
+ if (!Array.isArray(workspaces) || workspaces.length === 0) {
63
+ return {
64
+ type: 'message',
65
+ messageType: 'info',
66
+ content: [
67
+ '┌────────────────────────────────────────────────┐',
68
+ '│ A2A Workspaces │',
69
+ '├────────────────────────────────────────────────┤',
70
+ '│ No workspaces found. │',
71
+ '│ │',
72
+ '│ Create one from the recoder.xyz dashboard │',
73
+ '│ or via the backend API. │',
74
+ '└────────────────────────────────────────────────┘',
75
+ ].join('\n'),
76
+ };
77
+ }
78
+ const lines = [
79
+ '┌────────────────────────────────────────────────┐',
80
+ '│ A2A Workspaces │',
81
+ '├────────────────────────────────────────────────┤',
82
+ ];
83
+ for (const ws of workspaces) {
84
+ const agents = ws.agentCount !== undefined ? `${ws.agentCount} agents` : '';
85
+ const channels = ws.channelCount !== undefined ? `${ws.channelCount} channels` : '';
86
+ const meta = [agents, channels].filter(Boolean).join(', ');
87
+ lines.push(`│ ${ws.name.padEnd(45)}│`);
88
+ lines.push(`│ ID: ${ws.id.padEnd(39)}│`);
89
+ if (ws.description) {
90
+ lines.push(`│ ${ws.description.substring(0, 43).padEnd(45)}│`);
91
+ }
92
+ if (meta) {
93
+ lines.push(`│ ${meta.padEnd(45)}│`);
94
+ }
95
+ lines.push('│ │');
96
+ }
97
+ lines.push('├────────────────────────────────────────────────┤');
98
+ lines.push(`│ Total: ${String(workspaces.length).padEnd(38)}│`);
99
+ lines.push('│ Join with: /workspace join <id> │');
100
+ lines.push('└────────────────────────────────────────────────┘');
101
+ return { type: 'message', messageType: 'info', content: lines.join('\n') };
102
+ }
103
+ catch (error) {
104
+ const msg = error instanceof Error ? error.message : 'Unknown error';
105
+ return {
106
+ type: 'message',
107
+ messageType: 'error',
108
+ content: `Failed to list workspaces: ${msg}`,
109
+ };
110
+ }
111
+ },
112
+ },
113
+ // -----------------------------------------------------------------------
114
+ // /workspace join <id>
115
+ // -----------------------------------------------------------------------
116
+ {
117
+ name: 'join',
118
+ description: 'Connect to a workspace (e.g., /workspace join <workspace-id>)',
119
+ kind: CommandKind.BUILT_IN,
120
+ action: async (_context, args) => {
121
+ if (!args?.trim()) {
122
+ return {
123
+ type: 'message',
124
+ messageType: 'info',
125
+ content: [
126
+ 'Join Workspace',
127
+ '',
128
+ 'Usage: /workspace join <workspace-id>',
129
+ '',
130
+ 'Run /workspace list to see available workspace IDs.',
131
+ ].join('\n'),
132
+ };
133
+ }
134
+ const workspaceId = args.trim();
135
+ const bridge = getBridge();
136
+ if (bridge.isConnected() && bridge.getWorkspaceId() === workspaceId) {
137
+ return {
138
+ type: 'message',
139
+ messageType: 'info',
140
+ content: `Already connected to workspace: ${workspaceId}`,
141
+ };
142
+ }
143
+ // Disconnect from any existing workspace first
144
+ if (bridge.isConnected()) {
145
+ bridge.disconnect();
146
+ }
147
+ try {
148
+ await bridge.connect(workspaceId, ['code', 'review', 'test']);
149
+ if (bridge.isConnected()) {
150
+ return {
151
+ type: 'message',
152
+ messageType: 'info',
153
+ content: [
154
+ '┌────────────────────────────────────────────────┐',
155
+ '│ Workspace Connected │',
156
+ '├────────────────────────────────────────────────┤',
157
+ `│ Workspace: ${workspaceId.substring(0, 35).padEnd(35)}│`,
158
+ `│ Agent ID: ${bridge.getAgentId().substring(0, 35).padEnd(35)}│`,
159
+ '│ Status: Online │',
160
+ '├────────────────────────────────────────────────┤',
161
+ '│ You will receive task assignments in │',
162
+ '│ real-time from the workspace coordinator. │',
163
+ '│ │',
164
+ '│ /workspace status - Check connection │',
165
+ '│ /workspace leave - Disconnect │',
166
+ '└────────────────────────────────────────────────┘',
167
+ ].join('\n'),
168
+ };
169
+ }
170
+ else {
171
+ return {
172
+ type: 'message',
173
+ messageType: 'error',
174
+ content: [
175
+ 'Failed to connect to workspace.',
176
+ '',
177
+ 'Possible reasons:',
178
+ ' - docker-backend is not running',
179
+ ' - Workspace ID does not exist',
180
+ ' - Authentication token is invalid',
181
+ '',
182
+ 'Check your connection and try again.',
183
+ ].join('\n'),
184
+ };
185
+ }
186
+ }
187
+ catch (error) {
188
+ const msg = error instanceof Error ? error.message : 'Unknown error';
189
+ return {
190
+ type: 'message',
191
+ messageType: 'error',
192
+ content: `Failed to join workspace: ${msg}`,
193
+ };
194
+ }
195
+ },
196
+ },
197
+ // -----------------------------------------------------------------------
198
+ // /workspace leave
199
+ // -----------------------------------------------------------------------
200
+ {
201
+ name: 'leave',
202
+ description: 'Disconnect from current workspace',
203
+ kind: CommandKind.BUILT_IN,
204
+ action: async () => {
205
+ const bridge = getBridge();
206
+ if (!bridge.isConnected()) {
207
+ return {
208
+ type: 'message',
209
+ messageType: 'info',
210
+ content: 'Not connected to any workspace.',
211
+ };
212
+ }
213
+ const workspaceId = bridge.getWorkspaceId() || 'unknown';
214
+ bridge.disconnect();
215
+ return {
216
+ type: 'message',
217
+ messageType: 'info',
218
+ content: [
219
+ '┌────────────────────────────────────────────────┐',
220
+ '│ Workspace Disconnected │',
221
+ '├────────────────────────────────────────────────┤',
222
+ `│ Left: ${workspaceId.substring(0, 40).padEnd(40)}│`,
223
+ '│ Status: Offline │',
224
+ '│ │',
225
+ '│ Reconnect with: /workspace join <id> │',
226
+ '└────────────────────────────────────────────────┘',
227
+ ].join('\n'),
228
+ };
229
+ },
230
+ },
231
+ // -----------------------------------------------------------------------
232
+ // /workspace status
233
+ // -----------------------------------------------------------------------
234
+ {
235
+ name: 'status',
236
+ description: 'Show current connection status and online agents',
237
+ kind: CommandKind.BUILT_IN,
238
+ action: async () => {
239
+ const bridge = getBridge();
240
+ if (!bridge.isConnected()) {
241
+ return {
242
+ type: 'message',
243
+ messageType: 'info',
244
+ content: [
245
+ '┌────────────────────────────────────────────────┐',
246
+ '│ Workspace Status │',
247
+ '├────────────────────────────────────────────────┤',
248
+ '│ Status: Disconnected │',
249
+ `│ Agent ID: ${bridge.getAgentId().substring(0, 35).padEnd(35)}│`,
250
+ '│ │',
251
+ '│ Connect with: /workspace join <id> │',
252
+ '└────────────────────────────────────────────────┘',
253
+ ].join('\n'),
254
+ };
255
+ }
256
+ const workspaceId = bridge.getWorkspaceId() || 'none';
257
+ const lines = [
258
+ '┌────────────────────────────────────────────────┐',
259
+ '│ Workspace Status │',
260
+ '├────────────────────────────────────────────────┤',
261
+ '│ Status: Connected │',
262
+ `│ Workspace: ${workspaceId.substring(0, 35).padEnd(35)}│`,
263
+ `│ Agent ID: ${bridge.getAgentId().substring(0, 35).padEnd(35)}│`,
264
+ ];
265
+ // Try to fetch online agents from the backend
266
+ const token = await authService.getAccessToken();
267
+ if (token) {
268
+ try {
269
+ const response = await fetch(`${DOCKER_BACKEND_URL}/api/a2a/workspaces/${encodeURIComponent(workspaceId)}/agents`, {
270
+ method: 'GET',
271
+ headers: {
272
+ 'Authorization': `Bearer ${token}`,
273
+ 'Content-Type': 'application/json',
274
+ },
275
+ });
276
+ if (response.ok) {
277
+ const data = await response.json();
278
+ const agents = data.agents || data.data || [];
279
+ if (agents.length > 0) {
280
+ lines.push('├────────────────────────────────────────────────┤');
281
+ lines.push(`│ Online Agents (${String(agents.length).padEnd(30)}│`);
282
+ lines.push('│ │');
283
+ for (const agent of agents) {
284
+ const local = agent.isLocal ? ' (local)' : '';
285
+ const caps = agent.capabilities.length > 0
286
+ ? ` [${agent.capabilities.join(', ')}]`
287
+ : '';
288
+ const label = `${agent.agentId}${local}${caps}`;
289
+ lines.push(`│ ${label.substring(0, 44).padEnd(44)}│`);
290
+ }
291
+ }
292
+ }
293
+ }
294
+ catch {
295
+ // Silently skip agent listing on error
296
+ }
297
+ }
298
+ lines.push('├────────────────────────────────────────────────┤');
299
+ lines.push('│ /workspace leave - Disconnect │');
300
+ lines.push('└────────────────────────────────────────────────┘');
301
+ return { type: 'message', messageType: 'info', content: lines.join('\n') };
302
+ },
303
+ },
304
+ ],
305
+ // -------------------------------------------------------------------------
306
+ // Default action (no subcommand)
307
+ // -------------------------------------------------------------------------
308
+ action: async () => {
309
+ const bridge = getBridge();
310
+ const connected = bridge.isConnected();
311
+ const wsId = bridge.getWorkspaceId();
312
+ const statusLine = connected
313
+ ? `Connected to: ${wsId}`
314
+ : 'Disconnected';
315
+ return {
316
+ type: 'message',
317
+ messageType: 'info',
318
+ content: [
319
+ `A2A Workspace (${statusLine})`,
320
+ '',
321
+ 'Commands:',
322
+ ' /workspace list - List available workspaces',
323
+ ' /workspace join <id> - Connect to a workspace',
324
+ ' /workspace leave - Disconnect from workspace',
325
+ ' /workspace status - Show connection status',
326
+ ].join('\n'),
327
+ };
328
+ },
329
+ };