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.
- package/dist/index.js +0 -0
- package/dist/src/commands/context/index.js +2 -2
- package/dist/src/commands/mcp/marketplace.d.ts +6 -0
- package/dist/src/commands/mcp/marketplace.js +448 -0
- package/dist/src/commands/mcp.js +2 -0
- package/dist/src/commands/parallel.d.ts +20 -0
- package/dist/src/commands/parallel.js +133 -0
- package/dist/src/commands/recoderWeb.js +184 -5
- package/dist/src/commands/web/diff.d.ts +13 -0
- package/dist/src/commands/web/diff.js +235 -0
- package/dist/src/commands/web/link.d.ts +11 -0
- package/dist/src/commands/web/link.js +96 -0
- package/dist/src/commands/web/pull.d.ts +13 -0
- package/dist/src/commands/web/pull.js +203 -0
- package/dist/src/commands/web/status.d.ts +10 -0
- package/dist/src/commands/web/status.js +104 -0
- package/dist/src/commands/web/unlink.d.ts +10 -0
- package/dist/src/commands/web/unlink.js +45 -0
- package/dist/src/commands/web/watch.d.ts +14 -0
- package/dist/src/commands/web/watch.js +360 -0
- package/dist/src/commands/web.js +12 -0
- package/dist/src/config/config.js +6 -2
- package/dist/src/config/defaultMcpServers.d.ts +1 -0
- package/dist/src/config/defaultMcpServers.js +46 -0
- package/dist/src/gemini.js +10 -0
- package/dist/src/parallel/git-utils.d.ts +42 -0
- package/dist/src/parallel/git-utils.js +161 -0
- package/dist/src/parallel/index.d.ts +14 -0
- package/dist/src/parallel/index.js +14 -0
- package/dist/src/parallel/parallel-mode.d.ts +48 -0
- package/dist/src/parallel/parallel-mode.js +224 -0
- package/dist/src/services/AgentBridgeService.d.ts +61 -0
- package/dist/src/services/AgentBridgeService.js +253 -0
- package/dist/src/services/BuiltinCommandLoader.js +7 -0
- package/dist/src/services/PlatformSyncService.d.ts +154 -0
- package/dist/src/services/PlatformSyncService.js +588 -0
- package/dist/src/ui/commands/workflowCommands.d.ts +16 -0
- package/dist/src/ui/commands/workflowCommands.js +291 -0
- package/dist/src/ui/commands/workspaceCommand.d.ts +11 -0
- package/dist/src/ui/commands/workspaceCommand.js +329 -0
- package/dist/src/zed-integration/schema.d.ts +30 -30
- package/package.json +29 -10
- package/src/postinstall.cjs +3 -2
- 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;
|