lazy-gravity 0.2.0 → 0.3.0

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 (56) hide show
  1. package/README.md +76 -15
  2. package/dist/bin/commands/doctor.js +19 -2
  3. package/dist/bin/commands/setup.js +286 -70
  4. package/dist/bot/eventRouter.js +70 -0
  5. package/dist/bot/index.js +353 -147
  6. package/dist/bot/telegramCommands.js +428 -0
  7. package/dist/bot/telegramMessageHandler.js +304 -0
  8. package/dist/bot/telegramProjectCommand.js +137 -0
  9. package/dist/bot/workspaceQueue.js +61 -0
  10. package/dist/commands/joinCommandHandler.js +4 -1
  11. package/dist/database/telegramBindingRepository.js +97 -0
  12. package/dist/database/userPreferenceRepository.js +46 -1
  13. package/dist/events/interactionCreateHandler.js +36 -0
  14. package/dist/events/messageCreateHandler.js +11 -7
  15. package/dist/handlers/approvalButtonAction.js +99 -0
  16. package/dist/handlers/autoAcceptButtonAction.js +43 -0
  17. package/dist/handlers/buttonHandler.js +55 -0
  18. package/dist/handlers/commandHandler.js +44 -0
  19. package/dist/handlers/errorPopupButtonAction.js +137 -0
  20. package/dist/handlers/messageHandler.js +70 -0
  21. package/dist/handlers/modeSelectAction.js +63 -0
  22. package/dist/handlers/modelButtonAction.js +102 -0
  23. package/dist/handlers/planningButtonAction.js +118 -0
  24. package/dist/handlers/selectHandler.js +41 -0
  25. package/dist/handlers/templateButtonAction.js +54 -0
  26. package/dist/platform/adapter.js +8 -0
  27. package/dist/platform/discord/discordAdapter.js +99 -0
  28. package/dist/platform/discord/index.js +15 -0
  29. package/dist/platform/discord/wrappers.js +331 -0
  30. package/dist/platform/index.js +18 -0
  31. package/dist/platform/richContentBuilder.js +76 -0
  32. package/dist/platform/telegram/index.js +16 -0
  33. package/dist/platform/telegram/telegramAdapter.js +195 -0
  34. package/dist/platform/telegram/telegramFormatter.js +134 -0
  35. package/dist/platform/telegram/wrappers.js +329 -0
  36. package/dist/platform/types.js +28 -0
  37. package/dist/services/approvalDetector.js +15 -2
  38. package/dist/services/cdpBridgeManager.js +91 -146
  39. package/dist/services/defaultModelApplicator.js +54 -0
  40. package/dist/services/modeService.js +16 -1
  41. package/dist/services/modelService.js +57 -16
  42. package/dist/services/notificationSender.js +149 -0
  43. package/dist/services/responseMonitor.js +1 -2
  44. package/dist/ui/autoAcceptUi.js +37 -0
  45. package/dist/ui/modeUi.js +38 -1
  46. package/dist/ui/modelsUi.js +96 -0
  47. package/dist/ui/outputUi.js +32 -0
  48. package/dist/ui/projectListUi.js +55 -0
  49. package/dist/ui/screenshotUi.js +26 -0
  50. package/dist/ui/sessionPickerUi.js +35 -1
  51. package/dist/ui/templateUi.js +41 -0
  52. package/dist/utils/configLoader.js +63 -12
  53. package/dist/utils/lockfile.js +5 -5
  54. package/dist/utils/logger.js +7 -0
  55. package/dist/utils/telegramImageHandler.js +127 -0
  56. package/package.json +4 -2
@@ -12,7 +12,7 @@ class UserPreferenceRepository {
12
12
  this.initialize();
13
13
  }
14
14
  /**
15
- * Initialize table (create if not exists)
15
+ * Initialize table (create if not exists) and run migrations
16
16
  */
17
17
  initialize() {
18
18
  this.db.exec(`
@@ -24,6 +24,29 @@ class UserPreferenceRepository {
24
24
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
25
25
  )
26
26
  `);
27
+ this.migrateDefaultModel();
28
+ }
29
+ /**
30
+ * Safe migration: add default_model column if it does not exist.
31
+ * Uses pragma when available, falls back to try/catch ALTER TABLE.
32
+ */
33
+ migrateDefaultModel() {
34
+ if (typeof this.db.pragma === 'function') {
35
+ const columns = this.db.pragma('table_info(user_preferences)');
36
+ const hasColumn = columns.some(c => c.name === 'default_model');
37
+ if (!hasColumn) {
38
+ this.db.exec('ALTER TABLE user_preferences ADD COLUMN default_model TEXT DEFAULT NULL');
39
+ }
40
+ }
41
+ else {
42
+ // Fallback for mock/alternate DB implementations without pragma
43
+ try {
44
+ this.db.exec('ALTER TABLE user_preferences ADD COLUMN default_model TEXT DEFAULT NULL');
45
+ }
46
+ catch {
47
+ // Column already exists — safe to ignore
48
+ }
49
+ }
27
50
  }
28
51
  /**
29
52
  * Get the output format preference for a user.
@@ -47,6 +70,27 @@ class UserPreferenceRepository {
47
70
  updated_at = datetime('now')
48
71
  `).run(userId, format);
49
72
  }
73
+ /**
74
+ * Get the default model for a user.
75
+ * Returns null if no default is stored.
76
+ */
77
+ getDefaultModel(userId) {
78
+ const row = this.db.prepare('SELECT default_model FROM user_preferences WHERE user_id = ?').get(userId);
79
+ return row?.default_model ?? null;
80
+ }
81
+ /**
82
+ * Set the default model for a user (upsert).
83
+ * Pass null to clear the default.
84
+ */
85
+ setDefaultModel(userId, modelName) {
86
+ this.db.prepare(`
87
+ INSERT INTO user_preferences (user_id, default_model)
88
+ VALUES (?, ?)
89
+ ON CONFLICT(user_id)
90
+ DO UPDATE SET default_model = excluded.default_model,
91
+ updated_at = datetime('now')
92
+ `).run(userId, modelName);
93
+ }
50
94
  /**
51
95
  * Get full preference record for a user
52
96
  */
@@ -64,6 +108,7 @@ class UserPreferenceRepository {
64
108
  id: row.id,
65
109
  userId: row.user_id,
66
110
  outputFormat: row.output_format,
111
+ defaultModel: row.default_model ?? null,
67
112
  createdAt: row.created_at,
68
113
  updatedAt: row.updated_at,
69
114
  };
@@ -378,6 +378,42 @@ function createInteractionCreateHandler(deps) {
378
378
  await deps.cleanupHandler.handleCancel(interaction);
379
379
  return;
380
380
  }
381
+ if (interaction.customId === 'model_set_default_btn') {
382
+ await interaction.deferUpdate();
383
+ const cdp = deps.getCurrentCdp(deps.bridge);
384
+ if (!cdp) {
385
+ await interaction.followUp({ content: 'Not connected to CDP.', flags: discord_js_1.MessageFlags.Ephemeral });
386
+ return;
387
+ }
388
+ const currentModel = await cdp.getCurrentModel();
389
+ if (!currentModel) {
390
+ await interaction.followUp({ content: 'No current model detected.', flags: discord_js_1.MessageFlags.Ephemeral });
391
+ return;
392
+ }
393
+ deps.modelService.setDefaultModel(currentModel);
394
+ if (deps.userPrefRepo) {
395
+ deps.userPrefRepo.setDefaultModel(interaction.user.id, currentModel);
396
+ }
397
+ await deps.sendModelsUI({ editReply: async (data) => await interaction.editReply(data) }, {
398
+ getCurrentCdp: () => deps.getCurrentCdp(deps.bridge),
399
+ fetchQuota: async () => deps.bridge.quota.fetchQuota(),
400
+ });
401
+ await interaction.followUp({ content: `Default model set to **${currentModel}**.`, flags: discord_js_1.MessageFlags.Ephemeral });
402
+ return;
403
+ }
404
+ if (interaction.customId === 'model_clear_default_btn') {
405
+ await interaction.deferUpdate();
406
+ deps.modelService.setDefaultModel(null);
407
+ if (deps.userPrefRepo) {
408
+ deps.userPrefRepo.setDefaultModel(interaction.user.id, null);
409
+ }
410
+ await deps.sendModelsUI({ editReply: async (data) => await interaction.editReply(data) }, {
411
+ getCurrentCdp: () => deps.getCurrentCdp(deps.bridge),
412
+ fetchQuota: async () => deps.bridge.quota.fetchQuota(),
413
+ });
414
+ await interaction.followUp({ content: 'Default model cleared.', flags: discord_js_1.MessageFlags.Ephemeral });
415
+ return;
416
+ }
381
417
  if (interaction.customId === 'model_refresh_btn') {
382
418
  await interaction.deferUpdate();
383
419
  await deps.sendModelsUI({ editReply: async (data) => await interaction.editReply(data) }, {
@@ -4,6 +4,7 @@ exports.createMessageCreateHandler = createMessageCreateHandler;
4
4
  const discord_js_1 = require("discord.js");
5
5
  const messageParser_1 = require("../commands/messageParser");
6
6
  const plainTextFormatter_1 = require("../utils/plainTextFormatter");
7
+ const wrappers_1 = require("../platform/discord/wrappers");
7
8
  const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
8
9
  const modeService_1 = require("../services/modeService");
9
10
  const imageHandler_1 = require("../utils/imageHandler");
@@ -115,6 +116,7 @@ function createMessageCreateHandler(deps) {
115
116
  channelManager: deps.channelManager,
116
117
  titleGenerator: deps.titleGenerator,
117
118
  userPrefRepo: deps.userPrefRepo,
119
+ extractionMode: deps.config.extractionMode,
118
120
  });
119
121
  }
120
122
  else {
@@ -162,14 +164,15 @@ function createMessageCreateHandler(deps) {
162
164
  const cdp = await deps.bridge.pool.getOrConnect(workspacePath);
163
165
  const projectName = deps.bridge.pool.extractProjectName(workspacePath);
164
166
  deps.bridge.lastActiveWorkspace = projectName;
165
- deps.bridge.lastActiveChannel = message.channel;
166
- registerApprovalWorkspaceChannel(deps.bridge, projectName, message.channel);
167
- ensureApprovalDetector(deps.bridge, cdp, projectName, deps.client);
168
- ensureErrorPopupDetector(deps.bridge, cdp, projectName, deps.client);
169
- ensurePlanningDetector(deps.bridge, cdp, projectName, deps.client);
167
+ const platformChannel = (0, wrappers_1.wrapDiscordChannel)(message.channel);
168
+ deps.bridge.lastActiveChannel = platformChannel;
169
+ registerApprovalWorkspaceChannel(deps.bridge, projectName, platformChannel);
170
+ ensureApprovalDetector(deps.bridge, cdp, projectName);
171
+ ensureErrorPopupDetector(deps.bridge, cdp, projectName);
172
+ ensurePlanningDetector(deps.bridge, cdp, projectName);
170
173
  const session = deps.chatSessionRepo.findByChannelId(message.channelId);
171
174
  if (session?.displayName) {
172
- registerApprovalSessionChannel(deps.bridge, projectName, session.displayName, message.channel);
175
+ registerApprovalSessionChannel(deps.bridge, projectName, session.displayName, platformChannel);
173
176
  }
174
177
  if (session?.isRenamed && session.displayName) {
175
178
  const activationResult = await deps.chatSessionService.activateSessionByTitle(cdp, session.displayName);
@@ -197,7 +200,7 @@ function createMessageCreateHandler(deps) {
197
200
  // Re-register session channel after autoRenameChannel sets displayName
198
201
  const updatedSession = deps.chatSessionRepo.findByChannelId(message.channelId);
199
202
  if (updatedSession?.displayName) {
200
- registerApprovalSessionChannel(deps.bridge, projectName, updatedSession.displayName, message.channel);
203
+ registerApprovalSessionChannel(deps.bridge, projectName, updatedSession.displayName, platformChannel);
201
204
  }
202
205
  // Register echo hash so UserMessageDetector skips this message
203
206
  const userMsgDetector = deps.bridge.pool.getUserMessageDetector?.(projectName);
@@ -232,6 +235,7 @@ function createMessageCreateHandler(deps) {
232
235
  channelManager: deps.channelManager,
233
236
  titleGenerator: deps.titleGenerator,
234
237
  userPrefRepo: deps.userPrefRepo,
238
+ extractionMode: deps.config.extractionMode,
235
239
  onFullCompletion: settle,
236
240
  }).catch((err) => {
237
241
  // sendPromptToAntigravity rejected before onFullCompletion fired
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ /**
3
+ * Platform-agnostic approval button action.
4
+ *
5
+ * Handles Allow / Always Allow / Deny button presses from both Discord
6
+ * and Telegram using the ButtonAction interface.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.createApprovalButtonAction = createApprovalButtonAction;
10
+ const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
11
+ const logger_1 = require("../utils/logger");
12
+ function createApprovalButtonAction(deps) {
13
+ return {
14
+ match(customId) {
15
+ const parsed = (0, cdpBridgeManager_1.parseApprovalCustomId)(customId);
16
+ if (!parsed)
17
+ return null;
18
+ return {
19
+ action: parsed.action,
20
+ projectName: parsed.projectName ?? '',
21
+ channelId: parsed.channelId ?? '',
22
+ };
23
+ },
24
+ async execute(interaction, params) {
25
+ const { action, channelId } = params;
26
+ // Acknowledge immediately so Telegram doesn't time out
27
+ await interaction.deferUpdate().catch(() => { });
28
+ // Channel scope check (skip if no channelId was encoded)
29
+ if (channelId && channelId !== interaction.channel.id) {
30
+ await interaction
31
+ .reply({ text: 'This approval action is linked to a different session channel.' })
32
+ .catch(() => { });
33
+ return;
34
+ }
35
+ const projectName = params.projectName || deps.bridge.lastActiveWorkspace;
36
+ logger_1.logger.debug(`[ApprovalAction] action=${action} project=${projectName ?? 'null'} channel=${interaction.channel.id}`);
37
+ const detector = projectName
38
+ ? deps.bridge.pool.getApprovalDetector(projectName)
39
+ : undefined;
40
+ if (!detector) {
41
+ logger_1.logger.warn(`[ApprovalAction] No detector for project=${projectName}`);
42
+ await interaction
43
+ .reply({ text: 'Approval detector not found.' })
44
+ .catch(() => { });
45
+ return;
46
+ }
47
+ const lastInfo = detector.getLastDetectedInfo();
48
+ logger_1.logger.debug(`[ApprovalAction] lastDetectedInfo: ${lastInfo ? JSON.stringify(lastInfo) : 'null'}`);
49
+ let success = false;
50
+ let actionLabel = '';
51
+ try {
52
+ if (action === 'approve') {
53
+ success = await detector.approveButton();
54
+ actionLabel = 'Allow';
55
+ }
56
+ else if (action === 'always_allow') {
57
+ success = await detector.alwaysAllowButton();
58
+ actionLabel = 'Allow Chat';
59
+ }
60
+ else {
61
+ success = await detector.denyButton();
62
+ actionLabel = 'Deny';
63
+ }
64
+ }
65
+ catch (err) {
66
+ const msg = err instanceof Error ? err.message : String(err);
67
+ logger_1.logger.error(`[ApprovalAction] CDP click failed: ${msg}`);
68
+ await interaction
69
+ .reply({ text: `Approval failed: ${msg}` })
70
+ .catch(() => { });
71
+ return;
72
+ }
73
+ logger_1.logger.debug(`[ApprovalAction] ${actionLabel} result: ${success}`);
74
+ if (success) {
75
+ // Remove buttons by editing the original message.
76
+ // If update() fails, fall back to editReply(), then followUp().
77
+ const updatePayload = { text: `✅ ${actionLabel} completed`, components: [] };
78
+ try {
79
+ await interaction.update(updatePayload);
80
+ }
81
+ catch (updateErr) {
82
+ logger_1.logger.warn('[ApprovalAction] update failed, trying editReply:', updateErr);
83
+ try {
84
+ await interaction.editReply(updatePayload);
85
+ }
86
+ catch (editErr) {
87
+ logger_1.logger.warn('[ApprovalAction] editReply failed, sending followUp:', editErr);
88
+ await interaction.followUp({ text: `✅ ${actionLabel} completed` }).catch(() => { });
89
+ }
90
+ }
91
+ }
92
+ else {
93
+ await interaction
94
+ .reply({ text: 'Approval button not found.' })
95
+ .catch(() => { });
96
+ }
97
+ },
98
+ };
99
+ }
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ /**
3
+ * Platform-agnostic ButtonAction for auto-accept toggle interactions.
4
+ *
5
+ * Handles:
6
+ * autoaccept_btn_on — Enable auto-accept
7
+ * autoaccept_btn_off — Disable auto-accept
8
+ * autoaccept_btn_refresh — Refresh the auto-accept UI
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.createAutoAcceptButtonAction = createAutoAcceptButtonAction;
12
+ const autoAcceptUi_1 = require("../ui/autoAcceptUi");
13
+ function createAutoAcceptButtonAction(deps) {
14
+ return {
15
+ match(customId) {
16
+ if (customId === autoAcceptUi_1.AUTOACCEPT_BTN_ON)
17
+ return { action: 'on' };
18
+ if (customId === autoAcceptUi_1.AUTOACCEPT_BTN_OFF)
19
+ return { action: 'off' };
20
+ if (customId === autoAcceptUi_1.AUTOACCEPT_BTN_REFRESH)
21
+ return { action: 'refresh' };
22
+ return null;
23
+ },
24
+ async execute(interaction, params) {
25
+ await interaction.deferUpdate();
26
+ if (params.action === 'on' || params.action === 'off') {
27
+ const result = deps.autoAcceptService.handle(params.action);
28
+ // Only update UI if the state actually changed to avoid
29
+ // Telegram "message is not modified" error.
30
+ if (result.changed) {
31
+ const payload = (0, autoAcceptUi_1.buildAutoAcceptPayload)(deps.autoAcceptService.isEnabled());
32
+ await interaction.update(payload);
33
+ }
34
+ await interaction.followUp({ text: result.message }).catch(() => { });
35
+ }
36
+ else {
37
+ // refresh — always update to show latest state
38
+ const payload = (0, autoAcceptUi_1.buildAutoAcceptPayload)(deps.autoAcceptService.isEnabled());
39
+ await interaction.update(payload);
40
+ }
41
+ },
42
+ };
43
+ }
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ /**
3
+ * Platform-agnostic button interaction handler.
4
+ *
5
+ * Uses a registry pattern: each button type registers a match+execute pair.
6
+ * The first matching action wins (order matters).
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.createPlatformButtonHandler = createPlatformButtonHandler;
10
+ const logger_1 = require("../utils/logger");
11
+ // ---------------------------------------------------------------------------
12
+ // Factory
13
+ // ---------------------------------------------------------------------------
14
+ /**
15
+ * Create a platform-agnostic button interaction handler.
16
+ * Returns an async function that processes PlatformButtonInteraction events.
17
+ */
18
+ function createPlatformButtonHandler(deps) {
19
+ return async (interaction) => {
20
+ for (const action of deps.actions) {
21
+ let params;
22
+ try {
23
+ params = action.match(interaction.customId);
24
+ }
25
+ catch (err) {
26
+ const errorMessage = err instanceof Error ? err.message : String(err);
27
+ logger_1.logger.error('[ButtonHandler] Match error:', errorMessage);
28
+ await interaction
29
+ .reply({
30
+ text: 'An error occurred while processing the button action.',
31
+ ephemeral: true,
32
+ })
33
+ .catch(() => { });
34
+ return;
35
+ }
36
+ if (params !== null) {
37
+ try {
38
+ await action.execute(interaction, params);
39
+ }
40
+ catch (err) {
41
+ const errorMessage = err instanceof Error ? err.message : String(err);
42
+ logger_1.logger.error('[ButtonHandler] Action error:', errorMessage);
43
+ await interaction
44
+ .reply({
45
+ text: 'An error occurred while processing the button action.',
46
+ ephemeral: true,
47
+ })
48
+ .catch(() => { });
49
+ }
50
+ return;
51
+ }
52
+ }
53
+ logger_1.logger.warn(`[ButtonHandler] No handler for customId: ${interaction.customId}`);
54
+ };
55
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ /**
3
+ * Platform-agnostic slash command handler.
4
+ *
5
+ * Maintains a lookup map from command name to CommandDef for O(1) dispatch.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.createPlatformCommandHandler = createPlatformCommandHandler;
9
+ const logger_1 = require("../utils/logger");
10
+ // ---------------------------------------------------------------------------
11
+ // Factory
12
+ // ---------------------------------------------------------------------------
13
+ /**
14
+ * Create a platform-agnostic slash command handler.
15
+ * Returns an async function that processes PlatformCommandInteraction events.
16
+ */
17
+ function createPlatformCommandHandler(deps) {
18
+ const commandMap = new Map();
19
+ for (const cmd of deps.commands) {
20
+ commandMap.set(cmd.name, cmd);
21
+ }
22
+ return async (interaction) => {
23
+ const cmd = commandMap.get(interaction.commandName);
24
+ if (!cmd) {
25
+ logger_1.logger.warn(`[CommandHandler] Unknown command: ${interaction.commandName}`);
26
+ await interaction.editReply({
27
+ text: `Unknown command: ${interaction.commandName}`,
28
+ }).catch(() => { });
29
+ return;
30
+ }
31
+ try {
32
+ await cmd.execute(interaction);
33
+ }
34
+ catch (err) {
35
+ const errorMessage = err instanceof Error ? err.message : String(err);
36
+ logger_1.logger.error(`[CommandHandler] Command "${interaction.commandName}" error:`, errorMessage);
37
+ await interaction
38
+ .editReply({
39
+ text: 'An error occurred while processing the command.',
40
+ })
41
+ .catch(() => { });
42
+ }
43
+ };
44
+ }
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ /**
3
+ * Platform-agnostic error popup button action.
4
+ *
5
+ * Handles Dismiss / Copy Debug / Retry button presses for the error
6
+ * popup dialog from both Discord and Telegram using the ButtonAction interface.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.createErrorPopupButtonAction = createErrorPopupButtonAction;
10
+ const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
11
+ const logger_1 = require("../utils/logger");
12
+ const MAX_DEBUG_CONTENT = 4096;
13
+ function createErrorPopupButtonAction(deps) {
14
+ return {
15
+ match(customId) {
16
+ const parsed = (0, cdpBridgeManager_1.parseErrorPopupCustomId)(customId);
17
+ if (!parsed)
18
+ return null;
19
+ return {
20
+ action: parsed.action,
21
+ projectName: parsed.projectName ?? '',
22
+ channelId: parsed.channelId ?? '',
23
+ };
24
+ },
25
+ async execute(interaction, params) {
26
+ const { action, channelId } = params;
27
+ if (channelId && channelId !== interaction.channel.id) {
28
+ await interaction
29
+ .reply({ text: 'This error popup action is linked to a different session channel.' })
30
+ .catch(() => { });
31
+ return;
32
+ }
33
+ const projectName = params.projectName || deps.bridge.lastActiveWorkspace;
34
+ const detector = projectName
35
+ ? deps.bridge.pool.getErrorPopupDetector(projectName)
36
+ : undefined;
37
+ if (!detector) {
38
+ await interaction
39
+ .reply({ text: 'Error popup detector not found.' })
40
+ .catch(() => { });
41
+ return;
42
+ }
43
+ // Acknowledge immediately so Telegram doesn't time out
44
+ await interaction.deferUpdate().catch(() => { });
45
+ if (action === 'dismiss') {
46
+ let clicked = false;
47
+ try {
48
+ clicked = await detector.clickDismissButton();
49
+ }
50
+ catch (err) {
51
+ const msg = err instanceof Error ? err.message : String(err);
52
+ logger_1.logger.error(`[ErrorPopupAction] CDP click failed: ${msg}`);
53
+ await interaction.reply({ text: `Dismiss failed: ${msg}` }).catch(() => { });
54
+ return;
55
+ }
56
+ if (clicked) {
57
+ await interaction
58
+ .update({
59
+ text: '🗑️ Dismissed',
60
+ components: [],
61
+ })
62
+ .catch((err) => {
63
+ logger_1.logger.warn('[ErrorPopupAction] update failed:', err);
64
+ });
65
+ }
66
+ else {
67
+ await interaction
68
+ .reply({ text: 'Dismiss button not found.' })
69
+ .catch(() => { });
70
+ }
71
+ }
72
+ else if (action === 'copy_debug') {
73
+ const clicked = await detector.clickCopyDebugInfoButton();
74
+ if (!clicked) {
75
+ await interaction
76
+ .reply({ text: 'Copy debug info button not found.' })
77
+ .catch(() => { });
78
+ return;
79
+ }
80
+ // Wait for clipboard to be populated
81
+ await new Promise((resolve) => setTimeout(resolve, 300));
82
+ const clipboardContent = await detector.readClipboard();
83
+ await interaction
84
+ .update({
85
+ text: '📋 Debug info copied',
86
+ components: [],
87
+ })
88
+ .catch((err) => {
89
+ logger_1.logger.warn('[ErrorPopupAction] update failed:', err);
90
+ });
91
+ if (clipboardContent) {
92
+ const truncated = clipboardContent.length > MAX_DEBUG_CONTENT
93
+ ? clipboardContent.substring(0, MAX_DEBUG_CONTENT - 15) + '\n\n(truncated)'
94
+ : clipboardContent;
95
+ await interaction
96
+ .followUp({ text: truncated })
97
+ .catch((err) => {
98
+ logger_1.logger.warn('[ErrorPopupAction] followUp failed:', err);
99
+ });
100
+ }
101
+ else {
102
+ await interaction
103
+ .followUp({ text: 'Could not read debug info from clipboard.' })
104
+ .catch(() => { });
105
+ }
106
+ }
107
+ else {
108
+ // Retry action
109
+ let clicked = false;
110
+ try {
111
+ clicked = await detector.clickRetryButton();
112
+ }
113
+ catch (err) {
114
+ const msg = err instanceof Error ? err.message : String(err);
115
+ logger_1.logger.error(`[ErrorPopupAction] CDP click failed: ${msg}`);
116
+ await interaction.reply({ text: `Retry failed: ${msg}` }).catch(() => { });
117
+ return;
118
+ }
119
+ if (clicked) {
120
+ await interaction
121
+ .update({
122
+ text: '🔄 Retry initiated',
123
+ components: [],
124
+ })
125
+ .catch((err) => {
126
+ logger_1.logger.warn('[ErrorPopupAction] update failed:', err);
127
+ });
128
+ }
129
+ else {
130
+ await interaction
131
+ .reply({ text: 'Retry button not found.' })
132
+ .catch(() => { });
133
+ }
134
+ }
135
+ },
136
+ };
137
+ }
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ /**
3
+ * Platform-agnostic message handler.
4
+ *
5
+ * Extracts core message-handling logic from Discord-specific event handlers
6
+ * into a platform-independent factory that works with any PlatformMessage.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.createPlatformMessageHandler = createPlatformMessageHandler;
10
+ const logger_1 = require("../utils/logger");
11
+ // ---------------------------------------------------------------------------
12
+ // Factory
13
+ // ---------------------------------------------------------------------------
14
+ /**
15
+ * Create a platform-agnostic message handler.
16
+ * Returns an async function that processes PlatformMessage events.
17
+ */
18
+ function createPlatformMessageHandler(deps) {
19
+ return async (message) => {
20
+ // Skip bot messages
21
+ if (message.author.isBot)
22
+ return;
23
+ const content = message.content.trim();
24
+ if (!content && message.attachments.length === 0)
25
+ return;
26
+ // Check for text commands (prefixed with !)
27
+ if (content.startsWith('!')) {
28
+ const parts = content.slice(1).split(/\s+/);
29
+ const commandName = parts[0]?.toLowerCase();
30
+ const args = parts.slice(1);
31
+ if (commandName && deps.handleTextCommand) {
32
+ try {
33
+ const handled = await deps.handleTextCommand(message, commandName, args);
34
+ if (handled)
35
+ return;
36
+ }
37
+ catch (err) {
38
+ const errorMessage = err instanceof Error ? err.message : String(err);
39
+ logger_1.logger.error('[MessageHandler] Text command error:', errorMessage);
40
+ await message
41
+ .reply({
42
+ text: 'An error occurred while processing the command.',
43
+ })
44
+ .catch(() => { });
45
+ return;
46
+ }
47
+ }
48
+ }
49
+ // Route to workspace
50
+ const workspacePath = deps.getWorkspaceForChannel(message.channel.id);
51
+ if (!workspacePath) {
52
+ await message.reply({
53
+ text: 'No project is configured for this channel. Please create or select one with `/project`.',
54
+ });
55
+ return;
56
+ }
57
+ const promptText = content ||
58
+ 'Please review the attached images and respond accordingly.';
59
+ try {
60
+ await deps.sendPrompt(message, workspacePath, promptText);
61
+ }
62
+ catch (err) {
63
+ const errorMessage = err instanceof Error ? err.message : String(err);
64
+ logger_1.logger.error('[MessageHandler] Failed to send prompt:', errorMessage);
65
+ await message
66
+ .reply({ text: 'An error occurred while processing your message.' })
67
+ .catch(() => { });
68
+ }
69
+ };
70
+ }