lazy-gravity 0.2.0 → 0.4.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 (66) hide show
  1. package/README.md +77 -15
  2. package/dist/bin/cli.js +0 -0
  3. package/dist/bin/commands/doctor.js +19 -2
  4. package/dist/bin/commands/open.js +1 -1
  5. package/dist/bin/commands/setup.js +286 -70
  6. package/dist/bot/eventRouter.js +70 -0
  7. package/dist/bot/index.js +355 -147
  8. package/dist/bot/telegramCommands.js +478 -0
  9. package/dist/bot/telegramMessageHandler.js +308 -0
  10. package/dist/bot/telegramProjectCommand.js +137 -0
  11. package/dist/bot/workspaceQueue.js +61 -0
  12. package/dist/commands/joinCommandHandler.js +4 -1
  13. package/dist/database/telegramBindingRepository.js +97 -0
  14. package/dist/database/userPreferenceRepository.js +46 -1
  15. package/dist/events/interactionCreateHandler.js +36 -0
  16. package/dist/events/messageCreateHandler.js +11 -7
  17. package/dist/handlers/approvalButtonAction.js +99 -0
  18. package/dist/handlers/autoAcceptButtonAction.js +43 -0
  19. package/dist/handlers/buttonHandler.js +55 -0
  20. package/dist/handlers/commandHandler.js +44 -0
  21. package/dist/handlers/errorPopupButtonAction.js +137 -0
  22. package/dist/handlers/messageHandler.js +70 -0
  23. package/dist/handlers/modeSelectAction.js +63 -0
  24. package/dist/handlers/modelButtonAction.js +102 -0
  25. package/dist/handlers/planningButtonAction.js +118 -0
  26. package/dist/handlers/selectHandler.js +41 -0
  27. package/dist/handlers/templateButtonAction.js +54 -0
  28. package/dist/platform/adapter.js +8 -0
  29. package/dist/platform/discord/discordAdapter.js +99 -0
  30. package/dist/platform/discord/index.js +15 -0
  31. package/dist/platform/discord/wrappers.js +331 -0
  32. package/dist/platform/index.js +18 -0
  33. package/dist/platform/richContentBuilder.js +76 -0
  34. package/dist/platform/telegram/index.js +16 -0
  35. package/dist/platform/telegram/telegramAdapter.js +195 -0
  36. package/dist/platform/telegram/telegramFormatter.js +134 -0
  37. package/dist/platform/telegram/wrappers.js +333 -0
  38. package/dist/platform/types.js +28 -0
  39. package/dist/services/approvalDetector.js +15 -2
  40. package/dist/services/cdpBridgeManager.js +91 -146
  41. package/dist/services/cdpService.js +88 -2
  42. package/dist/services/chatSessionService.js +50 -10
  43. package/dist/services/defaultModelApplicator.js +54 -0
  44. package/dist/services/modeService.js +16 -1
  45. package/dist/services/modelService.js +57 -16
  46. package/dist/services/notificationSender.js +149 -0
  47. package/dist/services/responseMonitor.js +1 -2
  48. package/dist/services/screenshotService.js +2 -2
  49. package/dist/ui/autoAcceptUi.js +37 -0
  50. package/dist/ui/modeUi.js +38 -1
  51. package/dist/ui/modelsUi.js +96 -0
  52. package/dist/ui/outputUi.js +32 -0
  53. package/dist/ui/projectListUi.js +55 -0
  54. package/dist/ui/screenshotUi.js +26 -0
  55. package/dist/ui/sessionPickerUi.js +35 -1
  56. package/dist/ui/templateUi.js +41 -0
  57. package/dist/utils/configLoader.js +63 -12
  58. package/dist/utils/lockfile.js +5 -5
  59. package/dist/utils/logger.js +7 -0
  60. package/dist/utils/telegramImageHandler.js +127 -0
  61. package/package.json +6 -3
  62. package/dist/commands/joinDetachCommandHandler.js +0 -285
  63. package/dist/services/retryStore.js +0 -46
  64. package/dist/ui/buttonUtils.js +0 -33
  65. package/dist/utils/antigravityPaths.js +0 -94
  66. package/dist/utils/logFileTransport.js +0 -147
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EventRouter = void 0;
4
+ const logger_1 = require("../utils/logger");
5
+ /**
6
+ * Routes events from multiple PlatformAdapters through auth check
7
+ * and dispatches to unified handlers.
8
+ */
9
+ class EventRouter {
10
+ config;
11
+ handlers;
12
+ adapters = [];
13
+ constructor(config, handlers) {
14
+ this.config = config;
15
+ this.handlers = handlers;
16
+ }
17
+ /** Register an adapter. Stores it for later start/stop. */
18
+ registerAdapter(adapter) {
19
+ this.adapters.push(adapter);
20
+ }
21
+ /** Start all registered adapters. */
22
+ async startAll() {
23
+ await Promise.all(this.adapters.map((adapter) => {
24
+ const events = this.createAdapterEvents(adapter);
25
+ return adapter.start(events);
26
+ }));
27
+ }
28
+ /** Stop all registered adapters. */
29
+ async stopAll() {
30
+ await Promise.all(this.adapters.map((a) => a.stop()));
31
+ }
32
+ /** Check if a user is authorized on a given platform. */
33
+ isAuthorized(platform, userId) {
34
+ const allowed = this.config.allowedUsers.get(platform);
35
+ return allowed ? allowed.has(userId) : false;
36
+ }
37
+ createAdapterEvents(adapter) {
38
+ return {
39
+ onReady: () => {
40
+ logger_1.logger.info(`[EventRouter] ${adapter.platform} adapter ready`);
41
+ },
42
+ onMessage: async (msg) => {
43
+ if (msg.author.isBot)
44
+ return;
45
+ if (!this.isAuthorized(msg.platform, msg.author.id))
46
+ return;
47
+ await this.handlers.onMessage?.(msg);
48
+ },
49
+ onButtonInteraction: async (interaction) => {
50
+ if (!this.isAuthorized(interaction.platform, interaction.user.id))
51
+ return;
52
+ await this.handlers.onButtonInteraction?.(interaction);
53
+ },
54
+ onSelectInteraction: async (interaction) => {
55
+ if (!this.isAuthorized(interaction.platform, interaction.user.id))
56
+ return;
57
+ await this.handlers.onSelectInteraction?.(interaction);
58
+ },
59
+ onCommandInteraction: async (interaction) => {
60
+ if (!this.isAuthorized(interaction.platform, interaction.user.id))
61
+ return;
62
+ await this.handlers.onCommandInteraction?.(interaction);
63
+ },
64
+ onError: (err) => {
65
+ logger_1.logger.error(`[EventRouter] ${adapter.platform} error:`, err);
66
+ },
67
+ };
68
+ }
69
+ }
70
+ exports.EventRouter = EventRouter;
package/dist/bot/index.js CHANGED
@@ -43,11 +43,13 @@ const logger_1 = require("../utils/logger");
43
43
  const logBuffer_1 = require("../utils/logBuffer");
44
44
  const discord_js_1 = require("discord.js");
45
45
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
46
+ const wrappers_1 = require("../platform/discord/wrappers");
46
47
  const config_1 = require("../utils/config");
47
48
  const slashCommandHandler_1 = require("../commands/slashCommandHandler");
48
49
  const registerSlashCommands_1 = require("../commands/registerSlashCommands");
49
50
  const modeService_1 = require("../services/modeService");
50
51
  const modelService_1 = require("../services/modelService");
52
+ const defaultModelApplicator_1 = require("../services/defaultModelApplicator");
51
53
  const templateRepository_1 = require("../database/templateRepository");
52
54
  const workspaceBindingRepository_1 = require("../database/workspaceBindingRepository");
53
55
  const chatSessionRepository_1 = require("../database/chatSessionRepository");
@@ -78,6 +80,22 @@ const userPreferenceRepository_1 = require("../database/userPreferenceRepository
78
80
  const plainTextFormatter_1 = require("../utils/plainTextFormatter");
79
81
  const interactionCreateHandler_1 = require("../events/interactionCreateHandler");
80
82
  const messageCreateHandler_1 = require("../events/messageCreateHandler");
83
+ // Telegram platform support
84
+ const grammy_1 = require("grammy");
85
+ const telegramAdapter_1 = require("../platform/telegram/telegramAdapter");
86
+ const telegramBindingRepository_1 = require("../database/telegramBindingRepository");
87
+ const telegramMessageHandler_1 = require("./telegramMessageHandler");
88
+ const telegramProjectCommand_1 = require("./telegramProjectCommand");
89
+ const eventRouter_1 = require("./eventRouter");
90
+ const buttonHandler_1 = require("../handlers/buttonHandler");
91
+ const selectHandler_1 = require("../handlers/selectHandler");
92
+ const approvalButtonAction_1 = require("../handlers/approvalButtonAction");
93
+ const planningButtonAction_1 = require("../handlers/planningButtonAction");
94
+ const errorPopupButtonAction_1 = require("../handlers/errorPopupButtonAction");
95
+ const modelButtonAction_1 = require("../handlers/modelButtonAction");
96
+ const autoAcceptButtonAction_1 = require("../handlers/autoAcceptButtonAction");
97
+ const templateButtonAction_1 = require("../handlers/templateButtonAction");
98
+ const modeSelectAction_1 = require("../handlers/modeSelectAction");
81
99
  // =============================================================================
82
100
  // Embed color palette (color-coded by phase)
83
101
  // =============================================================================
@@ -286,6 +304,11 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
286
304
  signalCompletion('cdp-disconnected');
287
305
  return;
288
306
  }
307
+ // Apply default model preference on CDP connect
308
+ const defaultModelResult = await (0, defaultModelApplicator_1.applyDefaultModel)(cdp, modelService);
309
+ if (defaultModelResult.stale && defaultModelResult.staleMessage && channel) {
310
+ await channel.send(defaultModelResult.staleMessage).catch(() => { });
311
+ }
289
312
  const localMode = modeService.getCurrentMode();
290
313
  const modeName = modeService_1.MODE_UI_NAMES[localMode] || localMode;
291
314
  const currentModel = (await cdp.getCurrentModel()) || modelService.getCurrentModel();
@@ -450,6 +473,7 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
450
473
  }
451
474
  }, `upsert-activity:${opts?.source ?? 'unknown'}`);
452
475
  try {
476
+ logger_1.logger.prompt(prompt);
453
477
  let injectResult;
454
478
  if (inboundImages.length > 0) {
455
479
  injectResult = await cdp.injectMessageWithImageFiles(prompt, inboundImages.map((image) => image.localPath));
@@ -476,6 +500,7 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
476
500
  pollIntervalMs: 2000,
477
501
  maxDurationMs: 300000,
478
502
  stopGoneConfirmCount: 3,
503
+ extractionMode: options?.extractionMode,
479
504
  onPhaseChange: (_phase, _text) => {
480
505
  // Phase transitions are already logged inside ResponseMonitor.setPhase()
481
506
  },
@@ -602,7 +627,7 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
602
627
  ? bridge.pool.extractProjectName(session.workspacePath)
603
628
  : cdp.getCurrentWorkspaceName();
604
629
  if (projectName) {
605
- (0, cdpBridgeManager_1.registerApprovalSessionChannel)(bridge, projectName, sessionInfo.title, message.channel);
630
+ (0, cdpBridgeManager_1.registerApprovalSessionChannel)(bridge, projectName, sessionInfo.title, (0, wrappers_1.wrapDiscordChannel)(message.channel));
606
631
  }
607
632
  const newName = options.titleGenerator.sanitizeForChannelName(sessionInfo.title);
608
633
  if (session && session.displayName !== sessionInfo.title) {
@@ -700,6 +725,17 @@ const startBot = async (cliLogLevel) => {
700
725
  const modelService = new modelService_1.ModelService();
701
726
  const templateRepo = new templateRepository_1.TemplateRepository(db);
702
727
  const userPrefRepo = new userPreferenceRepository_1.UserPreferenceRepository(db);
728
+ // Eagerly load default model from DB (single-user bot optimization)
729
+ try {
730
+ const firstUser = db.prepare('SELECT user_id FROM user_preferences LIMIT 1').get();
731
+ if (firstUser) {
732
+ const savedDefault = userPrefRepo.getDefaultModel(firstUser.user_id);
733
+ modelService.loadDefaultModel(savedDefault);
734
+ }
735
+ }
736
+ catch {
737
+ // DB may not have user_preferences yet — safe to ignore
738
+ }
703
739
  const workspaceBindingRepo = new workspaceBindingRepository_1.WorkspaceBindingRepository(db);
704
740
  const chatSessionRepo = new chatSessionRepository_1.ChatSessionRepository(db);
705
741
  const workspaceService = new workspaceService_1.WorkspaceService(config.workspaceBaseDir);
@@ -722,165 +758,337 @@ const startBot = async (cliLogLevel) => {
722
758
  const chatHandler = new chatCommandHandler_1.ChatCommandHandler(chatSessionService, chatSessionRepo, workspaceBindingRepo, channelManager, workspaceService, bridge.pool);
723
759
  const cleanupHandler = new cleanupCommandHandler_1.CleanupCommandHandler(chatSessionRepo, workspaceBindingRepo);
724
760
  const slashCommandHandler = new slashCommandHandler_1.SlashCommandHandler(templateRepo);
725
- const client = new discord_js_1.Client({
726
- intents: [
727
- discord_js_1.GatewayIntentBits.Guilds,
728
- discord_js_1.GatewayIntentBits.GuildMessages,
729
- discord_js_1.GatewayIntentBits.MessageContent,
730
- ]
731
- });
732
- const joinHandler = new joinCommandHandler_1.JoinCommandHandler(chatSessionService, chatSessionRepo, workspaceBindingRepo, channelManager, bridge.pool, workspaceService, client);
733
- client.once(discord_js_1.Events.ClientReady, async (readyClient) => {
734
- logger_1.logger.info(`Ready! Logged in as ${readyClient.user.tag} | extractionMode=${config.extractionMode}`);
735
- try {
736
- await (0, registerSlashCommands_1.registerSlashCommands)(config.discordToken, config.clientId, config.guildId);
737
- }
738
- catch (error) {
739
- logger_1.logger.warn('Failed to register slash commands, but text commands remain available.');
761
+ // Discord platform — only initialise the Discord client when the platform is enabled
762
+ if (config.platforms.includes('discord')) {
763
+ if (!config.discordToken || !config.clientId) {
764
+ logger_1.logger.error('Discord platform enabled but discordToken or clientId is missing. Skipping Discord initialization.');
740
765
  }
741
- // Startup dashboard embed
742
- try {
743
- const os = await Promise.resolve().then(() => __importStar(require('os')));
744
- const pkg = await Promise.resolve().then(() => __importStar(require('../../package.json')));
745
- const version = pkg.default?.version ?? pkg.version ?? 'unknown';
746
- const projects = workspaceService.scanWorkspaces();
747
- // Check CDP connection status
748
- const activeWorkspaces = bridge.pool.getActiveWorkspaceNames();
749
- const cdpStatus = activeWorkspaces.length > 0
750
- ? `Connected (${activeWorkspaces.join(', ')})`
751
- : 'Not connected';
752
- const dashboardEmbed = new discord_js_1.EmbedBuilder()
753
- .setTitle('LazyGravity Online')
754
- .setColor(0x57F287)
755
- .addFields({ name: 'Version', value: version, inline: true }, { name: 'Node.js', value: process.versions.node, inline: true }, { name: 'OS', value: `${os.platform()} ${os.release()}`, inline: true }, { name: 'CDP', value: cdpStatus, inline: true }, { name: 'Model', value: modelService.getCurrentModel(), inline: true }, { name: 'Mode', value: modeService.getCurrentMode(), inline: true }, { name: 'Projects', value: `${projects.length} registered`, inline: true }, { name: 'Extraction', value: config.extractionMode, inline: true })
756
- .setFooter({ text: `Started at ${new Date().toLocaleString()}` })
757
- .setTimestamp();
758
- // Send to the first available text channel in the guild
759
- const guild = readyClient.guilds.cache.first();
760
- if (guild) {
761
- const channel = guild.channels.cache.find((ch) => ch.isTextBased() && !ch.isVoiceBased() && ch.permissionsFor(readyClient.user)?.has('SendMessages'));
762
- if (channel && channel.isTextBased()) {
763
- await channel.send({ embeds: [dashboardEmbed] });
764
- logger_1.logger.info('Startup dashboard embed sent.');
766
+ else {
767
+ const discordToken = config.discordToken;
768
+ const discordClientId = config.clientId;
769
+ const client = new discord_js_1.Client({
770
+ intents: [
771
+ discord_js_1.GatewayIntentBits.Guilds,
772
+ discord_js_1.GatewayIntentBits.GuildMessages,
773
+ discord_js_1.GatewayIntentBits.MessageContent,
774
+ ]
775
+ });
776
+ const joinHandler = new joinCommandHandler_1.JoinCommandHandler(chatSessionService, chatSessionRepo, workspaceBindingRepo, channelManager, bridge.pool, workspaceService, client, config.extractionMode);
777
+ client.once(discord_js_1.Events.ClientReady, async (readyClient) => {
778
+ logger_1.logger.info(`Ready! Logged in as ${readyClient.user.tag} | extractionMode=${config.extractionMode}`);
779
+ try {
780
+ await (0, registerSlashCommands_1.registerSlashCommands)(discordToken, discordClientId, config.guildId);
765
781
  }
766
- }
767
- }
768
- catch (error) {
769
- logger_1.logger.warn('Failed to send startup dashboard embed:', error);
770
- }
771
- });
772
- // [Discord Interactions API] Slash command interaction handler
773
- client.on(discord_js_1.Events.InteractionCreate, (0, interactionCreateHandler_1.createInteractionCreateHandler)({
774
- config,
775
- bridge,
776
- cleanupHandler,
777
- modeService,
778
- modelService,
779
- slashCommandHandler,
780
- wsHandler,
781
- chatHandler,
782
- client,
783
- sendModeUI: modeUi_1.sendModeUI,
784
- sendModelsUI: modelsUi_1.sendModelsUI,
785
- sendAutoAcceptUI: autoAcceptUi_1.sendAutoAcceptUI,
786
- getCurrentCdp: cdpBridgeManager_1.getCurrentCdp,
787
- parseApprovalCustomId: cdpBridgeManager_1.parseApprovalCustomId,
788
- parseErrorPopupCustomId: cdpBridgeManager_1.parseErrorPopupCustomId,
789
- parsePlanningCustomId: cdpBridgeManager_1.parsePlanningCustomId,
790
- joinHandler,
791
- userPrefRepo,
792
- handleSlashInteraction: async (interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg) => handleSlashInteraction(interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg, promptDispatcher, templateRepo, joinHandler, userPrefRepo),
793
- handleTemplateUse: async (interaction, templateId) => {
794
- const template = templateRepo.findById(templateId);
795
- if (!template) {
796
- await interaction.followUp({
797
- content: 'Template not found. It may have been deleted.',
798
- flags: discord_js_1.MessageFlags.Ephemeral,
799
- });
800
- return;
801
- }
802
- // Resolve CDP via workspace binding (same flow as text messages)
803
- const channelId = interaction.channelId;
804
- const workspacePath = wsHandler.getWorkspaceForChannel(channelId);
805
- let cdp = null;
806
- if (workspacePath) {
782
+ catch (error) {
783
+ logger_1.logger.warn('Failed to register slash commands, but text commands remain available.');
784
+ }
785
+ // Startup dashboard embed
807
786
  try {
808
- cdp = await bridge.pool.getOrConnect(workspacePath);
809
- const projectName = bridge.pool.extractProjectName(workspacePath);
810
- bridge.lastActiveWorkspace = projectName;
811
- bridge.lastActiveChannel = interaction.channel;
812
- (0, cdpBridgeManager_1.registerApprovalWorkspaceChannel)(bridge, projectName, interaction.channel);
813
- const session = chatSessionRepo.findByChannelId(channelId);
814
- if (session?.displayName) {
815
- (0, cdpBridgeManager_1.registerApprovalSessionChannel)(bridge, projectName, session.displayName, interaction.channel);
787
+ const os = await Promise.resolve().then(() => __importStar(require('os')));
788
+ const pkg = await Promise.resolve().then(() => __importStar(require('../../package.json')));
789
+ const version = pkg.default?.version ?? pkg.version ?? 'unknown';
790
+ const projects = workspaceService.scanWorkspaces();
791
+ // Check CDP connection status
792
+ const activeWorkspaces = bridge.pool.getActiveWorkspaceNames();
793
+ const cdpStatus = activeWorkspaces.length > 0
794
+ ? `Connected (${activeWorkspaces.join(', ')})`
795
+ : 'Not connected';
796
+ const dashboardEmbed = new discord_js_1.EmbedBuilder()
797
+ .setTitle('LazyGravity Online')
798
+ .setColor(0x57F287)
799
+ .addFields({ name: 'Version', value: version, inline: true }, { name: 'Node.js', value: process.versions.node, inline: true }, { name: 'OS', value: `${os.platform()} ${os.release()}`, inline: true }, { name: 'CDP', value: cdpStatus, inline: true }, { name: 'Model', value: modelService.getCurrentModel(), inline: true }, { name: 'Mode', value: modeService.getCurrentMode(), inline: true }, { name: 'Projects', value: `${projects.length} registered`, inline: true }, { name: 'Extraction', value: config.extractionMode, inline: true })
800
+ .setFooter({ text: `Started at ${new Date().toLocaleString()}` })
801
+ .setTimestamp();
802
+ // Send to the first available text channel in the guild
803
+ const guild = readyClient.guilds.cache.first();
804
+ if (guild) {
805
+ const channel = guild.channels.cache.find((ch) => ch.isTextBased() && !ch.isVoiceBased() && ch.permissionsFor(readyClient.user)?.has('SendMessages'));
806
+ if (channel && channel.isTextBased()) {
807
+ await channel.send({ embeds: [dashboardEmbed] });
808
+ logger_1.logger.info('Startup dashboard embed sent.');
809
+ }
816
810
  }
817
- (0, cdpBridgeManager_1.ensureApprovalDetector)(bridge, cdp, projectName, client);
818
- (0, cdpBridgeManager_1.ensureErrorPopupDetector)(bridge, cdp, projectName, client);
819
- (0, cdpBridgeManager_1.ensurePlanningDetector)(bridge, cdp, projectName, client);
820
811
  }
821
- catch (e) {
822
- await interaction.followUp({
823
- content: `Failed to connect to workspace: ${e.message}`,
824
- flags: discord_js_1.MessageFlags.Ephemeral,
812
+ catch (error) {
813
+ logger_1.logger.warn('Failed to send startup dashboard embed:', error);
814
+ }
815
+ });
816
+ // [Discord Interactions API] Slash command interaction handler
817
+ client.on(discord_js_1.Events.InteractionCreate, (0, interactionCreateHandler_1.createInteractionCreateHandler)({
818
+ config,
819
+ bridge,
820
+ cleanupHandler,
821
+ modeService,
822
+ modelService,
823
+ slashCommandHandler,
824
+ wsHandler,
825
+ chatHandler,
826
+ client,
827
+ sendModeUI: modeUi_1.sendModeUI,
828
+ sendModelsUI: modelsUi_1.sendModelsUI,
829
+ sendAutoAcceptUI: autoAcceptUi_1.sendAutoAcceptUI,
830
+ getCurrentCdp: cdpBridgeManager_1.getCurrentCdp,
831
+ parseApprovalCustomId: cdpBridgeManager_1.parseApprovalCustomId,
832
+ parseErrorPopupCustomId: cdpBridgeManager_1.parseErrorPopupCustomId,
833
+ parsePlanningCustomId: cdpBridgeManager_1.parsePlanningCustomId,
834
+ joinHandler,
835
+ userPrefRepo,
836
+ handleSlashInteraction: async (interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg) => handleSlashInteraction(interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg, promptDispatcher, templateRepo, joinHandler, userPrefRepo),
837
+ handleTemplateUse: async (interaction, templateId) => {
838
+ const template = templateRepo.findById(templateId);
839
+ if (!template) {
840
+ await interaction.followUp({
841
+ content: 'Template not found. It may have been deleted.',
842
+ flags: discord_js_1.MessageFlags.Ephemeral,
843
+ });
844
+ return;
845
+ }
846
+ // Resolve CDP via workspace binding (same flow as text messages)
847
+ const channelId = interaction.channelId;
848
+ const workspacePath = wsHandler.getWorkspaceForChannel(channelId);
849
+ let cdp = null;
850
+ if (workspacePath) {
851
+ try {
852
+ cdp = await bridge.pool.getOrConnect(workspacePath);
853
+ const projectName = bridge.pool.extractProjectName(workspacePath);
854
+ bridge.lastActiveWorkspace = projectName;
855
+ const platformCh = (0, wrappers_1.wrapDiscordChannel)(interaction.channel);
856
+ bridge.lastActiveChannel = platformCh;
857
+ (0, cdpBridgeManager_1.registerApprovalWorkspaceChannel)(bridge, projectName, platformCh);
858
+ const session = chatSessionRepo.findByChannelId(channelId);
859
+ if (session?.displayName) {
860
+ (0, cdpBridgeManager_1.registerApprovalSessionChannel)(bridge, projectName, session.displayName, platformCh);
861
+ }
862
+ (0, cdpBridgeManager_1.ensureApprovalDetector)(bridge, cdp, projectName);
863
+ (0, cdpBridgeManager_1.ensureErrorPopupDetector)(bridge, cdp, projectName);
864
+ (0, cdpBridgeManager_1.ensurePlanningDetector)(bridge, cdp, projectName);
865
+ }
866
+ catch (e) {
867
+ await interaction.followUp({
868
+ content: `Failed to connect to workspace: ${e.message}`,
869
+ flags: discord_js_1.MessageFlags.Ephemeral,
870
+ });
871
+ return;
872
+ }
873
+ }
874
+ else {
875
+ cdp = (0, cdpBridgeManager_1.getCurrentCdp)(bridge);
876
+ }
877
+ if (!cdp) {
878
+ await interaction.followUp({
879
+ content: 'Not connected to CDP. Please connect to a project first.',
880
+ flags: discord_js_1.MessageFlags.Ephemeral,
881
+ });
882
+ return;
883
+ }
884
+ const followUp = await interaction.followUp({
885
+ content: `Executing template **${template.name}**...`,
825
886
  });
887
+ if (followUp instanceof discord_js_1.Message) {
888
+ await promptDispatcher.send({
889
+ message: followUp,
890
+ prompt: template.prompt,
891
+ cdp,
892
+ inboundImages: [],
893
+ options: {
894
+ chatSessionService,
895
+ chatSessionRepo,
896
+ channelManager,
897
+ titleGenerator,
898
+ userPrefRepo,
899
+ extractionMode: config.extractionMode,
900
+ },
901
+ });
902
+ }
903
+ },
904
+ }));
905
+ // [Text message handler]
906
+ client.on(discord_js_1.Events.MessageCreate, (0, messageCreateHandler_1.createMessageCreateHandler)({
907
+ config,
908
+ bridge,
909
+ modeService,
910
+ modelService,
911
+ slashCommandHandler,
912
+ wsHandler,
913
+ chatSessionService,
914
+ chatSessionRepo,
915
+ channelManager,
916
+ titleGenerator,
917
+ client,
918
+ sendPromptToAntigravity: async (_bridge, message, prompt, cdp, _modeService, _modelService, inboundImages = [], options) => promptDispatcher.send({
919
+ message,
920
+ prompt,
921
+ cdp,
922
+ inboundImages,
923
+ options,
924
+ }),
925
+ autoRenameChannel,
926
+ handleScreenshot: screenshotUi_1.handleScreenshot,
927
+ userPrefRepo,
928
+ }));
929
+ await client.login(discordToken);
930
+ } // end: else (credentials present)
931
+ } // end: Discord platform gate
932
+ // Telegram platform
933
+ if (config.platforms.includes('telegram') && config.telegramToken) {
934
+ try {
935
+ const telegramBot = new grammy_1.Bot(config.telegramToken);
936
+ // Attach toInputFile so wrappers can convert Buffer to grammY InputFile
937
+ telegramBot.toInputFile = (data, filename) => new grammy_1.InputFile(data, filename);
938
+ // Retry getMe() up to 3 times to handle transient network failures
939
+ const botInfo = await (async () => {
940
+ for (let attempt = 1; attempt <= 3; attempt++) {
941
+ try {
942
+ return await telegramBot.api.getMe();
943
+ }
944
+ catch (err) {
945
+ if (attempt === 3)
946
+ throw err;
947
+ logger_1.logger.warn(`[Telegram] getMe() failed (attempt ${attempt}/3): ${err?.message ?? err}. Retrying in 3s...`);
948
+ await new Promise(r => setTimeout(r, 3000));
949
+ }
950
+ }
951
+ throw new Error('getMe() failed after 3 attempts');
952
+ })();
953
+ const telegramBindingRepo = new telegramBindingRepository_1.TelegramBindingRepository(db);
954
+ const telegramAdapter = new telegramAdapter_1.TelegramAdapter(telegramBot, String(botInfo.id));
955
+ const activeMonitors = new Map();
956
+ const telegramHandler = (0, telegramMessageHandler_1.createTelegramMessageHandler)({
957
+ bridge,
958
+ telegramBindingRepo,
959
+ workspaceService,
960
+ modeService,
961
+ modelService,
962
+ extractionMode: config.extractionMode,
963
+ templateRepo,
964
+ fetchQuota: () => bridge.quota.fetchQuota(),
965
+ activeMonitors,
966
+ botToken: config.telegramToken,
967
+ botApi: telegramBot.api,
968
+ chatSessionService,
969
+ });
970
+ // Compose select handlers: project select + mode select
971
+ const projectSelectHandler = (0, telegramProjectCommand_1.createTelegramSelectHandler)({
972
+ workspaceService,
973
+ telegramBindingRepo,
974
+ });
975
+ const modeSelectAction = (0, modeSelectAction_1.createModeSelectAction)({ bridge, modeService });
976
+ const telegramSelectHandler = (0, selectHandler_1.createPlatformSelectHandler)({
977
+ actions: [
978
+ modeSelectAction,
979
+ ],
980
+ });
981
+ // Composite handler that routes to the right handler
982
+ const compositeSelectHandler = async (interaction) => {
983
+ if (interaction.customId === 'mode_select') {
984
+ await telegramSelectHandler(interaction);
826
985
  return;
827
986
  }
987
+ await projectSelectHandler(interaction);
988
+ };
989
+ const allowedUsers = new Map();
990
+ if (config.telegramAllowedUserIds && config.telegramAllowedUserIds.length > 0) {
991
+ allowedUsers.set('telegram', new Set(config.telegramAllowedUserIds));
828
992
  }
829
993
  else {
830
- cdp = (0, cdpBridgeManager_1.getCurrentCdp)(bridge);
831
- }
832
- if (!cdp) {
833
- await interaction.followUp({
834
- content: 'Not connected to CDP. Please connect to a project first.',
835
- flags: discord_js_1.MessageFlags.Ephemeral,
836
- });
837
- return;
994
+ logger_1.logger.warn('Telegram platform enabled but TELEGRAM_ALLOWED_USER_IDS is empty — all users will be denied access.');
838
995
  }
839
- const followUp = await interaction.followUp({
840
- content: `Executing template **${template.name}**...`,
996
+ const telegramButtonHandler = (0, buttonHandler_1.createPlatformButtonHandler)({
997
+ actions: [
998
+ (0, approvalButtonAction_1.createApprovalButtonAction)({ bridge }),
999
+ (0, planningButtonAction_1.createPlanningButtonAction)({ bridge }),
1000
+ (0, errorPopupButtonAction_1.createErrorPopupButtonAction)({ bridge }),
1001
+ (0, modelButtonAction_1.createModelButtonAction)({ bridge, fetchQuota: () => bridge.quota.fetchQuota(), modelService, userPrefRepo }),
1002
+ (0, autoAcceptButtonAction_1.createAutoAcceptButtonAction)({ autoAcceptService: bridge.autoAccept }),
1003
+ (0, templateButtonAction_1.createTemplateButtonAction)({ bridge, templateRepo }),
1004
+ ],
841
1005
  });
842
- if (followUp instanceof discord_js_1.Message) {
843
- await promptDispatcher.send({
844
- message: followUp,
845
- prompt: template.prompt,
846
- cdp,
847
- inboundImages: [],
848
- options: {
849
- chatSessionService,
850
- chatSessionRepo,
851
- channelManager,
852
- titleGenerator,
853
- userPrefRepo,
854
- },
855
- });
1006
+ const eventRouter = new eventRouter_1.EventRouter({ allowedUsers }, {
1007
+ onMessage: telegramHandler,
1008
+ onButtonInteraction: telegramButtonHandler,
1009
+ onSelectInteraction: compositeSelectHandler,
1010
+ });
1011
+ // Register bot commands BEFORE starting polling so Telegram shows "/" suggestions
1012
+ await telegramBot.api.setMyCommands([
1013
+ { command: 'start', description: 'Welcome message' },
1014
+ { command: 'project', description: 'Manage workspace bindings' },
1015
+ { command: 'status', description: 'Show bot status and connections' },
1016
+ { command: 'mode', description: 'Switch execution mode' },
1017
+ { command: 'model', description: 'Switch LLM model' },
1018
+ { command: 'screenshot', description: 'Capture Antigravity screenshot' },
1019
+ { command: 'autoaccept', description: 'Toggle auto-accept mode' },
1020
+ { command: 'template', description: 'List prompt templates' },
1021
+ { command: 'template_add', description: 'Add a prompt template' },
1022
+ { command: 'template_delete', description: 'Delete a prompt template' },
1023
+ { command: 'project_create', description: 'Create a new workspace' },
1024
+ { command: 'new', description: 'Start a new chat session' },
1025
+ { command: 'logs', description: 'Show recent log entries' },
1026
+ { command: 'stop', description: 'Interrupt active LLM generation' },
1027
+ { command: 'help', description: 'Show available commands' },
1028
+ { command: 'ping', description: 'Check bot latency' },
1029
+ ]).catch((e) => {
1030
+ logger_1.logger.warn('Failed to register Telegram commands:', e instanceof Error ? e.message : e);
1031
+ });
1032
+ eventRouter.registerAdapter(telegramAdapter);
1033
+ await eventRouter.startAll();
1034
+ logger_1.logger.info(`Telegram bot started: @${botInfo.username} (${config.telegramAllowedUserIds?.length ?? 0} allowed users)`);
1035
+ // Send startup message to all bound Telegram chats
1036
+ const bindings = telegramBindingRepo.findAll();
1037
+ if (bindings.length > 0) {
1038
+ const os = await Promise.resolve().then(() => __importStar(require('os')));
1039
+ const pkg = await Promise.resolve().then(() => __importStar(require('../../package.json')));
1040
+ const version = pkg.default?.version ?? pkg.version ?? 'unknown';
1041
+ const projects = workspaceService.scanWorkspaces();
1042
+ const activeWorkspaces = bridge.pool.getActiveWorkspaceNames();
1043
+ const cdpStatus = activeWorkspaces.length > 0
1044
+ ? `Connected (${activeWorkspaces.join(', ')})`
1045
+ : 'Not connected';
1046
+ const startupText = [
1047
+ '<b>LazyGravity Online</b>',
1048
+ '',
1049
+ `Version: ${version}`,
1050
+ `Node.js: ${process.versions.node}`,
1051
+ `OS: ${os.platform()} ${os.release()}`,
1052
+ `CDP: ${cdpStatus}`,
1053
+ `Model: ${modelService.getCurrentModel()}`,
1054
+ `Mode: ${modeService.getCurrentMode()}`,
1055
+ `Projects: ${projects.length} registered`,
1056
+ `Extraction: ${config.extractionMode}`,
1057
+ '',
1058
+ `<i>Started at ${new Date().toLocaleString()}</i>`,
1059
+ ].join('\n');
1060
+ const sendWithRetry = async (chatId, text, retries = 3, delayMs = 2000) => {
1061
+ for (let attempt = 1; attempt <= retries; attempt++) {
1062
+ try {
1063
+ await telegramBot.api.sendMessage(chatId, text, { parse_mode: 'HTML' });
1064
+ return;
1065
+ }
1066
+ catch (err) {
1067
+ if (attempt < retries) {
1068
+ logger_1.logger.debug(`[Telegram] Startup message attempt ${attempt}/${retries} failed, retrying in ${delayMs}ms...`);
1069
+ await new Promise((r) => setTimeout(r, delayMs));
1070
+ }
1071
+ else {
1072
+ throw err;
1073
+ }
1074
+ }
1075
+ }
1076
+ };
1077
+ const results = await Promise.allSettled(bindings.map((binding) => sendWithRetry(binding.chatId, startupText)));
1078
+ const failed = results.filter((r) => r.status === 'rejected');
1079
+ if (failed.length > 0) {
1080
+ logger_1.logger.warn(`[Telegram] Startup message failed for ${failed.length}/${bindings.length} chat(s) after retries: ${failed[0].reason?.message ?? 'unknown error'}`);
1081
+ }
1082
+ else {
1083
+ logger_1.logger.info(`Telegram startup message sent to ${bindings.length} bound chat(s).`);
1084
+ }
856
1085
  }
857
- },
858
- }));
859
- // [Text message handler]
860
- client.on(discord_js_1.Events.MessageCreate, (0, messageCreateHandler_1.createMessageCreateHandler)({
861
- config,
862
- bridge,
863
- modeService,
864
- modelService,
865
- slashCommandHandler,
866
- wsHandler,
867
- chatSessionService,
868
- chatSessionRepo,
869
- channelManager,
870
- titleGenerator,
871
- client,
872
- sendPromptToAntigravity: async (_bridge, message, prompt, cdp, _modeService, _modelService, inboundImages = [], options) => promptDispatcher.send({
873
- message,
874
- prompt,
875
- cdp,
876
- inboundImages,
877
- options,
878
- }),
879
- autoRenameChannel,
880
- handleScreenshot: screenshotUi_1.handleScreenshot,
881
- userPrefRepo,
882
- }));
883
- await client.login(config.discordToken);
1086
+ }
1087
+ catch (e) {
1088
+ const message = e instanceof Error ? e.message : String(e);
1089
+ logger_1.logger.error('Failed to start Telegram adapter:', message);
1090
+ }
1091
+ }
884
1092
  };
885
1093
  exports.startBot = startBot;
886
1094
  /**