crewx 0.8.8-rc.2 → 0.8.8-rc.21

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/bin/cli-commands.js +1 -1
  2. package/dist/assets/{MarketPage-Bh1UfiC2.js → MarketPage-B5JbqK4T.js} +9 -14
  3. package/dist/assets/{PromptTab-DYmYVBKo.js → PromptTab-_97mSbLk.js} +7 -7
  4. package/dist/assets/{arc-C14FbhzP.js → arc-3FDm7TxG.js} +1 -1
  5. package/dist/assets/architectureDiagram-UYN6MBPD-D8X56UlC.js +36 -0
  6. package/dist/assets/blockDiagram-ZHA2E4KO-B6jUnEVc.js +121 -0
  7. package/dist/assets/{c4Diagram-6F5ED5ID-DLSfQS-Z.js → c4Diagram-6F5ED5ID-Cy8x6Ims.js} +1 -1
  8. package/dist/assets/channel-Bs0J8jjN.js +1 -0
  9. package/dist/assets/{chunk-5HRBRIJM-j7YcJ7Bx.js → chunk-5HRBRIJM-Bh819Q5A.js} +1 -1
  10. package/dist/assets/{chunk-7U56Z5CX-Baqt1cj3.js → chunk-7U56Z5CX-CfNtPGa8.js} +1 -1
  11. package/dist/assets/{chunk-ASOPGD6M-F1YJ2mzH.js → chunk-ASOPGD6M-CMR1ElQ6.js} +1 -1
  12. package/dist/assets/{chunk-KFBOBJHC-Dvhzl6G9.js → chunk-KFBOBJHC-Dm2I1dHM.js} +1 -1
  13. package/dist/assets/{chunk-T2TOU4HS-BDczCXP6.js → chunk-T2TOU4HS-0tf-aNtk.js} +1 -1
  14. package/dist/assets/{chunk-TMUBEWPD-4BhwBH_F.js → chunk-TMUBEWPD-b5ZrpKJw.js} +1 -1
  15. package/dist/assets/classDiagram-LNE6IOMH-B1-6qC07.js +1 -0
  16. package/dist/assets/classDiagram-v2-MQ7JQ4JX-B1-6qC07.js +1 -0
  17. package/dist/assets/dagre-4EVJKHTY-zYmFBlwR.js +4 -0
  18. package/dist/assets/diagram-QW4FP2JN-DkZngH-Z.js +24 -0
  19. package/dist/assets/{erDiagram-6RL3IURR-DRhNKkH0.js → erDiagram-6RL3IURR-8TfuIQ6D.js} +2 -2
  20. package/dist/assets/{flowDiagram-7ASYPVHJ-BDJZsUJS.js → flowDiagram-7ASYPVHJ-BnhSLZTo.js} +1 -1
  21. package/dist/assets/{ganttDiagram-NTVNEXSI-BYBeyULV.js → ganttDiagram-NTVNEXSI--jDGPnYB.js} +3 -3
  22. package/dist/assets/gitGraph-YCYPL57B-DkCOt6bF.js +133 -0
  23. package/dist/assets/gitGraphDiagram-NRZ2UAAF-D4p17Lzw.js +65 -0
  24. package/dist/assets/graph-BlwPajkw.js +1 -0
  25. package/dist/assets/infoDiagram-A4XQUW5V-COnYi0Q8.js +2 -0
  26. package/dist/assets/{journeyDiagram-G5WM74LC-BhSSQ4Od.js → journeyDiagram-G5WM74LC-CTZcqDJf.js} +1 -1
  27. package/dist/assets/{kanban-definition-QRCXZQQD-CZOkitn-.js → kanban-definition-QRCXZQQD-CezLYsK3.js} +1 -1
  28. package/dist/assets/layout-Bvwu4dCi.js +1 -0
  29. package/dist/assets/{linear-BRR4YPhd.js → linear-BcYBjD0V.js} +1 -1
  30. package/dist/assets/main-CgT_Atre.js +1210 -0
  31. package/dist/assets/main-kJ_DZpZU.css +10 -0
  32. package/dist/assets/{mindmap-definition-GWI6TPTV-CiMg1rSL.js → mindmap-definition-GWI6TPTV-NM7qIPkp.js} +1 -1
  33. package/dist/assets/pieDiagram-YF2LJOPJ-Ck2UBMrZ.js +30 -0
  34. package/dist/assets/{quadrantDiagram-OS5C2QUG-cYMjsA0L.js → quadrantDiagram-OS5C2QUG-0OKnDbeW.js} +1 -1
  35. package/dist/assets/{requirementDiagram-MIRIMTAZ-D8piGWQV.js → requirementDiagram-MIRIMTAZ-Nq_mStSk.js} +2 -2
  36. package/dist/assets/{sankeyDiagram-Y46BX6SQ-DfNocUti.js → sankeyDiagram-Y46BX6SQ-ZPgHNk3C.js} +1 -1
  37. package/dist/assets/{sequenceDiagram-G6AWOVSC-Xu2X2iUX.js → sequenceDiagram-G6AWOVSC-BRzYMlx2.js} +1 -1
  38. package/dist/assets/stateDiagram-MAYHULR4-CCNAeCLE.js +1 -0
  39. package/dist/assets/stateDiagram-v2-4JROLMXI-Bz1kxCVf.js +1 -0
  40. package/dist/assets/{timeline-definition-U7ZMHBDA-CNra78s7.js → timeline-definition-U7ZMHBDA-BSw5vmnf.js} +1 -1
  41. package/dist/assets/{xychartDiagram-6QU3TZC5-BSwY5sKC.js → xychartDiagram-6QU3TZC5-BuqrahPf.js} +2 -2
  42. package/dist/index.html +2 -2
  43. package/dist-server/app.module.js +4 -0
  44. package/dist-server/bootstrap/crewx-server.js +6 -3
  45. package/dist-server/common/limits/defaults.js +2 -0
  46. package/dist-server/domain/auth/auth.module.js +2 -1
  47. package/dist-server/domain/auth/guards/base-auth.guard.js +36 -14
  48. package/dist-server/domain/doc/doc.controller.js +21 -0
  49. package/dist-server/domain/doc/doc.service.js +36 -6
  50. package/dist-server/domain/doc/image-constants.js +11 -0
  51. package/dist-server/domain/events/events.module.js +31 -0
  52. package/dist-server/domain/events/server-conversation.plugin.js +45 -0
  53. package/dist-server/domain/events/workspace-event.bus.js +32 -0
  54. package/dist-server/domain/events/workspace-event.types.js +12 -0
  55. package/dist-server/domain/events/workspace-events-test.controller.js +83 -0
  56. package/dist-server/domain/events/workspace-events.controller.js +47 -0
  57. package/dist-server/domain/mcp/browser-session.store.js +50 -0
  58. package/dist-server/domain/mcp/chromex-blacklist.js +26 -0
  59. package/dist-server/domain/mcp/chromex-negotiate.controller.js +77 -0
  60. package/dist-server/domain/mcp/chromex-negotiate.dto.js +95 -0
  61. package/dist-server/domain/mcp/chromex-negotiate.service.js +249 -0
  62. package/dist-server/domain/mcp/mcp-auth.guard.js +40 -26
  63. package/dist-server/domain/mcp/mcp.constants.js +4 -1
  64. package/dist-server/domain/mcp/mcp.controller.js +5 -4
  65. package/dist-server/domain/mcp/mcp.module.js +6 -2
  66. package/dist-server/domain/mcp/mcp.service.js +62 -5
  67. package/dist-server/domain/message/dto/send-message.dto.js +48 -1
  68. package/dist-server/domain/message/message.controller.js +40 -1
  69. package/dist-server/domain/message/message.service.js +50 -5
  70. package/dist-server/domain/task/task.service.js +34 -13
  71. package/dist-server/domain/thread/thread.controller.js +16 -0
  72. package/dist-server/domain/thread/thread.module.js +2 -0
  73. package/dist-server/domain/thread/thread.service.js +80 -11
  74. package/dist-server/domain/token/token.controller.js +73 -0
  75. package/dist-server/domain/token/token.dto.js +56 -0
  76. package/dist-server/domain/token/token.module.js +22 -0
  77. package/dist-server/domain/token/token.service.js +228 -0
  78. package/dist-server/main.js +23 -2
  79. package/dist-server/modules/crewx-pool.service.js +129 -5
  80. package/dist-server/modules/crewx.module.js +6 -0
  81. package/package.json +17 -14
  82. package/packages/cli/dist/builtin.js +2 -0
  83. package/packages/cli/dist/commands/init.js +5 -5
  84. package/packages/cli/dist/commands/registry.js +1 -1
  85. package/packages/cli/package.json +2 -1
  86. package/dist/assets/_baseUniq-B5kIqYhv.js +0 -1
  87. package/dist/assets/architectureDiagram-UYN6MBPD-Bvk4uHjQ.js +0 -36
  88. package/dist/assets/blockDiagram-ZHA2E4KO-C9-iioqe.js +0 -121
  89. package/dist/assets/channel-B1S1YSXQ.js +0 -1
  90. package/dist/assets/classDiagram-LNE6IOMH-BMiPdBl0.js +0 -1
  91. package/dist/assets/classDiagram-v2-MQ7JQ4JX-BMiPdBl0.js +0 -1
  92. package/dist/assets/clone-DkRtovKq.js +0 -1
  93. package/dist/assets/dagre-4EVJKHTY-LsgK-pqb.js +0 -4
  94. package/dist/assets/diagram-QW4FP2JN-uvhH5Eln.js +0 -24
  95. package/dist/assets/gitGraph-YCYPL57B-D9Hkzkze.js +0 -133
  96. package/dist/assets/gitGraphDiagram-NRZ2UAAF-oWiYlrjZ.js +0 -65
  97. package/dist/assets/graph-D66Q_If6.js +0 -1
  98. package/dist/assets/infoDiagram-A4XQUW5V-aJ6i9Snw.js +0 -2
  99. package/dist/assets/layout-CaxkerSC.js +0 -1
  100. package/dist/assets/main-3IYf82MG.css +0 -10
  101. package/dist/assets/main-DCHJFonK.js +0 -1167
  102. package/dist/assets/min-uKYaLZUT.js +0 -1
  103. package/dist/assets/pieDiagram-YF2LJOPJ-BFJ_DAm-.js +0 -30
  104. package/dist/assets/stateDiagram-MAYHULR4-WmJVTQsF.js +0 -1
  105. package/dist/assets/stateDiagram-v2-4JROLMXI-DNAOV6w0.js +0 -1
@@ -16,6 +16,8 @@ const mcp_dto_js_1 = require("./mcp.dto.js");
16
16
  const crewx_tool_factory_js_1 = require("./crewx-tool.factory.js");
17
17
  const browser_session_store_js_1 = require("./browser-session.store.js");
18
18
  const tool_router_service_js_1 = require("./tool-router.service.js");
19
+ const chromex_negotiate_service_js_1 = require("./chromex-negotiate.service.js");
20
+ const chromex_blacklist_js_1 = require("./chromex-blacklist.js");
19
21
  const crewx_pool_service_js_1 = require("../../modules/crewx-pool.service.js");
20
22
  const project_repository_js_1 = require("../../repository/project.repository.js");
21
23
  const repository_1 = require("@crewx/sdk/repository");
@@ -166,14 +168,16 @@ let McpService = McpService_1 = class McpService {
166
168
  browserSessions;
167
169
  toolRouter;
168
170
  toolCallRepo;
171
+ negotiateService;
169
172
  logger = new common_1.Logger(McpService_1.name);
170
173
  activeTaskMap = new Map();
171
- constructor(crewxPool, projectRepo, browserSessions, toolRouter, toolCallRepo) {
174
+ constructor(crewxPool, projectRepo, browserSessions, toolRouter, toolCallRepo, negotiateService) {
172
175
  this.crewxPool = crewxPool;
173
176
  this.projectRepo = projectRepo;
174
177
  this.browserSessions = browserSessions;
175
178
  this.toolRouter = toolRouter;
176
179
  this.toolCallRepo = toolCallRepo;
180
+ this.negotiateService = negotiateService;
177
181
  }
178
182
  async getToolForCurrentRequest() {
179
183
  const crewx = await this.crewxPool.getForCurrentRequest();
@@ -235,7 +239,7 @@ let McpService = McpService_1 = class McpService {
235
239
  isNotification(body) {
236
240
  return body.id === undefined || body.id === null;
237
241
  }
238
- async handleRequest(body, sessionId, clientType) {
242
+ async handleRequest(body, sessionId, clientType, request) {
239
243
  const { method, id } = body;
240
244
  this.logger.debug(`Handling MCP method: ${method}`);
241
245
  if (this.isNotification(body)) {
@@ -255,7 +259,7 @@ let McpService = McpService_1 = class McpService {
255
259
  case 'tools/list':
256
260
  return this.handleToolsList(id, sessionId);
257
261
  case 'tools/call':
258
- return this.handleToolsCall(id, body.params, sessionId);
262
+ return this.handleToolsCall(id, body.params, sessionId, request);
259
263
  case 'prompts/list':
260
264
  return {
261
265
  jsonrpc: '2.0',
@@ -294,6 +298,13 @@ let McpService = McpService_1 = class McpService {
294
298
  getBrowserSessionStore() {
295
299
  return this.browserSessions;
296
300
  }
301
+ /** Push a JSON-RPC notification to the latest Chrome X browser SSE session. Returns false if no browser connected. */
302
+ pushToChromexSession(payload) {
303
+ const session = this.browserSessions.getLatestBrowserSession();
304
+ if (!session)
305
+ return false;
306
+ return this.browserSessions.pushNotification(session.id, payload);
307
+ }
297
308
  handleInitialize(id, clientType) {
298
309
  const sessionId = this.createBrowserSession(clientType);
299
310
  return {
@@ -348,7 +359,7 @@ let McpService = McpService_1 = class McpService {
348
359
  this.browserSessions.registerTools(sessionId, params.tools, params.mode);
349
360
  return null;
350
361
  }
351
- async handleToolsCall(id, params, sessionId) {
362
+ async handleToolsCall(id, params, sessionId, request) {
352
363
  if (!params?.name) {
353
364
  return {
354
365
  jsonrpc: '2.0',
@@ -364,7 +375,52 @@ let McpService = McpService_1 = class McpService {
364
375
  // Browser tool: route via SSE to the extension (dual routing: prefix + session lookup)
365
376
  if (this.toolRouter.isBrowserTool(params.name, sessionId)) {
366
377
  this.logger.log(`tools/call [browser]: ${params.name}`);
378
+ const claims = request?.tokenClaims;
379
+ // Browser control is only available through a negotiate token. The token's
380
+ // agentScope is the authority; do not trust a client-supplied agentId.
381
+ if (claims?.source !== 'negotiate' || !claims.agentScope) {
382
+ return {
383
+ jsonrpc: '2.0',
384
+ id: id ?? null,
385
+ error: { code: -32001, message: 'Negotiate token required for browser tools' },
386
+ };
387
+ }
388
+ const requestedTabId = params.arguments?.['tabId'];
389
+ if (claims.browserScope &&
390
+ claims.browserScope !== 'all_tabs' &&
391
+ claims.browserScope.startsWith('tab:') &&
392
+ String(requestedTabId) !== claims.browserScope.slice(4)) {
393
+ return {
394
+ jsonrpc: '2.0',
395
+ id: id ?? null,
396
+ error: { code: -32001, message: 'Browser scope denied' },
397
+ };
398
+ }
399
+ const nestedArgs = params.arguments?.['args'];
400
+ const nestedUrl = nestedArgs && typeof nestedArgs === 'object' && !Array.isArray(nestedArgs)
401
+ ? nestedArgs['url']
402
+ : undefined;
403
+ const tabUrl = String(params.arguments?.['tabUrl'] ?? params.arguments?.['url'] ?? nestedUrl ?? '');
404
+ if (tabUrl && (0, chromex_blacklist_js_1.isBlacklisted)(tabUrl)) {
405
+ this.negotiateService.logAudit({
406
+ agentId: claims?.agentScope ?? 'unknown',
407
+ action: 'tool_call_blocked',
408
+ jti: claims?.jti,
409
+ tabUrl,
410
+ toolName: params.name,
411
+ result: 'blocked',
412
+ });
413
+ return { jsonrpc: '2.0', id: id ?? null, error: { code: -32001, message: 'URL is blacklisted' } };
414
+ }
367
415
  response = await this.toolRouter.dispatch(sessionId, params.name, params.arguments ?? {}, id);
416
+ this.negotiateService.logAudit({
417
+ agentId: claims?.agentScope ?? 'unknown',
418
+ action: 'tool_call',
419
+ jti: claims?.jti,
420
+ tabUrl: tabUrl || null,
421
+ toolName: params.name,
422
+ result: 'success',
423
+ });
368
424
  }
369
425
  else {
370
426
  // Server tool: must exist in TOOL_DEFINITIONS
@@ -671,7 +727,8 @@ exports.McpService = McpService = McpService_1 = __decorate([
671
727
  project_repository_js_1.ProjectRepository,
672
728
  browser_session_store_js_1.BrowserSessionStore,
673
729
  tool_router_service_js_1.ToolRouterService,
674
- repository_1.ToolCallRepository])
730
+ repository_1.ToolCallRepository,
731
+ chromex_negotiate_service_js_1.ChromexNegotiateService])
675
732
  ], McpService);
676
733
  function normalizeContent(result) {
677
734
  if (Array.isArray(result.content) && result.content.length > 0) {
@@ -9,14 +9,42 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.SendMessageDto = void 0;
12
+ exports.SendMessageDto = exports.ImageAttachmentDto = void 0;
13
13
  const swagger_1 = require("@nestjs/swagger");
14
14
  const class_validator_1 = require("class-validator");
15
+ const class_transformer_1 = require("class-transformer");
16
+ class ImageAttachmentDto {
17
+ data;
18
+ mimeType;
19
+ filename;
20
+ }
21
+ exports.ImageAttachmentDto = ImageAttachmentDto;
22
+ __decorate([
23
+ (0, class_validator_1.IsString)(),
24
+ (0, class_validator_1.IsNotEmpty)(),
25
+ (0, swagger_1.ApiProperty)({ description: 'Base64-encoded image data' }),
26
+ __metadata("design:type", String)
27
+ ], ImageAttachmentDto.prototype, "data", void 0);
28
+ __decorate([
29
+ (0, class_validator_1.IsString)(),
30
+ (0, class_validator_1.IsNotEmpty)(),
31
+ (0, swagger_1.ApiProperty)({ description: 'MIME type (image/png, image/jpeg, image/gif, image/webp)' }),
32
+ __metadata("design:type", String)
33
+ ], ImageAttachmentDto.prototype, "mimeType", void 0);
34
+ __decorate([
35
+ (0, class_validator_1.IsString)(),
36
+ (0, class_validator_1.IsOptional)(),
37
+ (0, swagger_1.ApiPropertyOptional)({ description: 'Original filename' }),
38
+ __metadata("design:type", String)
39
+ ], ImageAttachmentDto.prototype, "filename", void 0);
15
40
  class SendMessageDto {
16
41
  message;
17
42
  agent;
18
43
  title;
19
44
  mode = 'query';
45
+ images;
46
+ context;
47
+ browserSessionId;
20
48
  }
21
49
  exports.SendMessageDto = SendMessageDto;
22
50
  __decorate([
@@ -43,3 +71,22 @@ __decorate([
43
71
  (0, swagger_1.ApiPropertyOptional)({ description: 'Execution mode: query (read-only, default) or execute (full permissions)', default: 'query' }),
44
72
  __metadata("design:type", String)
45
73
  ], SendMessageDto.prototype, "mode", void 0);
74
+ __decorate([
75
+ (0, class_validator_1.IsOptional)(),
76
+ (0, class_validator_1.ValidateNested)({ each: true }),
77
+ (0, class_transformer_1.Type)(() => ImageAttachmentDto),
78
+ (0, swagger_1.ApiPropertyOptional)({ description: 'Attached images (base64)', type: [ImageAttachmentDto] }),
79
+ __metadata("design:type", Array)
80
+ ], SendMessageDto.prototype, "images", void 0);
81
+ __decorate([
82
+ (0, class_validator_1.IsString)(),
83
+ (0, class_validator_1.IsOptional)(),
84
+ (0, swagger_1.ApiPropertyOptional)({ description: 'Provider-agnostic context prepended to the message' }),
85
+ __metadata("design:type", String)
86
+ ], SendMessageDto.prototype, "context", void 0);
87
+ __decorate([
88
+ (0, class_validator_1.IsString)(),
89
+ (0, class_validator_1.IsOptional)(),
90
+ (0, swagger_1.ApiPropertyOptional)({ description: 'Chrome X browser MCP session id for reverse browser-tool calls' }),
91
+ __metadata("design:type", String)
92
+ ], SendMessageDto.prototype, "browserSessionId", void 0);
@@ -15,11 +15,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.MessageController = void 0;
16
16
  const common_1 = require("@nestjs/common");
17
17
  const swagger_1 = require("@nestjs/swagger");
18
+ const path_1 = require("path");
19
+ const fs_1 = require("fs");
18
20
  const message_service_js_1 = require("./message.service.js");
19
21
  const box_service_js_1 = require("../box/box.service.js");
20
22
  const send_message_dto_js_1 = require("./dto/send-message.dto.js");
21
23
  const list_messages_dto_js_1 = require("./dto/list-messages.dto.js");
22
24
  const workspace_decorator_js_1 = require("../../common/decorators/workspace.decorator.js");
25
+ const workspace_context_store_js_1 = require("../../common/workspace-context.store.js");
26
+ const EXT_MIME = {
27
+ png: 'image/png',
28
+ jpg: 'image/jpeg',
29
+ jpeg: 'image/jpeg',
30
+ gif: 'image/gif',
31
+ webp: 'image/webp',
32
+ };
23
33
  let MessageController = class MessageController {
24
34
  messageService;
25
35
  boxService;
@@ -39,9 +49,28 @@ let MessageController = class MessageController {
39
49
  }
40
50
  // MSG.SEND: Send message to thread (spawns crewx CLI in background)
41
51
  async sendMessage(threadId, body, ws) {
42
- const data = await this.messageService.sendMessage(threadId, body.message, body.agent, body.mode, body.title, ws.id);
52
+ const data = await this.messageService.sendMessage(threadId, body.message, body.agent, body.mode, body.title, ws.id, body.images, body.context, body.browserSessionId);
43
53
  return { success: true, data };
44
54
  }
55
+ serveAttachment(threadId, filename, res) {
56
+ if (filename.includes('/') || filename.includes('..')) {
57
+ throw new common_1.BadRequestException('Invalid filename');
58
+ }
59
+ const wsPath = (0, workspace_context_store_js_1.getWorkspacePath)();
60
+ const filePath = (0, path_1.join)(wsPath, '.crewx', 'attachments', threadId, filename);
61
+ if (!(0, fs_1.existsSync)(filePath)) {
62
+ throw new common_1.NotFoundException('Attachment not found');
63
+ }
64
+ const ext = filename.split('.').pop()?.toLowerCase() ?? '';
65
+ const mime = EXT_MIME[ext] ?? 'application/octet-stream';
66
+ const stat = (0, fs_1.statSync)(filePath);
67
+ res.set({
68
+ 'Content-Type': mime,
69
+ 'Content-Length': stat.size,
70
+ 'Cache-Control': 'public, max-age=31536000, immutable',
71
+ });
72
+ (0, fs_1.createReadStream)(filePath).pipe(res);
73
+ }
45
74
  };
46
75
  exports.MessageController = MessageController;
47
76
  __decorate([
@@ -65,6 +94,16 @@ __decorate([
65
94
  __metadata("design:paramtypes", [String, send_message_dto_js_1.SendMessageDto, Object]),
66
95
  __metadata("design:returntype", Promise)
67
96
  ], MessageController.prototype, "sendMessage", null);
97
+ __decorate([
98
+ (0, swagger_1.ApiOperation)({ operationId: 'MSG.ATTACHMENT', summary: 'Serve thread attachment image' }),
99
+ (0, common_1.Get)('attachments/:filename'),
100
+ __param(0, (0, common_1.Param)('threadId')),
101
+ __param(1, (0, common_1.Param)('filename')),
102
+ __param(2, (0, common_1.Res)()),
103
+ __metadata("design:type", Function),
104
+ __metadata("design:paramtypes", [String, String, Object]),
105
+ __metadata("design:returntype", void 0)
106
+ ], MessageController.prototype, "serveAttachment", null);
68
107
  exports.MessageController = MessageController = __decorate([
69
108
  (0, swagger_1.ApiTags)('messages'),
70
109
  (0, common_1.Controller)('ws/:slug/threads/:threadId/messages'),
@@ -12,8 +12,12 @@ var MessageService_1;
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.MessageService = void 0;
14
14
  const common_1 = require("@nestjs/common");
15
+ const crypto_1 = require("crypto");
16
+ const fs_1 = require("fs");
17
+ const path_1 = require("path");
15
18
  const thread_service_js_1 = require("../thread/thread.service.js");
16
19
  const thread_repository_js_1 = require("../../repository/thread.repository.js");
20
+ const sdk_1 = require("@crewx/sdk");
17
21
  const crewx_pool_service_js_1 = require("../../modules/crewx-pool.service.js");
18
22
  const workspace_context_store_js_1 = require("../../common/workspace-context.store.js");
19
23
  let MessageService = MessageService_1 = class MessageService {
@@ -56,6 +60,7 @@ let MessageService = MessageService_1 = class MessageService {
56
60
  prompt: t.prompt
57
61
  || (ctx.hot[i - 1]?.role === 'user' ? ctx.hot[i - 1].content : ''),
58
62
  content: msg.content,
63
+ error: t.error ?? undefined,
59
64
  startedAt: t.startedAt,
60
65
  completedAt: t.completedAt ?? undefined,
61
66
  durationMs: t.durationMs ?? undefined,
@@ -97,7 +102,7 @@ let MessageService = MessageService_1 = class MessageService {
97
102
  };
98
103
  }
99
104
  // MSG.SEND: Send message — fire crewx SDK; ConversationPlugin persists threads row
100
- async sendMessage(threadId, message, agent, mode = 'query', title, workspaceId) {
105
+ async sendMessage(threadId, message, agent, mode = 'query', title, workspaceId, images, context, browserSessionId) {
101
106
  this.validateId(threadId);
102
107
  if (!message || !message.trim()) {
103
108
  throw new common_1.BadRequestException('message is required');
@@ -115,10 +120,28 @@ let MessageService = MessageService_1 = class MessageService {
115
120
  this.logger.warn(`updateThreadTitle skipped [thread=${threadId}]: ${err.message}`);
116
121
  }
117
122
  }
123
+ const savedImages = this.saveImages(threadId, images);
124
+ let finalMessage = message;
125
+ if (savedImages.length > 0) {
126
+ const tags = savedImages
127
+ .map((img) => `<crewx_image path="${img.relativePath}" mime="${img.mimeType}" />`)
128
+ .join('\n');
129
+ finalMessage = `${message}\n${tags}`;
130
+ }
131
+ const trimmedContext = context?.trim();
132
+ if (trimmedContext) {
133
+ finalMessage = `<chrome_x_context>\n${trimmedContext}\n</chrome_x_context>\n\n${finalMessage}`;
134
+ }
135
+ const taskId = (0, sdk_1.generateId)('tsk');
136
+ const metadata = { thread: threadId, parentTaskId: null };
137
+ if (browserSessionId?.trim()) {
138
+ metadata.browserSessionId = browserSessionId.trim();
139
+ }
118
140
  const sdkOptions = {
119
141
  threadId,
142
+ taskId,
120
143
  platform: 'cli',
121
- metadata: { thread: threadId, parentTaskId: null },
144
+ metadata,
122
145
  cwd: (0, workspace_context_store_js_1.getWorkspacePath)(),
123
146
  };
124
147
  const crewx = await this.crewxPool.getForCurrentRequest();
@@ -133,14 +156,36 @@ let MessageService = MessageService_1 = class MessageService {
133
156
  }
134
157
  const agentRef = `@${agent}`;
135
158
  const sdkCall = mode === 'execute'
136
- ? crewx.execute(agentRef, message, sdkOptions)
137
- : crewx.query(agentRef, message, sdkOptions);
159
+ ? crewx.execute(agentRef, finalMessage, sdkOptions)
160
+ : crewx.query(agentRef, finalMessage, sdkOptions);
138
161
  sdkCall.catch((err) => {
139
162
  this.logger.error(`Background ${mode} failed [thread=${threadId}, agent=${agent}]: ${err.message}`, err.stack);
140
163
  });
141
- return { taskId: threadId, status: 'pending', agent };
164
+ return { taskId, status: 'pending', agent };
142
165
  }
143
166
  // --- Private helpers ---
167
+ saveImages(threadId, images) {
168
+ if (!images?.length)
169
+ return [];
170
+ const MIME_EXT = {
171
+ 'image/png': 'png',
172
+ 'image/jpeg': 'jpg',
173
+ 'image/gif': 'gif',
174
+ 'image/webp': 'webp',
175
+ };
176
+ const wsPath = (0, workspace_context_store_js_1.getWorkspacePath)();
177
+ const dir = (0, path_1.join)(wsPath, '.crewx', 'attachments', threadId);
178
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
179
+ return images.map((img) => {
180
+ const ext = MIME_EXT[img.mimeType] ?? 'bin';
181
+ const filename = `${(0, crypto_1.randomUUID)()}.${ext}`;
182
+ const filePath = (0, path_1.join)(dir, filename);
183
+ const buf = Buffer.from(img.data, 'base64');
184
+ (0, fs_1.writeFileSync)(filePath, new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
185
+ const relativePath = `.crewx/attachments/${threadId}/${filename}`;
186
+ return { relativePath, mimeType: img.mimeType };
187
+ });
188
+ }
144
189
  validateId(id) {
145
190
  if (!/^[\w\p{L}\p{N}._-]+$/u.test(id)) {
146
191
  throw new common_1.BadRequestException('Invalid ID format');
@@ -142,18 +142,30 @@ let TaskService = TaskService_1 = class TaskService {
142
142
  if (err instanceof common_1.HttpException)
143
143
  throw err;
144
144
  }
145
- // Send SIGTERM to the process
146
- try {
147
- process.kill(task.pid, 'SIGTERM');
148
- }
149
- catch (err) {
150
- const code = err.code;
151
- if (code === 'EPERM') {
152
- throw new common_1.HttpException({ code: 'PERMISSION_DENIED', message: '프로세스 중지 권한이 없습니다' }, 403);
145
+ // Send SIGTERM to the process group (negative pid kills detached ACP child processes too)
146
+ const pid = task.pid;
147
+ if (pid > 0) {
148
+ try {
149
+ process.kill(-pid, 'SIGTERM');
153
150
  }
154
- // ESRCH = process already gone — proceed to update DB
155
- if (code !== 'ESRCH')
156
- throw err;
151
+ catch (err) {
152
+ const code = err.code;
153
+ if (code === 'EPERM') {
154
+ throw new common_1.HttpException({ code: 'PERMISSION_DENIED', message: '프로세스 중지 권한이 없습니다' }, 403);
155
+ }
156
+ // ESRCH = process already gone — proceed to update DB
157
+ if (code !== 'ESRCH')
158
+ throw err;
159
+ }
160
+ // SIGKILL fallback: if process group is still alive after 2s, force-kill
161
+ setTimeout(() => {
162
+ try {
163
+ process.kill(-pid, 'SIGKILL');
164
+ }
165
+ catch {
166
+ // Process already gone — ignore
167
+ }
168
+ }, 2000);
157
169
  }
158
170
  // Conditionally update DB (race condition safe)
159
171
  this.taskRepo.markTaskFailed(taskId, '사용자에 의해 중지됨');
@@ -246,6 +258,7 @@ let TaskService = TaskService_1 = class TaskService {
246
258
  platform: data.platform,
247
259
  messages: messagesWithLogs,
248
260
  metadata: data.metadata,
261
+ renderedPrompt: data.rendered_prompt ?? null,
249
262
  ...tokenUsage,
250
263
  };
251
264
  }
@@ -332,6 +345,7 @@ let TaskService = TaskService_1 = class TaskService {
332
345
  durationMs: row.duration_ms ?? undefined,
333
346
  error: row.error || undefined,
334
347
  prompt: row.prompt || undefined,
348
+ renderedPrompt: row.rendered_prompt ?? null,
335
349
  result: row.result || undefined,
336
350
  model: row.model || undefined,
337
351
  threadId: row.thread_id || undefined,
@@ -519,13 +533,13 @@ let TaskService = TaskService_1 = class TaskService {
519
533
  }
520
534
  return undefined;
521
535
  }
522
- /** Merge consecutive type=text entries into one (fixes Gemini delta streaming fragmentation) */
536
+ /** Merge consecutive type=text entries into one while preserving message boundaries. */
523
537
  mergeConsecutiveText(entries) {
524
538
  const merged = [];
525
539
  for (const entry of entries) {
526
540
  const prev = merged[merged.length - 1];
527
541
  if (prev && prev.type === 'text' && entry.type === 'text') {
528
- prev.content = (prev.content || '') + (entry.content || '');
542
+ prev.content = this.joinTextEntries(prev.content || '', entry.content || '');
529
543
  }
530
544
  else {
531
545
  merged.push({ ...entry });
@@ -533,6 +547,13 @@ let TaskService = TaskService_1 = class TaskService {
533
547
  }
534
548
  return merged;
535
549
  }
550
+ joinTextEntries(previous, next) {
551
+ if (!previous)
552
+ return next;
553
+ if (!next)
554
+ return previous;
555
+ return previous + next;
556
+ }
536
557
  /**
537
558
  * When ACP streaming sends an initial tool_call with empty input followed by
538
559
  * a tool_call_update with refined input, both arrive as type=tool_use with
@@ -77,6 +77,11 @@ let ThreadController = class ThreadController {
77
77
  const data = this.threadService.getThreadTask(threadId, taskId, include, logLimit, ws.id);
78
78
  return { success: true, data };
79
79
  }
80
+ // TSK.RETRY: Retry a failed task — prepends dreaming context hint
81
+ async retryTask(threadId, taskId, ws) {
82
+ const data = await this.threadService.retryTask(threadId, taskId, ws.id);
83
+ return { success: true, data };
84
+ }
80
85
  };
81
86
  exports.ThreadController = ThreadController;
82
87
  __decorate([
@@ -166,6 +171,17 @@ __decorate([
166
171
  __metadata("design:paramtypes", [String, String, Object, String, Number]),
167
172
  __metadata("design:returntype", void 0)
168
173
  ], ThreadController.prototype, "getThreadTask", null);
174
+ __decorate([
175
+ (0, swagger_1.ApiOperation)({ operationId: 'TSK.RETRY', summary: 'Retry a failed task with dreaming context hint' }),
176
+ (0, common_1.Post)(':threadId/tasks/:taskId/retry'),
177
+ (0, common_1.HttpCode)(202),
178
+ __param(0, (0, common_1.Param)('threadId')),
179
+ __param(1, (0, common_1.Param)('taskId')),
180
+ __param(2, (0, workspace_decorator_js_1.Workspace)()),
181
+ __metadata("design:type", Function),
182
+ __metadata("design:paramtypes", [String, String, Object]),
183
+ __metadata("design:returntype", Promise)
184
+ ], ThreadController.prototype, "retryTask", null);
169
185
  exports.ThreadController = ThreadController = __decorate([
170
186
  (0, swagger_1.ApiTags)('threads'),
171
187
  (0, common_1.Controller)('ws/:slug/threads'),
@@ -11,11 +11,13 @@ const common_1 = require("@nestjs/common");
11
11
  const thread_controller_js_1 = require("./thread.controller.js");
12
12
  const thread_service_js_1 = require("./thread.service.js");
13
13
  const box_service_js_1 = require("../box/box.service.js");
14
+ const crewx_module_js_1 = require("../../modules/crewx.module.js");
14
15
  let ThreadModule = class ThreadModule {
15
16
  };
16
17
  exports.ThreadModule = ThreadModule;
17
18
  exports.ThreadModule = ThreadModule = __decorate([
18
19
  (0, common_1.Module)({
20
+ imports: [crewx_module_js_1.CrewxModule],
19
21
  controllers: [thread_controller_js_1.ThreadController],
20
22
  providers: [thread_service_js_1.ThreadService, box_service_js_1.BoxService],
21
23
  exports: [thread_service_js_1.ThreadService],
@@ -8,16 +8,22 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
8
8
  var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
+ var ThreadService_1;
11
12
  Object.defineProperty(exports, "__esModule", { value: true });
12
13
  exports.ThreadService = void 0;
13
14
  const common_1 = require("@nestjs/common");
14
15
  const tokenizer_js_1 = require("../../utils/tokenizer.js");
15
16
  const sdk_1 = require("@crewx/sdk");
16
17
  const thread_repository_js_1 = require("../../repository/thread.repository.js");
17
- let ThreadService = class ThreadService {
18
+ const crewx_pool_service_js_1 = require("../../modules/crewx-pool.service.js");
19
+ const workspace_context_store_js_1 = require("../../common/workspace-context.store.js");
20
+ let ThreadService = ThreadService_1 = class ThreadService {
18
21
  threadRepo;
19
- constructor(threadRepo) {
22
+ crewxPool;
23
+ logger = new common_1.Logger(ThreadService_1.name);
24
+ constructor(threadRepo, crewxPool) {
20
25
  this.threadRepo = threadRepo;
26
+ this.crewxPool = crewxPool;
21
27
  }
22
28
  // THD.LIST: List threads from threads table (with optional q search)
23
29
  listThreads(params, workspaceId) {
@@ -189,11 +195,11 @@ let ThreadService = class ThreadService {
189
195
  callerAgentId: row.caller_agent_id || undefined,
190
196
  };
191
197
  if (wantLogs) {
192
- result.logs = this.parseLogs(row.logs, limit);
198
+ result.logs = this.parseLogs(row.logs, limit, this.extractAdapterKey(row));
193
199
  }
194
200
  if (wantChildren && children) {
195
201
  result.childTasks = children.map((child) => {
196
- const logs = this.parseLogs(child.logs, 1);
202
+ const logs = this.parseLogs(child.logs, 1, this.extractAdapterKey(child));
197
203
  return {
198
204
  taskId: child.id,
199
205
  agentId: child.agent_id || '',
@@ -215,6 +221,42 @@ let ThreadService = class ThreadService {
215
221
  return (0, sdk_1.buildContext)(threadId, this, // ThreadService satisfies ITaskReader
216
222
  boxService, params, tiktokenTokenizer);
217
223
  }
224
+ // TSK.RETRY: Retry a failed task with dreaming context hint prepended
225
+ async retryTask(threadId, taskId, workspaceId) {
226
+ this.validateId(threadId);
227
+ this.validateId(taskId);
228
+ const found = this.threadRepo.findTaskById(threadId, taskId, workspaceId);
229
+ if (!found) {
230
+ throw new common_1.NotFoundException(`Task not found: ${taskId} in thread: ${threadId}`);
231
+ }
232
+ const { task } = found;
233
+ if (task.status !== 'failed') {
234
+ throw new common_1.BadRequestException(`Task ${taskId} is not in failed state (current: ${task.status}). Only failed tasks can be retried.`);
235
+ }
236
+ const dreamingPrefix = `<crewx_system_message>\n` +
237
+ `Retrying a failed task.\n` +
238
+ `You must run \`crewx dreaming --task=${taskId}\` to review where it stopped before continuing.\n` +
239
+ `</crewx_system_message>\n\n`;
240
+ const retryPrompt = `${dreamingPrefix}${task.prompt ?? ''}`;
241
+ const newTaskId = (0, sdk_1.generateId)('tsk');
242
+ const agentRef = `@${task.agent_id}`;
243
+ const mode = task.mode ?? 'execute';
244
+ const crewx = await this.crewxPool.getForCurrentRequest();
245
+ const sdkOptions = {
246
+ threadId,
247
+ taskId: newTaskId,
248
+ platform: 'cli',
249
+ metadata: { thread: threadId, parentTaskId: null, retriedFromTaskId: taskId },
250
+ cwd: (0, workspace_context_store_js_1.getWorkspacePath)(),
251
+ };
252
+ const sdkCall = mode === 'execute'
253
+ ? crewx.execute(agentRef, retryPrompt, sdkOptions)
254
+ : crewx.query(agentRef, retryPrompt, sdkOptions);
255
+ sdkCall.catch((err) => {
256
+ this.logger.error(`Retry task failed [thread=${threadId}, originalTask=${taskId}, newTask=${newTaskId}]: ${err.message}`, err.stack);
257
+ });
258
+ return { taskId: newTaskId, status: 'pending', agentId: task.agent_id };
259
+ }
218
260
  // --- Private helpers ---
219
261
  // Reconstruct messages from tasks (used by buildContext and MessageService)
220
262
  getThreadMessages(threadId, workspaceId) {
@@ -281,7 +323,7 @@ let ThreadService = class ThreadService {
281
323
  totalCostUsd,
282
324
  sessionId,
283
325
  error: task.error || null,
284
- logs: this.parseLogs(task.logs, 50),
326
+ logs: this.parseLogs(task.logs, 50, this.extractAdapterKey(task)),
285
327
  },
286
328
  },
287
329
  });
@@ -350,7 +392,7 @@ let ThreadService = class ThreadService {
350
392
  return text.slice(0, maxLen) + '...';
351
393
  }
352
394
  /** @see TaskService.parseLogs — 동일 역할, ACP 파싱 로직 동기화 필요 */
353
- parseLogs(logsJson, logLimit) {
395
+ parseLogs(logsJson, logLimit, providerId) {
354
396
  if (!logsJson)
355
397
  return [];
356
398
  let rawLogs;
@@ -366,7 +408,7 @@ let ThreadService = class ThreadService {
366
408
  for (const log of rawLogs) {
367
409
  if (log.level === 'stdout') {
368
410
  try {
369
- const parsed = (0, sdk_1.parseStdoutEvent)(log.timestamp, log.message);
411
+ const parsed = (0, sdk_1.parseStdoutEvent)(log.timestamp, log.message, providerId);
370
412
  if (parsed.length > 0) {
371
413
  entries.push(...parsed);
372
414
  continue;
@@ -387,13 +429,13 @@ let ThreadService = class ThreadService {
387
429
  }
388
430
  return deduped;
389
431
  }
390
- /** Merge consecutive type=text entries into one (fixes Gemini delta streaming fragmentation) */
432
+ /** Merge consecutive type=text entries into one while preserving message boundaries. */
391
433
  mergeConsecutiveText(entries) {
392
434
  const merged = [];
393
435
  for (const entry of entries) {
394
436
  const prev = merged[merged.length - 1];
395
437
  if (prev && prev.type === 'text' && entry.type === 'text') {
396
- prev.content = (prev.content || '') + (entry.content || '');
438
+ prev.content = this.joinTextEntries(prev.content || '', entry.content || '');
397
439
  }
398
440
  else {
399
441
  merged.push({ ...entry });
@@ -401,6 +443,32 @@ let ThreadService = class ThreadService {
401
443
  }
402
444
  return merged;
403
445
  }
446
+ joinTextEntries(previous, next) {
447
+ if (!previous)
448
+ return next;
449
+ if (!next)
450
+ return previous;
451
+ return previous + next;
452
+ }
453
+ extractAdapterKey(row) {
454
+ if (row.metadata) {
455
+ try {
456
+ const meta = JSON.parse(row.metadata);
457
+ const provider = meta.provider;
458
+ if (provider) {
459
+ const slash = provider.lastIndexOf('/');
460
+ return slash >= 0 ? provider.slice(slash + 1) : provider;
461
+ }
462
+ }
463
+ catch {
464
+ // Ignore malformed task metadata.
465
+ }
466
+ }
467
+ if (row.coding_agent_command) {
468
+ return row.coding_agent_command.split(/\s/)[0] || undefined;
469
+ }
470
+ return undefined;
471
+ }
404
472
  /**
405
473
  * ACP providers (claude, opencode) emit the same toolUseId twice — first with
406
474
  * a placeholder input, then with the real one. Keep only the last occurrence.
@@ -422,7 +490,8 @@ let ThreadService = class ThreadService {
422
490
  }
423
491
  };
424
492
  exports.ThreadService = ThreadService;
425
- exports.ThreadService = ThreadService = __decorate([
493
+ exports.ThreadService = ThreadService = ThreadService_1 = __decorate([
426
494
  (0, common_1.Injectable)(),
427
- __metadata("design:paramtypes", [thread_repository_js_1.ThreadRepository])
495
+ __metadata("design:paramtypes", [thread_repository_js_1.ThreadRepository,
496
+ crewx_pool_service_js_1.CrewxPoolService])
428
497
  ], ThreadService);