codevf 1.0.1 → 1.0.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 (105) hide show
  1. package/README.md +118 -62
  2. package/dist/commands/chat.d.ts +2 -0
  3. package/dist/commands/chat.d.ts.map +1 -0
  4. package/dist/commands/chat.js +130 -0
  5. package/dist/commands/chat.js.map +1 -0
  6. package/dist/commands/cvf-chat-command-content.d.ts +2 -0
  7. package/dist/commands/cvf-chat-command-content.d.ts.map +1 -0
  8. package/dist/commands/cvf-chat-command-content.js +22 -0
  9. package/dist/commands/cvf-chat-command-content.js.map +1 -0
  10. package/dist/commands/cvf-command-content.d.ts +2 -0
  11. package/dist/commands/cvf-command-content.d.ts.map +1 -0
  12. package/dist/commands/cvf-command-content.js +109 -0
  13. package/dist/commands/cvf-command-content.js.map +1 -0
  14. package/dist/commands/fix.d.ts +1 -0
  15. package/dist/commands/fix.d.ts.map +1 -1
  16. package/dist/commands/fix.js +36 -0
  17. package/dist/commands/fix.js.map +1 -1
  18. package/dist/commands/listen.d.ts +2 -0
  19. package/dist/commands/listen.d.ts.map +1 -0
  20. package/dist/commands/listen.js +109 -0
  21. package/dist/commands/listen.js.map +1 -0
  22. package/dist/commands/mcp-tools.d.ts.map +1 -1
  23. package/dist/commands/mcp-tools.js +15 -15
  24. package/dist/commands/mcp-tools.js.map +1 -1
  25. package/dist/commands/setup.d.ts.map +1 -1
  26. package/dist/commands/setup.js +138 -56
  27. package/dist/commands/setup.js.map +1 -1
  28. package/dist/index.js +130 -92
  29. package/dist/index.js.map +1 -1
  30. package/dist/lib/api/client.d.ts +4 -0
  31. package/dist/lib/api/client.d.ts.map +1 -1
  32. package/dist/lib/api/client.js +6 -0
  33. package/dist/lib/api/client.js.map +1 -1
  34. package/dist/lib/api/projects.d.ts +32 -0
  35. package/dist/lib/api/projects.d.ts.map +1 -0
  36. package/dist/lib/api/projects.js +61 -0
  37. package/dist/lib/api/projects.js.map +1 -0
  38. package/dist/lib/api/sessions.d.ts +85 -0
  39. package/dist/lib/api/sessions.d.ts.map +1 -0
  40. package/dist/lib/api/sessions.js +137 -0
  41. package/dist/lib/api/sessions.js.map +1 -0
  42. package/dist/lib/api/tasks.d.ts +44 -0
  43. package/dist/lib/api/tasks.d.ts.map +1 -1
  44. package/dist/lib/api/tasks.js +82 -1
  45. package/dist/lib/api/tasks.js.map +1 -1
  46. package/dist/lib/api/websocket.d.ts +4 -2
  47. package/dist/lib/api/websocket.d.ts.map +1 -1
  48. package/dist/lib/api/websocket.js +54 -5
  49. package/dist/lib/api/websocket.js.map +1 -1
  50. package/dist/lib/auth/token-manager.d.ts.map +1 -1
  51. package/dist/lib/auth/token-manager.js +17 -6
  52. package/dist/lib/auth/token-manager.js.map +1 -1
  53. package/dist/lib/utils/logger.d.ts +3 -0
  54. package/dist/lib/utils/logger.d.ts.map +1 -1
  55. package/dist/lib/utils/logger.js +12 -4
  56. package/dist/lib/utils/logger.js.map +1 -1
  57. package/dist/mcp/index.js +136 -8
  58. package/dist/mcp/index.js.map +1 -1
  59. package/dist/mcp/tools/chat.d.ts +81 -1
  60. package/dist/mcp/tools/chat.d.ts.map +1 -1
  61. package/dist/mcp/tools/chat.js +624 -21
  62. package/dist/mcp/tools/chat.js.map +1 -1
  63. package/dist/mcp/tools/instant.d.ts +20 -5
  64. package/dist/mcp/tools/instant.d.ts.map +1 -1
  65. package/dist/mcp/tools/instant.js +322 -27
  66. package/dist/mcp/tools/instant.js.map +1 -1
  67. package/dist/mcp/tools/listen.d.ts +35 -0
  68. package/dist/mcp/tools/listen.d.ts.map +1 -0
  69. package/dist/mcp/tools/listen.js +97 -0
  70. package/dist/mcp/tools/listen.js.map +1 -0
  71. package/dist/mcp/tools/task-checker.d.ts +60 -0
  72. package/dist/mcp/tools/task-checker.d.ts.map +1 -0
  73. package/dist/mcp/tools/task-checker.js +139 -0
  74. package/dist/mcp/tools/task-checker.js.map +1 -0
  75. package/dist/mcp/tools/tunnel.d.ts +38 -0
  76. package/dist/mcp/tools/tunnel.d.ts.map +1 -0
  77. package/dist/mcp/tools/tunnel.js +109 -0
  78. package/dist/mcp/tools/tunnel.js.map +1 -0
  79. package/dist/modules/commandHandler.d.ts.map +1 -1
  80. package/dist/modules/commandHandler.js +0 -17
  81. package/dist/modules/commandHandler.js.map +1 -1
  82. package/dist/modules/permissions.d.ts +1 -0
  83. package/dist/modules/permissions.d.ts.map +1 -1
  84. package/dist/modules/permissions.js +2 -0
  85. package/dist/modules/permissions.js.map +1 -1
  86. package/dist/modules/tunnel.d.ts +5 -0
  87. package/dist/modules/tunnel.d.ts.map +1 -1
  88. package/dist/modules/tunnel.js +55 -0
  89. package/dist/modules/tunnel.js.map +1 -1
  90. package/dist/modules/websocket.d.ts.map +1 -1
  91. package/dist/modules/websocket.js +3 -2
  92. package/dist/modules/websocket.js.map +1 -1
  93. package/dist/types/index.d.ts +7 -3
  94. package/dist/types/index.d.ts.map +1 -1
  95. package/dist/types/index.js.map +1 -1
  96. package/dist/ui/LiveSession.js +2 -2
  97. package/dist/ui/LiveSession.js.map +1 -1
  98. package/dist/ui/SessionUI.d.ts +1 -1
  99. package/dist/ui/SessionUI.d.ts.map +1 -1
  100. package/dist/ui/SessionUI.js +4 -10
  101. package/dist/ui/SessionUI.js.map +1 -1
  102. package/dist/utils/errors.d.ts.map +1 -1
  103. package/dist/utils/errors.js +8 -7
  104. package/dist/utils/errors.js.map +1 -1
  105. package/package.json +4 -3
@@ -3,9 +3,41 @@
3
3
  * Extended collaboration sessions
4
4
  */
5
5
  import { logger } from '../../lib/utils/logger.js';
6
+ import axios from 'axios';
7
+ import WebSocket from 'ws';
8
+ import { checkForActiveTasks } from './task-checker.js';
9
+ // Timeout constants
10
+ const ENGINEER_RESPONSE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
11
+ /**
12
+ * IMPORTANT: This class is NOT thread-safe for concurrent executions.
13
+ *
14
+ * The class uses shared instance variables (wsConnection, messageBuffer,
15
+ * responseResolver, currentTaskId, hasConnected) that will conflict if
16
+ * multiple tool executions happen concurrently.
17
+ *
18
+ * Current behavior: Only ONE active chat session per ChatTool instance.
19
+ * - Subsequent calls will disconnect any existing session
20
+ * - Message buffers and response handlers will be overwritten
21
+ *
22
+ * This is acceptable for MCP tools as they typically execute sequentially,
23
+ * but be aware that calling execute() while another execution is in progress
24
+ * will terminate the previous session.
25
+ *
26
+ * Future improvement: Move wsConnection, messageBuffer, etc. into a per-execution
27
+ * context object to support concurrent sessions.
28
+ */
6
29
  export class ChatTool {
7
- constructor(tasksApi, baseUrl) {
30
+ constructor(tasksApi, projectsApi, apiClient, sessionsApi, baseUrl) {
31
+ this.wsConnection = null;
32
+ this.messageBuffer = [];
33
+ this.responseResolver = null;
34
+ this.responseRejecter = null;
35
+ this.currentTaskId = null;
36
+ this.hasConnected = false;
8
37
  this.tasksApi = tasksApi;
38
+ this.projectsApi = projectsApi;
39
+ this.apiClient = apiClient;
40
+ this.sessionsApi = sessionsApi;
9
41
  this.baseUrl = baseUrl;
10
42
  }
11
43
  /**
@@ -13,7 +45,141 @@ export class ChatTool {
13
45
  */
14
46
  async execute(args) {
15
47
  try {
16
- logger.info('Executing codevf-chat', { message: args.message });
48
+ logger.info('Executing codevf-chat', {
49
+ message: args.message,
50
+ attachmentCount: args.attachments?.length || 0,
51
+ continueTaskId: args.continueTaskId,
52
+ });
53
+ // Get or create a project for this task
54
+ logger.info('Getting or creating project for chat');
55
+ const project = await this.projectsApi.getOrCreateDefault();
56
+ logger.info('Using project', { projectId: project.id, repoUrl: project.repoUrl });
57
+ // Check for active tasks and ask user for preference
58
+ const taskCheck = await checkForActiveTasks(this.tasksApi, project.id.toString(), args.continueTaskId, 'chat', args.message);
59
+ let parentTaskId;
60
+ if (taskCheck.shouldPromptUser) {
61
+ const task = taskCheck.decision?.existingTask;
62
+ const options = taskCheck.decision?.options;
63
+ const agentInstruction = taskCheck.decision?.agentInstruction;
64
+ // If no decision was provided, ask the user
65
+ if (!args.decision) {
66
+ return {
67
+ content: [
68
+ {
69
+ type: 'text',
70
+ text: JSON.stringify({
71
+ agentInstruction,
72
+ activeTask: task,
73
+ options: options,
74
+ }, null, 2),
75
+ },
76
+ ],
77
+ };
78
+ }
79
+ // Handle the user's decision
80
+ logger.info('Processing user decision', { decision: args.decision, taskId: task?.id });
81
+ switch (args.decision) {
82
+ case 'reconnect':
83
+ logger.info('User chose to reconnect to existing chat session');
84
+ // Resume the existing task without sending new message
85
+ if (task?.id) {
86
+ taskCheck.taskToResumeId = task.id;
87
+ taskCheck.shouldPromptUser = false;
88
+ // Clear message so we don't send it
89
+ args.message = '';
90
+ logger.info('Will reconnect to session', { taskId: task.id });
91
+ }
92
+ break;
93
+ case 'followup':
94
+ logger.info('User chose to send followup to existing chat session');
95
+ // Resume the existing task and send the new message
96
+ if (task?.id) {
97
+ // Set taskToResumeId so it gets handled below
98
+ taskCheck.taskToResumeId = task.id;
99
+ taskCheck.shouldPromptUser = false;
100
+ logger.info('Will reconnect and send message to session', { taskId: task.id });
101
+ }
102
+ break;
103
+ case 'override':
104
+ logger.info('User chose to override existing chat session');
105
+ // Store parent task ID for reference chain
106
+ if (task?.id) {
107
+ parentTaskId = task.id;
108
+ logger.info('Storing parent task ID for new task', { parentTaskId });
109
+ try {
110
+ logger.info('Overriding existing chat task', { taskId: task.id });
111
+ await this.apiClient.request(`/api/cli/tasks/${task.id}/override`, {
112
+ method: 'POST',
113
+ });
114
+ logger.info('Chat task overridden successfully');
115
+ }
116
+ catch (err) {
117
+ logger.error('Failed to override chat task', err);
118
+ // Do not proceed to create a new task if override fails; surface the issue to the user.
119
+ return {
120
+ content: [
121
+ {
122
+ type: 'text',
123
+ text: 'Failed to override the existing chat session. A new task was not created. ' +
124
+ 'Please try again or choose a different option.',
125
+ },
126
+ ],
127
+ };
128
+ }
129
+ }
130
+ // Continue to create new task
131
+ break;
132
+ }
133
+ }
134
+ // If we have a task to resume, continue with it instead of creating a new one
135
+ if (taskCheck.taskToResumeId) {
136
+ logger.info('Resuming existing task', { taskId: taskCheck.taskToResumeId });
137
+ // Set hasConnected based on previouslyConnected parameter
138
+ if (args.previouslyConnected) {
139
+ this.hasConnected = true;
140
+ logger.info('Skipping greeting - previously connected to this session');
141
+ }
142
+ // Fetch parent task chain to understand the context
143
+ try {
144
+ const parentChain = await this.tasksApi.getParentTaskChain(project.id.toString(), taskCheck.taskToResumeId);
145
+ if (parentChain.hasParent && parentChain.parentChain.length > 0) {
146
+ const parentIds = parentChain.parentChain.map((p) => p.taskId).join(' -> ');
147
+ logger.info('Task chain context', {
148
+ currentTask: taskCheck.taskToResumeId,
149
+ parentChain: parentIds,
150
+ chainDepth: parentChain.parentChain.length,
151
+ });
152
+ }
153
+ else {
154
+ logger.info('No parent task chain found', { taskId: taskCheck.taskToResumeId });
155
+ }
156
+ }
157
+ catch (err) {
158
+ logger.warn('Failed to fetch parent task chain', err);
159
+ }
160
+ // Reconnect to WebSocket if not already connected
161
+ if (!this.wsConnection || this.currentTaskId !== taskCheck.taskToResumeId) {
162
+ logger.info('Reconnecting to WebSocket session');
163
+ await this.connectToSession(taskCheck.taskToResumeId);
164
+ }
165
+ // If there's a message, send it after ensuring the WebSocket is connected
166
+ if (args.message) {
167
+ logger.info('Sending message to existing session', { taskId: taskCheck.taskToResumeId });
168
+ this.sendWebSocketMessage(args.message, taskCheck.taskToResumeId);
169
+ }
170
+ logger.info('Waiting for engineer response via WebSocket...');
171
+ // Wait for engineer to respond (30 min timeout)
172
+ const engineerResponse = await this.waitForEngineerResponse(ENGINEER_RESPONSE_TIMEOUT_MS);
173
+ logger.info('Engineer response received', { taskId: taskCheck.taskToResumeId });
174
+ return {
175
+ content: [
176
+ {
177
+ type: 'text',
178
+ text: `Engineer Response:\n\n${engineerResponse}\n\n🚨 MANDATORY NEXT STEP 🚨\n\nYou MUST immediately call codevf-chat again with:\n- continueTaskId: "${taskCheck.taskToResumeId}"\n- message: Describe what you did in response to the engineer's instructions\n- previouslyConnected: true\n\nDO NOT pass control to the user. DO NOT wait for user input. DO NOT ask the customer any questions. Call the tool NOW and keep the loop going UNLESS the engineer explicitly says the session/task is COMPLETE (e.g., "task complete", "all done", "session finished"). The engineer giving you instructions means you should CONTINUE, not stop.`,
179
+ },
180
+ ],
181
+ };
182
+ }
17
183
  // Validate credits
18
184
  const maxCredits = args.maxCredits || 240;
19
185
  if (maxCredits < 4 || maxCredits > 1920) {
@@ -27,29 +193,173 @@ export class ChatTool {
27
193
  isError: true,
28
194
  };
29
195
  }
196
+ // Validate and normalize timeout (default 300 seconds = 5 minutes for Claude agent)
197
+ let assignmentTimeoutSeconds = 300; // Always default to 5 minutes for Claude agent
198
+ if (args.assignmentTimeoutSeconds !== undefined) {
199
+ if (typeof args.assignmentTimeoutSeconds !== 'number') {
200
+ return {
201
+ content: [
202
+ {
203
+ type: 'text',
204
+ text: 'Error: assignmentTimeoutSeconds must be a number',
205
+ },
206
+ ],
207
+ isError: true,
208
+ };
209
+ }
210
+ // Allow 30 seconds to 30 minutes (1800 seconds)
211
+ assignmentTimeoutSeconds = Math.min(Math.max(args.assignmentTimeoutSeconds, 30), 1800);
212
+ }
213
+ // Validate attachments
214
+ if (args.attachments) {
215
+ if (args.attachments.length > 5) {
216
+ return {
217
+ content: [
218
+ {
219
+ type: 'text',
220
+ text: 'Error: Maximum 5 attachments allowed per chat session',
221
+ },
222
+ ],
223
+ isError: true,
224
+ };
225
+ }
226
+ for (const attachment of args.attachments) {
227
+ if (!attachment.fileName || !attachment.content || !attachment.mimeType) {
228
+ return {
229
+ content: [
230
+ {
231
+ type: 'text',
232
+ text: 'Error: Each attachment must have fileName, content, and mimeType',
233
+ },
234
+ ],
235
+ isError: true,
236
+ };
237
+ }
238
+ // Validate file size (10MB for images/PDFs, 1MB for text)
239
+ const isImage = attachment.mimeType.startsWith('image/');
240
+ const isPdf = attachment.mimeType === 'application/pdf';
241
+ const maxSize = isImage || isPdf ? 10 * 1024 * 1024 : 1 * 1024 * 1024;
242
+ let fileSize = 0;
243
+ try {
244
+ if (isImage || isPdf) {
245
+ fileSize = Buffer.from(attachment.content, 'base64').length;
246
+ }
247
+ else {
248
+ fileSize = Buffer.byteLength(attachment.content, 'utf8');
249
+ }
250
+ }
251
+ catch (error) {
252
+ return {
253
+ content: [
254
+ {
255
+ type: 'text',
256
+ text: `Error: Invalid content encoding for file ${attachment.fileName}`,
257
+ },
258
+ ],
259
+ isError: true,
260
+ };
261
+ }
262
+ if (fileSize > maxSize) {
263
+ const maxSizeMB = Math.round(maxSize / (1024 * 1024));
264
+ return {
265
+ content: [
266
+ {
267
+ type: 'text',
268
+ text: `Error: File ${attachment.fileName} is too large (max ${maxSizeMB}MB for ${isImage ? 'images' : isPdf ? 'PDFs' : 'text files'})`,
269
+ },
270
+ ],
271
+ isError: true,
272
+ };
273
+ }
274
+ }
275
+ }
30
276
  // Create task
277
+ logger.info('Chat tool creating task', { message: args.message, maxCredits, parentTaskId });
31
278
  const task = await this.tasksApi.create({
32
279
  message: args.message,
33
280
  taskMode: 'realtime_chat',
34
281
  maxCredits,
282
+ projectId: project.id.toString(),
283
+ assignmentTimeoutSeconds,
284
+ parentActionId: parentTaskId, // Link to parent task for reference chain
35
285
  });
36
- logger.info('Chat session created', {
37
- taskId: task.taskId,
38
- actionId: task.actionId,
39
- });
40
- // Format response with session info
41
- const formattedResponse = this.formatResponse(task, maxCredits);
286
+ logger.info('Chat task created', { taskId: task.taskId });
287
+ // Connect to WebSocket as AI assistant
288
+ this.currentTaskId = task.taskId;
289
+ await this.connectToSession(task.taskId);
290
+ // Upload attachments if provided
291
+ if (args.attachments && args.attachments.length > 0) {
292
+ logger.info('Uploading attachments', { count: args.attachments.length });
293
+ try {
294
+ await this.uploadAttachments(task.taskId, args.attachments);
295
+ logger.info('All attachments uploaded successfully');
296
+ }
297
+ catch (uploadError) {
298
+ logger.error('Failed to upload attachments', uploadError);
299
+ return {
300
+ content: [
301
+ {
302
+ type: 'text',
303
+ text: `Error: Failed to upload attachments: ${uploadError.message}`,
304
+ },
305
+ ],
306
+ isError: true,
307
+ };
308
+ }
309
+ }
310
+ // Show warning if low balance
311
+ if (task.warning) {
312
+ logger.warn('Credit warning', { warning: task.warning });
313
+ }
314
+ // Return session URL for extended collaboration
315
+ const sessionUrl = `${this.baseUrl}/engineer/tasks/${task.taskId}`;
316
+ let response = `Chat session started successfully!\n\n`;
317
+ response += `Task ID: ${task.taskId}\n`;
318
+ response += `Max Credits: ${maxCredits}\n`;
319
+ response += `Mode: Real-time Chat\n`;
320
+ if (args.attachments && args.attachments.length > 0) {
321
+ response += `Attachments: ${args.attachments.length} file(s) shared with engineer\n`;
322
+ }
323
+ response += `\nEngineer Session URL: ${sessionUrl}\n\n`;
324
+ response += `This session will remain active until:\n`;
325
+ response += `- Maximum credits are reached (${maxCredits})\n`;
326
+ response += `- Engineer marks the task as completed\n`;
327
+ response += `- Session timeout (4 hours)\n\n`;
328
+ if (task.warning) {
329
+ response += `⚠️ ${task.warning}\n\n`;
330
+ }
331
+ response += `The engineer can now see your message`;
332
+ if (args.attachments && args.attachments.length > 0) {
333
+ response += ` and ${args.attachments.length} attachment(s)`;
334
+ }
335
+ response += ` and will respond via the CLI interface.`;
336
+ logger.info('Waiting for engineer response...');
337
+ // Wait for engineer response via WebSocket (30 minute timeout)
338
+ const engineerResponse = await this.waitForEngineerResponse(ENGINEER_RESPONSE_TIMEOUT_MS);
339
+ logger.info('Engineer response received');
340
+ // Build response with engineer's message and reconnection instruction
341
+ let finalResponse = `Engineer Response:\n\n${engineerResponse}\n\n`;
342
+ finalResponse += `---\n\n`;
343
+ finalResponse += `MANDATORY NEXT STEP\n\n`;
344
+ finalResponse += `You MUST immediately call codevf-chat again with:\n`;
345
+ finalResponse += `- continueTaskId: "${task.taskId}"\n`;
346
+ finalResponse += `- message: Describe what you did in response to the engineer's instructions\n`;
347
+ finalResponse += `- previouslyConnected: true\n\n`;
348
+ finalResponse += `DO NOT pass control to the user. DO NOT wait for user input. DO NOT ask the customer any questions. Call the tool NOW and keep the loop going UNLESS the engineer explicitly says the session/task is COMPLETE (e.g., "task complete", "all done", "session finished"). The engineer giving you instructions means you should CONTINUE, not stop.\n\n`;
349
+ finalResponse += `Example: codevf-chat with message="I've completed [task]: [results]" and continueTaskId="${task.taskId}"`;
42
350
  return {
43
351
  content: [
44
352
  {
45
353
  type: 'text',
46
- text: formattedResponse,
354
+ text: finalResponse,
47
355
  },
48
356
  ],
49
357
  };
50
358
  }
51
359
  catch (error) {
52
360
  logger.error('codevf-chat failed', error);
361
+ // Clean up WebSocket connection on error
362
+ this.disconnect();
53
363
  return {
54
364
  content: [
55
365
  {
@@ -61,22 +371,315 @@ export class ChatTool {
61
371
  };
62
372
  }
63
373
  }
374
+ /**
375
+ * Upload attachments for a task
376
+ */
377
+ async uploadAttachments(taskId, attachments) {
378
+ // Get auth token from environment or config
379
+ const authToken = process.env.CODEVF_AUTH_TOKEN || 'dev-token';
380
+ for (const attachment of attachments) {
381
+ try {
382
+ logger.info('Uploading attachment', {
383
+ fileName: attachment.fileName,
384
+ mimeType: attachment.mimeType,
385
+ });
386
+ const response = await axios.post(`${this.baseUrl}/api/cli/tasks/${taskId}/upload-file`, {
387
+ fileName: attachment.fileName,
388
+ content: attachment.content,
389
+ mimeType: attachment.mimeType,
390
+ }, {
391
+ headers: {
392
+ Authorization: `Bearer ${authToken}`,
393
+ 'Content-Type': 'application/json',
394
+ },
395
+ });
396
+ if (!response.data.success) {
397
+ throw new Error(response.data.error || 'Upload failed');
398
+ }
399
+ logger.info('Attachment uploaded successfully', {
400
+ fileName: attachment.fileName,
401
+ size: response.data.data?.size || 0,
402
+ });
403
+ }
404
+ catch (error) {
405
+ logger.error('Failed to upload attachment', {
406
+ fileName: attachment.fileName,
407
+ error: error.message,
408
+ });
409
+ throw new Error(`Failed to upload ${attachment.fileName}: ${error.message}`);
410
+ }
411
+ }
412
+ }
64
413
  /**
65
414
  * Format chat session info
66
415
  */
67
- formatResponse(task, maxCredits) {
68
- const sessionUrl = `${this.baseUrl}/session/${task.actionId}`;
69
- let output = 'Chat session started with engineer.\n\n';
70
- output += `Session URL: ${sessionUrl}\n\n`;
71
- output += 'Share this link with your user to monitor the conversation.\n\n';
72
- output += `Max credits: ${maxCredits}\n`;
73
- output += `Rate: 2 credits/minute\n`;
74
- output += `Estimated duration: ${Math.floor(maxCredits / 2)} minutes\n`;
75
- if (task.warning) {
76
- output += `\n⚠️ ${task.warning}\n`;
416
+ formatResponse(response) {
417
+ return {
418
+ content: [
419
+ {
420
+ type: 'text',
421
+ text: response,
422
+ },
423
+ ],
424
+ };
425
+ }
426
+ /**
427
+ * Connect Claude to the chat session via WebSocket
428
+ */
429
+ async connectToSession(taskId) {
430
+ try {
431
+ // Get authentication token
432
+ const token = await this.apiClient.getToken();
433
+ // Build WebSocket URL
434
+ const wsUrl = this.baseUrl.replace('http://', 'ws://').replace('https://', 'wss://');
435
+ const connectionUrl = `${wsUrl}/ws?taskId=${taskId}&userType=ai-assistant`;
436
+ logger.info('Connecting Claude to WebSocket session', { taskId, url: connectionUrl });
437
+ // Create WebSocket connection with authentication
438
+ // Pass token as protocol (second parameter), not in headers
439
+ this.wsConnection = new WebSocket(connectionUrl, token);
440
+ // Wait for the WebSocket connection to be established before returning
441
+ await new Promise((resolve, reject) => {
442
+ if (!this.wsConnection) {
443
+ reject(new Error('WebSocket connection was not created'));
444
+ return;
445
+ }
446
+ this.wsConnection.once('open', () => {
447
+ logger.info('Claude connected to chat session', { taskId });
448
+ resolve();
449
+ });
450
+ this.wsConnection.once('error', (error) => {
451
+ logger.error('WebSocket connection error during initial connection', error);
452
+ reject(error);
453
+ });
454
+ });
455
+ // Setup event handlers for the established connection
456
+ this.wsConnection.on('message', (data) => {
457
+ try {
458
+ const message = JSON.parse(data.toString());
459
+ this.handleWebSocketMessage(message, taskId);
460
+ }
461
+ catch (error) {
462
+ logger.error('Error parsing WebSocket message', error);
463
+ }
464
+ });
465
+ this.wsConnection.on('error', (error) => {
466
+ logger.error('WebSocket connection error', error);
467
+ });
468
+ this.wsConnection.on('close', () => {
469
+ logger.info('Claude disconnected from chat session', { taskId });
470
+ this.wsConnection = null;
471
+ });
472
+ }
473
+ catch (error) {
474
+ logger.error('Failed to connect to WebSocket', error);
475
+ // Don't throw - allow the chat to continue without WebSocket
476
+ }
477
+ }
478
+ /**
479
+ * Handle incoming WebSocket messages
480
+ */
481
+ handleWebSocketMessage(message, taskId) {
482
+ logger.debug('WebSocket message received', { type: message.type, taskId });
483
+ switch (message.type) {
484
+ case 'connected':
485
+ logger.info('WebSocket connection confirmed', { taskId });
486
+ // Send initial greeting only on first connection
487
+ if (!this.hasConnected) {
488
+ this.sendWebSocketMessage("Hello! I'm Claude, an AI assistant monitoring this session. I can provide code analysis, debugging help, and suggestions. Feel free to ask me questions or request my input at any time.", taskId);
489
+ this.hasConnected = true;
490
+ }
491
+ break;
492
+ case 'customer_message':
493
+ case 'engineer_message':
494
+ // Log the conversation for context
495
+ const sender = message.payload?.sender || message.type.replace('_message', '');
496
+ // Support both payload.content and payload.message for backwards compatibility
497
+ const content = message.payload?.content || message.payload?.message || '';
498
+ logger.debug('Chat message', { sender, content: content.substring(0, 100) });
499
+ // Analyze if Claude should respond
500
+ this.analyzeAndRespond(content, sender, taskId);
501
+ break;
502
+ case 'request_command':
503
+ const command = message.payload?.command || '';
504
+ logger.info('Engineer requested command', { command });
505
+ // Could warn about dangerous commands
506
+ this.analyzeCommand(command, taskId);
507
+ break;
508
+ case 'command_output':
509
+ const output = message.payload?.output || '';
510
+ logger.debug('Command output received', { length: output.length });
511
+ // Could analyze errors in output
512
+ this.analyzeOutput(output, taskId);
513
+ break;
514
+ case 'engineer_connected':
515
+ logger.info('Engineer joined the session', { engineerId: message.payload?.userId });
516
+ break;
517
+ case 'customer_connected':
518
+ logger.info('Customer joined the session', { customerId: message.payload?.userId });
519
+ break;
520
+ }
521
+ }
522
+ /**
523
+ * Send a message via WebSocket
524
+ */
525
+ sendWebSocketMessage(content, taskId) {
526
+ if (this.wsConnection && this.wsConnection.readyState === WebSocket.OPEN) {
527
+ this.wsConnection.send(JSON.stringify({
528
+ type: 'ai_assistant_message',
529
+ timestamp: new Date().toISOString(),
530
+ payload: {
531
+ content,
532
+ metadata: {
533
+ source: 'claude-mcp',
534
+ taskId,
535
+ },
536
+ },
537
+ }));
538
+ logger.info('Claude sent message via WebSocket', {
539
+ taskId,
540
+ preview: content.substring(0, 50),
541
+ });
542
+ }
543
+ else {
544
+ logger.warn('Failed to send WebSocket message: connection is not open', {
545
+ taskId,
546
+ hasConnection: !!this.wsConnection,
547
+ readyState: this.wsConnection?.readyState,
548
+ preview: content.substring(0, 50),
549
+ });
550
+ }
551
+ }
552
+ /**
553
+ * Accumulate messages and resolve when engineer responds
554
+ */
555
+ analyzeAndRespond(content, sender, taskId) {
556
+ // Add message to buffer
557
+ this.messageBuffer.push({
558
+ sender,
559
+ content,
560
+ timestamp: new Date().toISOString(),
561
+ });
562
+ logger.info('Session message', {
563
+ taskId,
564
+ sender,
565
+ content: content.substring(0, 200),
566
+ bufferSize: this.messageBuffer.length,
567
+ });
568
+ // If this is an engineer message and we're waiting for response, resolve
569
+ if (sender === 'engineer' && this.responseResolver) {
570
+ // Send acknowledgment before disconnecting
571
+ const acknowledgment = "Got it! Working on this now. I'll report back once complete.";
572
+ this.sendWebSocketMessage(acknowledgment, taskId);
573
+ logger.info('Sent acknowledgment, preparing to disconnect and work', { taskId });
574
+ const allMessages = this.messageBuffer
575
+ .map((msg) => `[${msg.sender}]: ${msg.content}`)
576
+ .join('\n\n');
577
+ this.responseResolver(allMessages);
578
+ this.responseResolver = null;
579
+ this.messageBuffer = []; // Clear buffer after resolving
580
+ }
581
+ }
582
+ /**
583
+ * Analyze requested command and log if dangerous
584
+ */
585
+ analyzeCommand(command, taskId) {
586
+ const dangerous = ['rm -rf', 'sudo rm', 'drop database', 'delete from', 'format', 'del /f'];
587
+ const isDangerous = dangerous.some((cmd) => command.toLowerCase().includes(cmd));
588
+ // Log for Claude's awareness, but don't send automatic warnings
589
+ logger.info('Command requested', {
590
+ taskId,
591
+ command,
592
+ isDangerous,
593
+ warning: isDangerous ? 'This command could be destructive' : undefined,
594
+ });
595
+ }
596
+ /**
597
+ * Analyze command output and log errors for Claude's context
598
+ */
599
+ analyzeOutput(output, taskId) {
600
+ const lowerOutput = output.toLowerCase();
601
+ const hasError = lowerOutput.includes('error:') || lowerOutput.includes('exception:');
602
+ const hasFailed = lowerOutput.includes('failed');
603
+ // Log output analysis for Claude's context
604
+ if (hasError || hasFailed) {
605
+ const lines = output.split('\n').slice(0, 5);
606
+ const errorPreview = lines.join('\n');
607
+ logger.info('Command output with errors', {
608
+ taskId,
609
+ hasError,
610
+ hasFailed,
611
+ preview: errorPreview.substring(0, 300),
612
+ });
613
+ }
614
+ }
615
+ /**
616
+ * Wait for engineer response via WebSocket
617
+ */
618
+ async waitForEngineerResponse(timeoutMs) {
619
+ return new Promise((resolve, reject) => {
620
+ // Set up resolver and rejecter
621
+ this.responseResolver = resolve;
622
+ this.responseRejecter = reject;
623
+ // Set timeout
624
+ const timeout = setTimeout(() => {
625
+ if (this.responseResolver) {
626
+ this.responseResolver = null;
627
+ this.responseRejecter = null;
628
+ reject(new Error('Timeout waiting for engineer response'));
629
+ }
630
+ }, timeoutMs);
631
+ // Clear timeout when resolved
632
+ const originalResolver = this.responseResolver;
633
+ this.responseResolver = (value) => {
634
+ clearTimeout(timeout);
635
+ this.responseRejecter = null;
636
+ originalResolver(value);
637
+ };
638
+ });
639
+ }
640
+ /**
641
+ * Send disconnect notification to engineer and close WebSocket
642
+ */
643
+ async notifyDisconnect() {
644
+ if (this.wsConnection && this.wsConnection.readyState === WebSocket.OPEN) {
645
+ try {
646
+ logger.info('Sending disconnect notification to engineer', { taskId: this.currentTaskId });
647
+ // Send explicit disconnect notification
648
+ this.wsConnection.send(JSON.stringify({
649
+ type: 'end_session',
650
+ timestamp: new Date().toISOString(),
651
+ payload: {
652
+ endedBy: 'customer',
653
+ reason: 'Customer closed Claude Code session',
654
+ },
655
+ }));
656
+ // Wait a moment for message to send before closing
657
+ await new Promise((resolve) => setTimeout(resolve, 500));
658
+ logger.info('Disconnect notification sent');
659
+ }
660
+ catch (error) {
661
+ logger.error('Failed to send disconnect notification', error);
662
+ }
663
+ }
664
+ }
665
+ /**
666
+ * Disconnect from WebSocket
667
+ */
668
+ disconnect() {
669
+ // Reject any pending response promise before cleanup
670
+ if (this.responseRejecter) {
671
+ const rejecter = this.responseRejecter;
672
+ this.responseResolver = null;
673
+ this.responseRejecter = null;
674
+ logger.warn('Rejecting pending response promise due to disconnect');
675
+ rejecter(new Error('WebSocket connection closed during response wait'));
676
+ }
677
+ if (this.wsConnection) {
678
+ this.wsConnection.close();
679
+ this.wsConnection = null;
77
680
  }
78
- output += `\nCredits remaining: ${task.creditsRemaining}\n`;
79
- return output;
681
+ this.messageBuffer = [];
682
+ this.currentTaskId = null;
80
683
  }
81
684
  }
82
685
  //# sourceMappingURL=chat.js.map