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,588 @@
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 WebSocket from 'ws';
7
+ import { EventEmitter } from 'events';
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ const DOCKER_BACKEND_URL = process.env['DOCKER_BACKEND_URL'] || 'https://docker.recoder.xyz';
11
+ const RECODER_WEB_URL = process.env['RECODER_WEB_URL'] || 'https://web.recoder.xyz';
12
+ export class PlatformSyncService extends EventEmitter {
13
+ ws = null;
14
+ reconnectTimeout = null;
15
+ platformInfo = null;
16
+ connected = false;
17
+ fileWatcher = null;
18
+ pendingChanges = [];
19
+ syncEnabled = true;
20
+ /**
21
+ * Detect platform environment (container or local linked project)
22
+ */
23
+ async detectPlatform(workingDir) {
24
+ const cwd = workingDir || process.cwd();
25
+ // Check for container markers first
26
+ const isContainer = this.isInRecoderContainer();
27
+ if (isContainer) {
28
+ // Running in container
29
+ const projectId = this.detectProjectId();
30
+ const previewUrl = await this.detectPreviewUrl();
31
+ this.platformInfo = {
32
+ isContainer: true,
33
+ isLinked: true,
34
+ projectId,
35
+ previewUrl,
36
+ backendUrl: DOCKER_BACKEND_URL,
37
+ webUrl: RECODER_WEB_URL,
38
+ workingDir: '/workspace',
39
+ };
40
+ return this.platformInfo;
41
+ }
42
+ // Check for local linked project (.recoder-web file)
43
+ const linkedProject = this.detectLinkedProject(cwd);
44
+ if (linkedProject) {
45
+ this.platformInfo = {
46
+ isContainer: false,
47
+ isLinked: true,
48
+ projectId: linkedProject.projectId,
49
+ previewUrl: linkedProject.previewUrl,
50
+ backendUrl: DOCKER_BACKEND_URL,
51
+ webUrl: linkedProject.webUrl || RECODER_WEB_URL,
52
+ workingDir: cwd,
53
+ };
54
+ return this.platformInfo;
55
+ }
56
+ // Not in container and not linked
57
+ this.platformInfo = { isContainer: false, isLinked: false };
58
+ return this.platformInfo;
59
+ }
60
+ /**
61
+ * Detect linked project from .recoder-web file
62
+ */
63
+ detectLinkedProject(dir) {
64
+ try {
65
+ const metaFile = path.join(dir, '.recoder-web');
66
+ if (fs.existsSync(metaFile)) {
67
+ const meta = JSON.parse(fs.readFileSync(metaFile, 'utf-8'));
68
+ const projectId = meta.urlId || meta.projectId;
69
+ if (projectId) {
70
+ return {
71
+ projectId,
72
+ previewUrl: meta.previewUrl || `${DOCKER_BACKEND_URL}/preview/${projectId}`,
73
+ webUrl: meta.webUrl || `${RECODER_WEB_URL}/chat/${projectId}`,
74
+ };
75
+ }
76
+ }
77
+ }
78
+ catch {
79
+ // Ignore
80
+ }
81
+ return null;
82
+ }
83
+ /**
84
+ * Check if we're inside a recoder container
85
+ */
86
+ isInRecoderContainer() {
87
+ // Check for container-specific markers
88
+ const markers = [
89
+ // Check if /workspace exists (our standard working directory)
90
+ fs.existsSync('/workspace'),
91
+ // Check for recoder environment variable
92
+ process.env['RECODER_CONTAINER'] === 'true',
93
+ // Check for PTY_PORT (our container's PTY server)
94
+ !!process.env['PTY_PORT'],
95
+ // Check if running in Docker
96
+ fs.existsSync('/.dockerenv'),
97
+ ];
98
+ return markers.some(Boolean);
99
+ }
100
+ /**
101
+ * Extract project ID from container context
102
+ */
103
+ detectProjectId() {
104
+ // Try environment variable first
105
+ if (process.env['RECODER_PROJECT_ID']) {
106
+ return process.env['RECODER_PROJECT_ID'];
107
+ }
108
+ // Try to read from .recoder-web file
109
+ try {
110
+ const metaFile = path.join('/workspace', '.recoder-web');
111
+ if (fs.existsSync(metaFile)) {
112
+ const meta = JSON.parse(fs.readFileSync(metaFile, 'utf-8'));
113
+ return meta.projectId || meta.urlId;
114
+ }
115
+ }
116
+ catch {
117
+ // Ignore
118
+ }
119
+ // Try to extract from volume name (container labels)
120
+ // This is set by docker-backend when creating containers
121
+ try {
122
+ const hostname = fs.readFileSync('/etc/hostname', 'utf-8').trim();
123
+ if (hostname.startsWith('recoder-')) {
124
+ return hostname.replace('recoder-', '');
125
+ }
126
+ }
127
+ catch {
128
+ // Ignore
129
+ }
130
+ return undefined;
131
+ }
132
+ /**
133
+ * Get preview URL - constructed from env vars
134
+ * Preview is proxied through docker-backend at /preview/{projectId}
135
+ */
136
+ async detectPreviewUrl() {
137
+ const projectId = process.env['RECODER_PROJECT_ID'];
138
+ if (!projectId)
139
+ return undefined;
140
+ // Preview is accessible via docker-backend proxy
141
+ return `${DOCKER_BACKEND_URL}/preview/${projectId}`;
142
+ }
143
+ /**
144
+ * Auto-initialize when running in container or linked project
145
+ * Call this on CLI startup
146
+ */
147
+ async autoInitialize() {
148
+ const platform = await this.detectPlatform();
149
+ // Not connected to platform at all
150
+ if (!platform.isContainer && !platform.isLinked) {
151
+ return;
152
+ }
153
+ // Show appropriate header
154
+ if (platform.isContainer) {
155
+ console.log('\n┌────────────────────────────────────────────────┐');
156
+ console.log('│ 🐳 recoder.xyz Container Environment │');
157
+ }
158
+ else {
159
+ console.log('\n┌────────────────────────────────────────────────┐');
160
+ console.log('│ 🔗 recoder.xyz Linked Project │');
161
+ }
162
+ console.log('├────────────────────────────────────────────────┤');
163
+ console.log(`│ 📁 Project: ${(platform.projectId || 'Unknown').padEnd(33)}│`);
164
+ if (platform.previewUrl) {
165
+ console.log(`│ 🌐 Preview: ${platform.previewUrl.substring(0, 33).padEnd(33)}│`);
166
+ }
167
+ const webUrl = this.getWebProjectUrl();
168
+ if (webUrl) {
169
+ console.log(`│ 💻 Web IDE: ${webUrl.substring(0, 33).padEnd(33)}│`);
170
+ }
171
+ // Auto-connect to sync (only for containers, local projects sync manually)
172
+ if (platform.isContainer) {
173
+ const connected = await this.connect();
174
+ if (connected) {
175
+ console.log('│ 📡 Sync: Connected │');
176
+ this.startFileWatcher();
177
+ }
178
+ else {
179
+ console.log('│ 📡 Sync: Offline (changes queued) │');
180
+ }
181
+ }
182
+ else {
183
+ console.log('│ 📡 Sync: Manual (use `recoder web sync`) │');
184
+ }
185
+ console.log('└────────────────────────────────────────────────┘');
186
+ console.log('');
187
+ }
188
+ /**
189
+ * Connect to platform sync WebSocket
190
+ */
191
+ async connect() {
192
+ const platform = await this.detectPlatform();
193
+ if (!platform.isContainer) {
194
+ console.log('📍 Not running in recoder.xyz container');
195
+ return false;
196
+ }
197
+ if (!platform.projectId) {
198
+ console.log('⚠️ Container detected but no project ID found');
199
+ return false;
200
+ }
201
+ return this.connectWebSocket(platform.projectId);
202
+ }
203
+ /**
204
+ * Connect to file sync WebSocket
205
+ */
206
+ connectWebSocket(projectId) {
207
+ return new Promise((resolve) => {
208
+ const wsUrl = DOCKER_BACKEND_URL.replace('http', 'ws');
209
+ try {
210
+ this.ws = new WebSocket(`${wsUrl}/sync/${projectId}`);
211
+ this.ws.on('open', () => {
212
+ console.log('🔗 Connected to recoder.xyz platform sync');
213
+ this.connected = true;
214
+ this.emit('connected');
215
+ // Flush any queued offline changes
216
+ this.flushPendingChanges();
217
+ resolve(true);
218
+ });
219
+ this.ws.on('message', (data) => {
220
+ try {
221
+ const msg = JSON.parse(data.toString());
222
+ this.handleMessage(msg);
223
+ }
224
+ catch (e) {
225
+ // Ignore parse errors
226
+ }
227
+ });
228
+ this.ws.on('close', () => {
229
+ console.log('🔌 Platform sync disconnected');
230
+ this.connected = false;
231
+ this.emit('disconnected');
232
+ this.scheduleReconnect(projectId);
233
+ });
234
+ this.ws.on('error', (err) => {
235
+ console.error('❌ Platform sync error:', err.message);
236
+ this.connected = false;
237
+ resolve(false);
238
+ });
239
+ // Timeout after 5 seconds
240
+ setTimeout(() => {
241
+ if (!this.connected) {
242
+ resolve(false);
243
+ }
244
+ }, 5000);
245
+ }
246
+ catch (error) {
247
+ console.error('❌ Failed to connect:', error);
248
+ resolve(false);
249
+ }
250
+ });
251
+ }
252
+ /**
253
+ * Handle incoming sync messages
254
+ */
255
+ handleMessage(msg) {
256
+ switch (msg.action) {
257
+ case 'file-changed':
258
+ // Remote file changed in web editor - check for conflicts
259
+ const changes = msg.changes;
260
+ for (const change of changes) {
261
+ if (change.content && this.detectConflict(change.path, change.content)) {
262
+ // Emit conflict for CLI to handle
263
+ try {
264
+ const fullPath = path.join('/workspace', change.path);
265
+ const localContent = fs.readFileSync(fullPath, 'utf-8');
266
+ this.emit('conflict', {
267
+ path: change.path,
268
+ localContent,
269
+ remoteContent: change.content,
270
+ });
271
+ }
272
+ catch {
273
+ // If can't read local, just apply remote
274
+ this.emit('remoteChange', [change]);
275
+ }
276
+ }
277
+ else {
278
+ this.emit('remoteChange', [change]);
279
+ }
280
+ }
281
+ break;
282
+ case 'refresh-preview':
283
+ // Web editor requested preview refresh
284
+ this.emit('refreshPreview');
285
+ break;
286
+ case 'content':
287
+ // File content received - track version
288
+ this.fileVersions.set(msg.path, {
289
+ content: msg.content,
290
+ timestamp: Date.now(),
291
+ });
292
+ this.emit('fileContent', { path: msg.path, content: msg.content });
293
+ break;
294
+ case 'files':
295
+ // File list received
296
+ this.emit('fileList', msg.files);
297
+ break;
298
+ case 'error':
299
+ console.error('Sync error:', msg.error);
300
+ break;
301
+ }
302
+ }
303
+ /**
304
+ * Schedule reconnection attempt
305
+ */
306
+ scheduleReconnect(projectId) {
307
+ if (this.reconnectTimeout) {
308
+ clearTimeout(this.reconnectTimeout);
309
+ }
310
+ this.reconnectTimeout = setTimeout(() => {
311
+ console.log('🔄 Attempting to reconnect...');
312
+ this.connectWebSocket(projectId);
313
+ }, 3000);
314
+ }
315
+ /**
316
+ * Send file change to platform (with offline queuing)
317
+ */
318
+ async notifyFileChange(change) {
319
+ if (!this.syncEnabled)
320
+ return;
321
+ // Read file content for create/modify
322
+ if (change.type !== 'delete' && !change.content) {
323
+ try {
324
+ const fullPath = path.join('/workspace', change.path);
325
+ change.content = fs.readFileSync(fullPath, 'utf-8');
326
+ }
327
+ catch {
328
+ // File might not exist yet
329
+ }
330
+ }
331
+ // If offline, queue the change for later sync
332
+ if (!this.connected || !this.ws) {
333
+ this.pendingChanges.push(change);
334
+ return;
335
+ }
336
+ this.ws.send(JSON.stringify({
337
+ action: 'write',
338
+ path: change.path,
339
+ content: change.content || '',
340
+ }));
341
+ }
342
+ /**
343
+ * Flush pending changes when reconnected
344
+ */
345
+ async flushPendingChanges() {
346
+ if (!this.connected || !this.ws || this.pendingChanges.length === 0)
347
+ return;
348
+ console.log(`📤 Syncing ${this.pendingChanges.length} offline changes...`);
349
+ for (const change of this.pendingChanges) {
350
+ this.ws.send(JSON.stringify({
351
+ action: 'write',
352
+ path: change.path,
353
+ content: change.content || '',
354
+ }));
355
+ }
356
+ this.pendingChanges = [];
357
+ console.log('✅ Offline changes synced');
358
+ }
359
+ /**
360
+ * Get count of pending offline changes
361
+ */
362
+ getPendingChangesCount() {
363
+ return this.pendingChanges.length;
364
+ }
365
+ /**
366
+ * Track file versions for conflict detection
367
+ */
368
+ fileVersions = new Map();
369
+ /**
370
+ * Check if a remote change conflicts with local changes
371
+ */
372
+ detectConflict(remotePath, remoteContent) {
373
+ const local = this.fileVersions.get(remotePath);
374
+ if (!local)
375
+ return false;
376
+ // Check if local file was modified after last sync
377
+ try {
378
+ const fullPath = path.join('/workspace', remotePath);
379
+ const currentContent = fs.readFileSync(fullPath, 'utf-8');
380
+ // Conflict if local content differs from both synced version and remote
381
+ if (currentContent !== local.content && currentContent !== remoteContent) {
382
+ return true;
383
+ }
384
+ }
385
+ catch {
386
+ return false;
387
+ }
388
+ return false;
389
+ }
390
+ /**
391
+ * Handle conflict resolution
392
+ * Returns: 'local' | 'remote' | 'merge' | 'skip'
393
+ */
394
+ async resolveConflict(filePath, localContent, remoteContent, callback) {
395
+ // Emit conflict event for UI to handle
396
+ this.emit('conflict', {
397
+ path: filePath,
398
+ localContent,
399
+ remoteContent,
400
+ resolve: callback,
401
+ });
402
+ }
403
+ /**
404
+ * Start watching local files for changes
405
+ */
406
+ startFileWatcher() {
407
+ if (this.fileWatcher || !this.platformInfo?.isContainer) {
408
+ return;
409
+ }
410
+ const workspaceDir = '/workspace';
411
+ const ignoredDirs = ['node_modules', '.git', 'dist', 'build', '.next'];
412
+ console.log('👁️ Started watching files for sync...');
413
+ // Use recursive file watching
414
+ this.fileWatcher = fs.watch(workspaceDir, { recursive: true }, (eventType, filename) => {
415
+ if (!filename)
416
+ return;
417
+ // Skip ignored directories
418
+ if (ignoredDirs.some(dir => filename.startsWith(dir + '/'))) {
419
+ return;
420
+ }
421
+ const fullPath = path.join(workspaceDir, filename);
422
+ const relativePath = filename;
423
+ // Debounce and determine change type
424
+ const changeType = fs.existsSync(fullPath) ?
425
+ (eventType === 'rename' ? 'create' : 'modify') :
426
+ 'delete';
427
+ const change = {
428
+ type: changeType,
429
+ path: relativePath,
430
+ timestamp: Date.now(),
431
+ };
432
+ this.notifyFileChange(change);
433
+ this.emit('localChange', change);
434
+ });
435
+ }
436
+ /**
437
+ * Stop file watcher
438
+ */
439
+ stopFileWatcher() {
440
+ if (this.fileWatcher) {
441
+ this.fileWatcher.close();
442
+ this.fileWatcher = null;
443
+ console.log('👁️ Stopped file watcher');
444
+ }
445
+ }
446
+ /**
447
+ * Request file from platform
448
+ */
449
+ async requestFile(filePath) {
450
+ if (!this.connected || !this.ws) {
451
+ return;
452
+ }
453
+ this.ws.send(JSON.stringify({
454
+ action: 'read',
455
+ path: filePath,
456
+ }));
457
+ }
458
+ /**
459
+ * Request file list from platform
460
+ */
461
+ async requestFileList(directory = '/workspace') {
462
+ if (!this.connected || !this.ws) {
463
+ return;
464
+ }
465
+ this.ws.send(JSON.stringify({
466
+ action: 'list',
467
+ path: directory,
468
+ }));
469
+ }
470
+ /**
471
+ * Delete file via platform
472
+ */
473
+ async deleteFile(filePath) {
474
+ if (!this.connected || !this.ws) {
475
+ return;
476
+ }
477
+ this.ws.send(JSON.stringify({
478
+ action: 'delete',
479
+ path: filePath,
480
+ }));
481
+ }
482
+ /**
483
+ * Disconnect from platform
484
+ */
485
+ disconnect() {
486
+ if (this.reconnectTimeout) {
487
+ clearTimeout(this.reconnectTimeout);
488
+ this.reconnectTimeout = null;
489
+ }
490
+ if (this.ws) {
491
+ this.ws.close();
492
+ this.ws = null;
493
+ }
494
+ this.stopFileWatcher();
495
+ this.connected = false;
496
+ }
497
+ /**
498
+ * Get connection status
499
+ */
500
+ isConnected() {
501
+ return this.connected;
502
+ }
503
+ /**
504
+ * Get platform info
505
+ */
506
+ getPlatformInfo() {
507
+ return this.platformInfo;
508
+ }
509
+ /**
510
+ * Enable/disable sync
511
+ */
512
+ setSyncEnabled(enabled) {
513
+ this.syncEnabled = enabled;
514
+ console.log(`📡 Sync ${enabled ? 'enabled' : 'disabled'}`);
515
+ }
516
+ /**
517
+ * Display platform status
518
+ */
519
+ displayStatus() {
520
+ const info = this.platformInfo;
521
+ if (!info?.isContainer) {
522
+ console.log('📍 Running locally (not in recoder.xyz container)');
523
+ console.log('💡 Run in recoder.xyz terminal for live preview sync');
524
+ return;
525
+ }
526
+ console.log('🐳 Running in recoder.xyz container');
527
+ console.log(`📁 Project: ${info.projectId || 'Unknown'}`);
528
+ console.log(`🔗 Backend: ${info.backendUrl}`);
529
+ if (info.previewUrl) {
530
+ console.log(`🌐 Preview: ${info.previewUrl}`);
531
+ }
532
+ console.log(`📡 Sync: ${this.connected ? 'Connected' : 'Disconnected'}`);
533
+ }
534
+ /**
535
+ * Save chat message to platform (for CLI-web chat sync)
536
+ */
537
+ async saveChatMessage(message) {
538
+ if (!this.platformInfo?.projectId)
539
+ return;
540
+ try {
541
+ const response = await fetch(`${RECODER_WEB_URL}/api/chat-history`, {
542
+ method: 'POST',
543
+ headers: { 'Content-Type': 'application/json' },
544
+ body: JSON.stringify({
545
+ urlId: this.platformInfo.projectId,
546
+ projectId: this.platformInfo.projectId,
547
+ messages: [message],
548
+ source: 'cli',
549
+ cliVersion: process.env['npm_package_version'] || '2.5.0',
550
+ }),
551
+ });
552
+ if (!response.ok) {
553
+ console.warn('Failed to sync chat to platform');
554
+ }
555
+ }
556
+ catch (e) {
557
+ // Silently ignore - chat sync is optional
558
+ }
559
+ }
560
+ /**
561
+ * Load chat history from platform
562
+ */
563
+ async loadChatHistory() {
564
+ if (!this.platformInfo?.projectId)
565
+ return [];
566
+ try {
567
+ const response = await fetch(`${RECODER_WEB_URL}/api/chat-history?urlId=${this.platformInfo.projectId}`);
568
+ if (response.ok) {
569
+ const data = await response.json();
570
+ return data.data?.[0]?.messages || [];
571
+ }
572
+ }
573
+ catch (e) {
574
+ // Silently ignore
575
+ }
576
+ return [];
577
+ }
578
+ /**
579
+ * Get the web URL for the current project
580
+ */
581
+ getWebProjectUrl() {
582
+ if (!this.platformInfo?.projectId)
583
+ return undefined;
584
+ return `${RECODER_WEB_URL}/chat/${this.platformInfo.projectId}`;
585
+ }
586
+ }
587
+ // Singleton instance
588
+ export const platformSync = new PlatformSyncService();
@@ -0,0 +1,16 @@
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 type { SlashCommand } from './types.js';
13
+ export declare const workflowCommand: SlashCommand;
14
+ export declare const analyticsCommand: SlashCommand;
15
+ export declare const costCommand: SlashCommand;
16
+ export declare const whoamiCommand: SlashCommand;