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,253 @@
1
+ /**
2
+ * Agent Bridge Service
3
+ *
4
+ * Connects recoder-code CLI to the A2A workspace system on docker-backend
5
+ * via WebSocket. Enables local agents to:
6
+ * - Register as workspace members
7
+ * - Receive task assignments in real-time
8
+ * - Report task progress and results
9
+ * - Participate in channel conversations
10
+ *
11
+ * Uses existing RecoderAuthService for authentication tokens.
12
+ * Auto-reconnects with exponential backoff (same pattern as PlatformSyncService).
13
+ */
14
+ import WebSocket from 'ws';
15
+ import { EventEmitter } from 'events';
16
+ import os from 'node:os';
17
+ const DOCKER_BACKEND_URL = process.env['DOCKER_BACKEND_URL'] || 'https://docker.recoder.xyz';
18
+ // ---------------------------------------------------------------------------
19
+ // Service
20
+ // ---------------------------------------------------------------------------
21
+ export class AgentBridgeService extends EventEmitter {
22
+ ws = null;
23
+ reconnectTimeout = null;
24
+ heartbeatInterval = null;
25
+ connected = false;
26
+ agentId;
27
+ workspaceId = null;
28
+ reconnectAttempts = 0;
29
+ maxReconnectDelay = 30_000;
30
+ getToken;
31
+ constructor(getToken, agentId) {
32
+ super();
33
+ this.getToken = getToken;
34
+ this.agentId = agentId || `recoder-code_${os.hostname()}_${process.pid}`;
35
+ }
36
+ // -----------------------------------------------------------------------
37
+ // Connection lifecycle
38
+ // -----------------------------------------------------------------------
39
+ async connect(workspaceId, capabilities = []) {
40
+ this.workspaceId = workspaceId;
41
+ const token = await this.getToken();
42
+ if (!token) {
43
+ console.error('Not authenticated. Please run `recoder auth login` first.');
44
+ return;
45
+ }
46
+ return this.connectWebSocket(token, capabilities);
47
+ }
48
+ connectWebSocket(token, capabilities = []) {
49
+ return new Promise((resolve) => {
50
+ const wsUrl = DOCKER_BACKEND_URL.replace(/^http/, 'ws');
51
+ const url = new URL(`${wsUrl}/agent-bridge`);
52
+ url.searchParams.set('token', token);
53
+ url.searchParams.set('agentId', this.agentId);
54
+ if (capabilities.length > 0) {
55
+ url.searchParams.set('capabilities', capabilities.join(','));
56
+ }
57
+ if (this.workspaceId) {
58
+ url.searchParams.set('workspaceIds', this.workspaceId);
59
+ }
60
+ try {
61
+ this.ws = new WebSocket(url.toString());
62
+ this.ws.on('open', () => {
63
+ this.connected = true;
64
+ this.reconnectAttempts = 0;
65
+ this.startHeartbeat();
66
+ this.emit('connected');
67
+ resolve();
68
+ });
69
+ this.ws.on('message', (data) => {
70
+ try {
71
+ const msg = JSON.parse(data.toString());
72
+ this.handleMessage(msg);
73
+ }
74
+ catch {
75
+ // Ignore parse errors
76
+ }
77
+ });
78
+ this.ws.on('close', (_code, _reason) => {
79
+ this.connected = false;
80
+ this.stopHeartbeat();
81
+ this.emit('disconnected');
82
+ this.scheduleReconnect(token, capabilities);
83
+ });
84
+ this.ws.on('error', (err) => {
85
+ if (!this.connected) {
86
+ resolve(); // Don't hang on initial connect failure
87
+ }
88
+ this.emit('error', err);
89
+ });
90
+ // Timeout initial connection attempt
91
+ setTimeout(() => {
92
+ if (!this.connected) {
93
+ resolve();
94
+ }
95
+ }, 5000);
96
+ }
97
+ catch (error) {
98
+ console.error('Failed to connect to agent bridge:', error);
99
+ resolve();
100
+ }
101
+ });
102
+ }
103
+ disconnect() {
104
+ if (this.reconnectTimeout) {
105
+ clearTimeout(this.reconnectTimeout);
106
+ this.reconnectTimeout = null;
107
+ }
108
+ this.stopHeartbeat();
109
+ if (this.ws) {
110
+ this.ws.close(1000, 'Client disconnect');
111
+ this.ws = null;
112
+ }
113
+ this.connected = false;
114
+ this.workspaceId = null;
115
+ this.reconnectAttempts = 0;
116
+ }
117
+ scheduleReconnect(token, capabilities) {
118
+ if (this.reconnectTimeout || !this.workspaceId)
119
+ return;
120
+ // Exponential backoff: 1s, 2s, 4s, 8s, ... up to maxReconnectDelay
121
+ const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), this.maxReconnectDelay);
122
+ this.reconnectAttempts++;
123
+ this.reconnectTimeout = setTimeout(() => {
124
+ this.reconnectTimeout = null;
125
+ void this.connectWebSocket(token, capabilities);
126
+ }, delay);
127
+ }
128
+ // -----------------------------------------------------------------------
129
+ // Heartbeat
130
+ // -----------------------------------------------------------------------
131
+ startHeartbeat() {
132
+ this.stopHeartbeat();
133
+ this.heartbeatInterval = setInterval(() => {
134
+ this.send({ type: 'heartbeat' });
135
+ }, 30_000);
136
+ }
137
+ stopHeartbeat() {
138
+ if (this.heartbeatInterval) {
139
+ clearInterval(this.heartbeatInterval);
140
+ this.heartbeatInterval = null;
141
+ }
142
+ }
143
+ // -----------------------------------------------------------------------
144
+ // Outgoing messages
145
+ // -----------------------------------------------------------------------
146
+ send(msg) {
147
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
148
+ return false;
149
+ try {
150
+ this.ws.send(JSON.stringify(msg));
151
+ return true;
152
+ }
153
+ catch {
154
+ return false;
155
+ }
156
+ }
157
+ joinWorkspace(workspaceId) {
158
+ this.workspaceId = workspaceId;
159
+ this.send({ type: 'join_workspace', workspaceId });
160
+ }
161
+ leaveWorkspace(workspaceId) {
162
+ this.send({ type: 'leave_workspace', workspaceId });
163
+ if (this.workspaceId === workspaceId) {
164
+ this.workspaceId = null;
165
+ }
166
+ }
167
+ sendProgress(taskId, progress, output) {
168
+ this.send({ type: 'task.progress', taskId, progress, output });
169
+ }
170
+ completeTask(taskId, result) {
171
+ this.send({ type: 'task.complete', taskId, result });
172
+ }
173
+ failTask(taskId, error) {
174
+ this.send({ type: 'task.failed', taskId, error });
175
+ }
176
+ sendChannelMessage(channelId, content) {
177
+ this.send({ type: 'channel.message', channelId, content });
178
+ }
179
+ // -----------------------------------------------------------------------
180
+ // Incoming message handling
181
+ // -----------------------------------------------------------------------
182
+ handleMessage(msg) {
183
+ switch (msg.type) {
184
+ case 'registered':
185
+ this.emit('registered', {
186
+ agentId: msg.agentId,
187
+ workspaces: msg.workspaces,
188
+ });
189
+ break;
190
+ case 'task.assigned': {
191
+ const assignment = {
192
+ taskId: msg.taskId,
193
+ prompt: msg.prompt,
194
+ context: msg.context || {},
195
+ worktreeBranch: msg.worktreeBranch,
196
+ };
197
+ this.emit('task.assigned', assignment);
198
+ break;
199
+ }
200
+ case 'task.cancelled':
201
+ this.emit('task.cancelled', { taskId: msg.taskId });
202
+ break;
203
+ case 'channel.message': {
204
+ const channelMsg = {
205
+ channelId: msg.channelId,
206
+ from: msg.from,
207
+ content: msg.content,
208
+ };
209
+ this.emit('channel.message', channelMsg);
210
+ break;
211
+ }
212
+ case 'workspace.updated':
213
+ this.emit('workspace.updated', {
214
+ workspaceId: msg.workspaceId,
215
+ changes: msg.changes,
216
+ });
217
+ break;
218
+ case 'workspace.joined':
219
+ this.emit('workspace.joined', { workspaceId: msg.workspaceId });
220
+ break;
221
+ case 'workspace.left':
222
+ this.emit('workspace.left', { workspaceId: msg.workspaceId });
223
+ break;
224
+ case 'ping':
225
+ // Server keepalive, no action needed
226
+ break;
227
+ default:
228
+ // Emit unknown messages for extensibility
229
+ this.emit('message', msg);
230
+ }
231
+ }
232
+ // -----------------------------------------------------------------------
233
+ // Status
234
+ // -----------------------------------------------------------------------
235
+ isConnected() {
236
+ return this.connected;
237
+ }
238
+ getAgentId() {
239
+ return this.agentId;
240
+ }
241
+ getWorkspaceId() {
242
+ return this.workspaceId;
243
+ }
244
+ displayStatus() {
245
+ if (!this.connected) {
246
+ console.log('Agent Bridge: Disconnected');
247
+ return;
248
+ }
249
+ console.log(`Agent Bridge: Connected`);
250
+ console.log(` Agent ID: ${this.agentId}`);
251
+ console.log(` Workspace: ${this.workspaceId || 'None'}`);
252
+ }
253
+ }
@@ -56,6 +56,8 @@ import { providersCommand } from '../ui/commands/providersCommand.js';
56
56
  import { connectCommand } from '../ui/commands/connectCommand.js';
57
57
  import { modelsCommand } from '../ui/commands/modelsCommand.js';
58
58
  import { agentCommand } from '../ui/commands/agentCommand.js';
59
+ import { workspaceCommand } from '../ui/commands/workspaceCommand.js';
60
+ import { workflowCommand as workflowCmd, analyticsCommand, costCommand, whoamiCommand, } from '../ui/commands/workflowCommands.js';
59
61
  /**
60
62
  * Loads the core, hard-coded slash commands that are an integral part
61
63
  * of the Gemini CLI application.
@@ -132,6 +134,11 @@ export class BuiltinCommandLoader {
132
134
  connectCommand,
133
135
  modelsCommand,
134
136
  agentCommand,
137
+ workspaceCommand,
138
+ workflowCmd,
139
+ analyticsCommand,
140
+ costCommand,
141
+ whoamiCommand,
135
142
  ];
136
143
  return allDefinitions.filter((cmd) => cmd !== null);
137
144
  }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Platform Sync Service
3
+ * Handles bidirectional sync between recoder-code CLI and recoder.xyz web platform
4
+ * Detects if running inside recoder.xyz container and auto-connects
5
+ */
6
+ import { EventEmitter } from 'events';
7
+ export interface FileChange {
8
+ type: 'create' | 'modify' | 'delete';
9
+ path: string;
10
+ content?: string;
11
+ timestamp: number;
12
+ }
13
+ export interface PlatformInfo {
14
+ isContainer: boolean;
15
+ isLinked: boolean;
16
+ projectId?: string;
17
+ previewUrl?: string;
18
+ backendUrl?: string;
19
+ webUrl?: string;
20
+ workingDir?: string;
21
+ }
22
+ export declare class PlatformSyncService extends EventEmitter {
23
+ private ws;
24
+ private reconnectTimeout;
25
+ private platformInfo;
26
+ private connected;
27
+ private fileWatcher;
28
+ private pendingChanges;
29
+ private syncEnabled;
30
+ /**
31
+ * Detect platform environment (container or local linked project)
32
+ */
33
+ detectPlatform(workingDir?: string): Promise<PlatformInfo>;
34
+ /**
35
+ * Detect linked project from .recoder-web file
36
+ */
37
+ private detectLinkedProject;
38
+ /**
39
+ * Check if we're inside a recoder container
40
+ */
41
+ private isInRecoderContainer;
42
+ /**
43
+ * Extract project ID from container context
44
+ */
45
+ private detectProjectId;
46
+ /**
47
+ * Get preview URL - constructed from env vars
48
+ * Preview is proxied through docker-backend at /preview/{projectId}
49
+ */
50
+ private detectPreviewUrl;
51
+ /**
52
+ * Auto-initialize when running in container or linked project
53
+ * Call this on CLI startup
54
+ */
55
+ autoInitialize(): Promise<void>;
56
+ /**
57
+ * Connect to platform sync WebSocket
58
+ */
59
+ connect(): Promise<boolean>;
60
+ /**
61
+ * Connect to file sync WebSocket
62
+ */
63
+ private connectWebSocket;
64
+ /**
65
+ * Handle incoming sync messages
66
+ */
67
+ private handleMessage;
68
+ /**
69
+ * Schedule reconnection attempt
70
+ */
71
+ private scheduleReconnect;
72
+ /**
73
+ * Send file change to platform (with offline queuing)
74
+ */
75
+ notifyFileChange(change: FileChange): Promise<void>;
76
+ /**
77
+ * Flush pending changes when reconnected
78
+ */
79
+ private flushPendingChanges;
80
+ /**
81
+ * Get count of pending offline changes
82
+ */
83
+ getPendingChangesCount(): number;
84
+ /**
85
+ * Track file versions for conflict detection
86
+ */
87
+ private fileVersions;
88
+ /**
89
+ * Check if a remote change conflicts with local changes
90
+ */
91
+ private detectConflict;
92
+ /**
93
+ * Handle conflict resolution
94
+ * Returns: 'local' | 'remote' | 'merge' | 'skip'
95
+ */
96
+ resolveConflict(filePath: string, localContent: string, remoteContent: string, callback: (choice: 'local' | 'remote' | 'skip') => void): Promise<void>;
97
+ /**
98
+ * Start watching local files for changes
99
+ */
100
+ startFileWatcher(): void;
101
+ /**
102
+ * Stop file watcher
103
+ */
104
+ stopFileWatcher(): void;
105
+ /**
106
+ * Request file from platform
107
+ */
108
+ requestFile(filePath: string): Promise<void>;
109
+ /**
110
+ * Request file list from platform
111
+ */
112
+ requestFileList(directory?: string): Promise<void>;
113
+ /**
114
+ * Delete file via platform
115
+ */
116
+ deleteFile(filePath: string): Promise<void>;
117
+ /**
118
+ * Disconnect from platform
119
+ */
120
+ disconnect(): void;
121
+ /**
122
+ * Get connection status
123
+ */
124
+ isConnected(): boolean;
125
+ /**
126
+ * Get platform info
127
+ */
128
+ getPlatformInfo(): PlatformInfo | null;
129
+ /**
130
+ * Enable/disable sync
131
+ */
132
+ setSyncEnabled(enabled: boolean): void;
133
+ /**
134
+ * Display platform status
135
+ */
136
+ displayStatus(): void;
137
+ /**
138
+ * Save chat message to platform (for CLI-web chat sync)
139
+ */
140
+ saveChatMessage(message: {
141
+ role: 'user' | 'assistant';
142
+ content: string;
143
+ id?: string;
144
+ }): Promise<void>;
145
+ /**
146
+ * Load chat history from platform
147
+ */
148
+ loadChatHistory(): Promise<any[]>;
149
+ /**
150
+ * Get the web URL for the current project
151
+ */
152
+ getWebProjectUrl(): string | undefined;
153
+ }
154
+ export declare const platformSync: PlatformSyncService;