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.
- package/README.md +77 -15
- package/dist/bin/cli.js +0 -0
- package/dist/bin/commands/doctor.js +19 -2
- package/dist/bin/commands/open.js +1 -1
- package/dist/bin/commands/setup.js +286 -70
- package/dist/bot/eventRouter.js +70 -0
- package/dist/bot/index.js +355 -147
- package/dist/bot/telegramCommands.js +478 -0
- package/dist/bot/telegramMessageHandler.js +308 -0
- package/dist/bot/telegramProjectCommand.js +137 -0
- package/dist/bot/workspaceQueue.js +61 -0
- package/dist/commands/joinCommandHandler.js +4 -1
- package/dist/database/telegramBindingRepository.js +97 -0
- package/dist/database/userPreferenceRepository.js +46 -1
- package/dist/events/interactionCreateHandler.js +36 -0
- package/dist/events/messageCreateHandler.js +11 -7
- package/dist/handlers/approvalButtonAction.js +99 -0
- package/dist/handlers/autoAcceptButtonAction.js +43 -0
- package/dist/handlers/buttonHandler.js +55 -0
- package/dist/handlers/commandHandler.js +44 -0
- package/dist/handlers/errorPopupButtonAction.js +137 -0
- package/dist/handlers/messageHandler.js +70 -0
- package/dist/handlers/modeSelectAction.js +63 -0
- package/dist/handlers/modelButtonAction.js +102 -0
- package/dist/handlers/planningButtonAction.js +118 -0
- package/dist/handlers/selectHandler.js +41 -0
- package/dist/handlers/templateButtonAction.js +54 -0
- package/dist/platform/adapter.js +8 -0
- package/dist/platform/discord/discordAdapter.js +99 -0
- package/dist/platform/discord/index.js +15 -0
- package/dist/platform/discord/wrappers.js +331 -0
- package/dist/platform/index.js +18 -0
- package/dist/platform/richContentBuilder.js +76 -0
- package/dist/platform/telegram/index.js +16 -0
- package/dist/platform/telegram/telegramAdapter.js +195 -0
- package/dist/platform/telegram/telegramFormatter.js +134 -0
- package/dist/platform/telegram/wrappers.js +333 -0
- package/dist/platform/types.js +28 -0
- package/dist/services/approvalDetector.js +15 -2
- package/dist/services/cdpBridgeManager.js +91 -146
- package/dist/services/cdpService.js +88 -2
- package/dist/services/chatSessionService.js +50 -10
- package/dist/services/defaultModelApplicator.js +54 -0
- package/dist/services/modeService.js +16 -1
- package/dist/services/modelService.js +57 -16
- package/dist/services/notificationSender.js +149 -0
- package/dist/services/responseMonitor.js +1 -2
- package/dist/services/screenshotService.js +2 -2
- package/dist/ui/autoAcceptUi.js +37 -0
- package/dist/ui/modeUi.js +38 -1
- package/dist/ui/modelsUi.js +96 -0
- package/dist/ui/outputUi.js +32 -0
- package/dist/ui/projectListUi.js +55 -0
- package/dist/ui/screenshotUi.js +26 -0
- package/dist/ui/sessionPickerUi.js +35 -1
- package/dist/ui/templateUi.js +41 -0
- package/dist/utils/configLoader.js +63 -12
- package/dist/utils/lockfile.js +5 -5
- package/dist/utils/logger.js +7 -0
- package/dist/utils/telegramImageHandler.js +127 -0
- package/package.json +6 -3
- package/dist/commands/joinDetachCommandHandler.js +0 -285
- package/dist/services/retryStore.js +0 -46
- package/dist/ui/buttonUtils.js +0 -33
- package/dist/utils/antigravityPaths.js +0 -94
- 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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
const
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
.
|
|
754
|
-
|
|
755
|
-
|
|
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
|
-
|
|
769
|
-
|
|
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
|
-
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
(
|
|
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 (
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
-
|
|
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
|
|
840
|
-
|
|
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
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
/**
|