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.
- package/README.md +76 -15
- package/dist/bin/commands/doctor.js +19 -2
- package/dist/bin/commands/setup.js +286 -70
- package/dist/bot/eventRouter.js +70 -0
- package/dist/bot/index.js +353 -147
- package/dist/bot/telegramCommands.js +428 -0
- package/dist/bot/telegramMessageHandler.js +304 -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 +329 -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/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/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 +4 -2
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal Telegram message handler.
|
|
4
|
+
*
|
|
5
|
+
* Handles incoming PlatformMessage from Telegram:
|
|
6
|
+
* 1. Resolves workspace from TelegramBindingRepository
|
|
7
|
+
* 2. Connects to CDP
|
|
8
|
+
* 3. Injects the prompt into Antigravity
|
|
9
|
+
* 4. Monitors the response via ResponseMonitor
|
|
10
|
+
* 5. Relays the response text back via PlatformChannel.send()
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.createTelegramMessageHandler = createTelegramMessageHandler;
|
|
14
|
+
const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
|
|
15
|
+
const responseMonitor_1 = require("../services/responseMonitor");
|
|
16
|
+
const processLogBuffer_1 = require("../utils/processLogBuffer");
|
|
17
|
+
const discordFormatter_1 = require("../utils/discordFormatter");
|
|
18
|
+
const telegramProjectCommand_1 = require("./telegramProjectCommand");
|
|
19
|
+
const telegramCommands_1 = require("./telegramCommands");
|
|
20
|
+
const defaultModelApplicator_1 = require("../services/defaultModelApplicator");
|
|
21
|
+
const logger_1 = require("../utils/logger");
|
|
22
|
+
const telegramImageHandler_1 = require("../utils/telegramImageHandler");
|
|
23
|
+
const imageHandler_1 = require("../utils/imageHandler");
|
|
24
|
+
/**
|
|
25
|
+
* Create a handler for Telegram messages.
|
|
26
|
+
* Returns an async function that processes a single PlatformMessage.
|
|
27
|
+
*/
|
|
28
|
+
function createTelegramMessageHandler(deps) {
|
|
29
|
+
// Per-workspace prompt queue to serialize messages
|
|
30
|
+
const workspaceQueues = new Map();
|
|
31
|
+
function enqueueForWorkspace(workspacePath, task) {
|
|
32
|
+
const current = (workspaceQueues.get(workspacePath) ?? Promise.resolve()).catch(() => { });
|
|
33
|
+
const next = current.then(async () => {
|
|
34
|
+
try {
|
|
35
|
+
await task();
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
logger_1.logger.error('[TelegramQueue] task error:', err?.message || err);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
workspaceQueues.set(workspacePath, next);
|
|
42
|
+
return next;
|
|
43
|
+
}
|
|
44
|
+
return async (message) => {
|
|
45
|
+
const handlerEntryTime = Date.now();
|
|
46
|
+
const chatId = message.channel.id;
|
|
47
|
+
const hasImageAttachments = message.attachments.length > 0
|
|
48
|
+
&& message.attachments.some((att) => (att.contentType || '').startsWith('image/'));
|
|
49
|
+
const promptText = message.content.trim();
|
|
50
|
+
// Allow through if there's text OR image attachments
|
|
51
|
+
if (!promptText && !hasImageAttachments)
|
|
52
|
+
return;
|
|
53
|
+
logger_1.logger.debug(`[TelegramHandler] handler entered (chat=${chatId}, msgTime=${message.createdAt.toISOString()}, handlerDelay=${handlerEntryTime - message.createdAt.getTime()}ms)`);
|
|
54
|
+
// Intercept built-in commands (/help, /status, /stop, /ping, /start)
|
|
55
|
+
const cmd = (0, telegramCommands_1.parseTelegramCommand)(promptText);
|
|
56
|
+
if (cmd) {
|
|
57
|
+
await (0, telegramCommands_1.handleTelegramCommand)({
|
|
58
|
+
bridge: deps.bridge,
|
|
59
|
+
modeService: deps.modeService,
|
|
60
|
+
modelService: deps.modelService,
|
|
61
|
+
telegramBindingRepo: deps.telegramBindingRepo,
|
|
62
|
+
templateRepo: deps.templateRepo,
|
|
63
|
+
workspaceService: deps.workspaceService,
|
|
64
|
+
fetchQuota: deps.fetchQuota,
|
|
65
|
+
activeMonitors: deps.activeMonitors,
|
|
66
|
+
}, message, cmd);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// Intercept /project command before CDP path
|
|
70
|
+
if (deps.workspaceService) {
|
|
71
|
+
const parsed = (0, telegramProjectCommand_1.parseTelegramProjectCommand)(promptText);
|
|
72
|
+
if (parsed) {
|
|
73
|
+
await (0, telegramProjectCommand_1.handleTelegramProjectCommand)({ workspaceService: deps.workspaceService, telegramBindingRepo: deps.telegramBindingRepo }, message, parsed);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Resolve workspace binding for this Telegram chat
|
|
78
|
+
const binding = deps.telegramBindingRepo.findByChatId(chatId);
|
|
79
|
+
if (!binding) {
|
|
80
|
+
await message.reply({
|
|
81
|
+
text: 'No project is linked to this chat. Use /project to bind a workspace.',
|
|
82
|
+
}).catch(logger_1.logger.error);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// Resolve relative workspace name to absolute path (mirrors Discord handler behavior).
|
|
86
|
+
// Without this, CDP receives a bare name like "DemoLG" and Antigravity
|
|
87
|
+
// falls back to its default scratch directory.
|
|
88
|
+
const workspacePath = deps.workspaceService
|
|
89
|
+
? deps.workspaceService.getWorkspacePath(binding.workspacePath)
|
|
90
|
+
: binding.workspacePath;
|
|
91
|
+
await enqueueForWorkspace(workspacePath, async () => {
|
|
92
|
+
const cdpStartTime = Date.now();
|
|
93
|
+
logger_1.logger.debug(`[TelegramHandler] getOrConnect start (elapsed=${cdpStartTime - handlerEntryTime}ms)`);
|
|
94
|
+
let cdp;
|
|
95
|
+
try {
|
|
96
|
+
cdp = await deps.bridge.pool.getOrConnect(workspacePath);
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
await message.reply({
|
|
100
|
+
text: `Failed to connect to workspace: ${e.message}`,
|
|
101
|
+
}).catch(logger_1.logger.error);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
logger_1.logger.debug(`[TelegramHandler] getOrConnect done (took=${Date.now() - cdpStartTime}ms)`);
|
|
105
|
+
const projectName = deps.bridge.pool.extractProjectName(workspacePath);
|
|
106
|
+
deps.bridge.lastActiveWorkspace = projectName;
|
|
107
|
+
deps.bridge.lastActiveChannel = message.channel;
|
|
108
|
+
(0, cdpBridgeManager_1.registerApprovalWorkspaceChannel)(deps.bridge, projectName, message.channel);
|
|
109
|
+
// Always push ModeService's mode to Antigravity on CDP connect.
|
|
110
|
+
// ModeService is the source of truth (what the user sees in /mode UI).
|
|
111
|
+
// Without this, Antigravity could be in a different mode (e.g. Planning)
|
|
112
|
+
// while the user believes they're in Fast mode.
|
|
113
|
+
if (deps.modeService) {
|
|
114
|
+
const currentMode = deps.modeService.getCurrentMode();
|
|
115
|
+
const syncRes = await cdp.setUiMode(currentMode);
|
|
116
|
+
if (syncRes.ok) {
|
|
117
|
+
deps.modeService.markSynced();
|
|
118
|
+
logger_1.logger.debug(`[TelegramHandler] Mode pushed to Antigravity: ${currentMode}`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
logger_1.logger.warn(`[TelegramHandler] Mode push failed: ${syncRes.error}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Apply default model preference on CDP connect
|
|
125
|
+
if (deps.modelService) {
|
|
126
|
+
const modelResult = await (0, defaultModelApplicator_1.applyDefaultModel)(cdp, deps.modelService);
|
|
127
|
+
if (modelResult.stale && modelResult.staleMessage) {
|
|
128
|
+
await message.reply({ text: modelResult.staleMessage }).catch(logger_1.logger.error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Start detectors (platform-agnostic now)
|
|
132
|
+
(0, cdpBridgeManager_1.ensureApprovalDetector)(deps.bridge, cdp, projectName);
|
|
133
|
+
(0, cdpBridgeManager_1.ensureErrorPopupDetector)(deps.bridge, cdp, projectName);
|
|
134
|
+
(0, cdpBridgeManager_1.ensurePlanningDetector)(deps.bridge, cdp, projectName);
|
|
135
|
+
// Acknowledge receipt
|
|
136
|
+
await message.react('\u{1F440}').catch(() => { });
|
|
137
|
+
// Download image attachments if present
|
|
138
|
+
let inboundImages = [];
|
|
139
|
+
if (hasImageAttachments && deps.botToken && deps.botApi) {
|
|
140
|
+
try {
|
|
141
|
+
inboundImages = await (0, telegramImageHandler_1.downloadTelegramPhotos)(message.attachments, deps.botToken, deps.botApi);
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
logger_1.logger.warn('[TelegramHandler] Image download failed:', err?.message || err);
|
|
145
|
+
}
|
|
146
|
+
if (hasImageAttachments && inboundImages.length === 0) {
|
|
147
|
+
await message.reply({
|
|
148
|
+
text: 'Failed to retrieve attached images. Please wait and try again.',
|
|
149
|
+
}).catch(logger_1.logger.error);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Determine the prompt text — use default for image-only messages
|
|
154
|
+
const effectivePrompt = promptText || 'Please review the attached images and respond accordingly.';
|
|
155
|
+
// Inject prompt (with or without images) into Antigravity
|
|
156
|
+
logger_1.logger.prompt(effectivePrompt);
|
|
157
|
+
let injectResult;
|
|
158
|
+
try {
|
|
159
|
+
if (inboundImages.length > 0) {
|
|
160
|
+
injectResult = await cdp.injectMessageWithImageFiles(effectivePrompt, inboundImages.map((img) => img.localPath));
|
|
161
|
+
if (!injectResult.ok) {
|
|
162
|
+
// Fallback: send text-only with image reference
|
|
163
|
+
logger_1.logger.warn('[TelegramHandler] Image injection failed, falling back to text-only');
|
|
164
|
+
injectResult = await cdp.injectMessage(effectivePrompt);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
injectResult = await cdp.injectMessage(effectivePrompt);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
// Cleanup temp files regardless of outcome
|
|
173
|
+
if (inboundImages.length > 0) {
|
|
174
|
+
await (0, imageHandler_1.cleanupInboundImageAttachments)(inboundImages).catch(() => { });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (!injectResult.ok) {
|
|
178
|
+
await message.reply({
|
|
179
|
+
text: `Failed to send message: ${injectResult.error}`,
|
|
180
|
+
}).catch(logger_1.logger.error);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// Monitor the response
|
|
184
|
+
const channel = message.channel;
|
|
185
|
+
const startTime = Date.now();
|
|
186
|
+
const processLogBuffer = new processLogBuffer_1.ProcessLogBuffer({ maxChars: 3500, maxEntries: 120, maxEntryLength: 220 });
|
|
187
|
+
let lastActivityLogText = '';
|
|
188
|
+
let statusMsg = null;
|
|
189
|
+
// Send initial status message
|
|
190
|
+
statusMsg = await channel.send({ text: 'Processing...' }).catch(() => null);
|
|
191
|
+
await new Promise((resolve) => {
|
|
192
|
+
const TIMEOUT_MS = 300_000;
|
|
193
|
+
let settled = false;
|
|
194
|
+
const settle = () => {
|
|
195
|
+
if (settled)
|
|
196
|
+
return;
|
|
197
|
+
settled = true;
|
|
198
|
+
clearTimeout(safetyTimer);
|
|
199
|
+
deps.activeMonitors?.delete(projectName);
|
|
200
|
+
resolve();
|
|
201
|
+
};
|
|
202
|
+
const monitor = new responseMonitor_1.ResponseMonitor({
|
|
203
|
+
cdpService: cdp,
|
|
204
|
+
pollIntervalMs: 2000,
|
|
205
|
+
maxDurationMs: TIMEOUT_MS,
|
|
206
|
+
stopGoneConfirmCount: 3,
|
|
207
|
+
extractionMode: deps.extractionMode,
|
|
208
|
+
onProcessLog: (logText) => {
|
|
209
|
+
if (logText && logText.trim().length > 0) {
|
|
210
|
+
lastActivityLogText = processLogBuffer.append(logText);
|
|
211
|
+
}
|
|
212
|
+
if (statusMsg && lastActivityLogText) {
|
|
213
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
214
|
+
statusMsg.edit({
|
|
215
|
+
text: `${lastActivityLogText}\n\n⏱️ ${elapsed}s`,
|
|
216
|
+
}).catch(() => { });
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
onComplete: async (finalText) => {
|
|
220
|
+
try {
|
|
221
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
222
|
+
// Console log output (mirroring Discord handler pattern)
|
|
223
|
+
const finalLogText = lastActivityLogText || processLogBuffer.snapshot();
|
|
224
|
+
if (finalLogText && finalLogText.trim().length > 0) {
|
|
225
|
+
logger_1.logger.divider('Process Log');
|
|
226
|
+
console.info(finalLogText);
|
|
227
|
+
}
|
|
228
|
+
const separated = (0, discordFormatter_1.splitOutputAndLogs)(finalText || '');
|
|
229
|
+
const finalOutputText = separated.output || finalText || '';
|
|
230
|
+
if (finalOutputText && finalOutputText.trim().length > 0) {
|
|
231
|
+
logger_1.logger.divider(`Output (${finalOutputText.length} chars)`);
|
|
232
|
+
console.info(finalOutputText);
|
|
233
|
+
}
|
|
234
|
+
logger_1.logger.divider();
|
|
235
|
+
// Update status message with final activity log
|
|
236
|
+
if (statusMsg && finalLogText && finalLogText.trim().length > 0) {
|
|
237
|
+
await statusMsg.edit({
|
|
238
|
+
text: `${finalLogText}\n\n✅ Done in ${elapsed}s`,
|
|
239
|
+
}).catch(() => { });
|
|
240
|
+
}
|
|
241
|
+
else if (statusMsg) {
|
|
242
|
+
await statusMsg.delete().catch(() => { });
|
|
243
|
+
}
|
|
244
|
+
// Send the final response
|
|
245
|
+
if (finalOutputText && finalOutputText.trim().length > 0) {
|
|
246
|
+
await sendTextChunked(channel, finalOutputText);
|
|
247
|
+
}
|
|
248
|
+
else if (finalText && finalText.trim().length > 0) {
|
|
249
|
+
await sendTextChunked(channel, finalText);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
await channel.send({ text: '(Empty response from Antigravity)' }).catch(logger_1.logger.error);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
settle();
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
onTimeout: async (lastText) => {
|
|
260
|
+
try {
|
|
261
|
+
// Update status message on timeout
|
|
262
|
+
if (statusMsg) {
|
|
263
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
264
|
+
await statusMsg.edit({
|
|
265
|
+
text: `⏰ Timed out after ${elapsed}s`,
|
|
266
|
+
}).catch(() => { });
|
|
267
|
+
}
|
|
268
|
+
if (lastText && lastText.trim().length > 0) {
|
|
269
|
+
await sendTextChunked(channel, `(Timeout) ${lastText}`);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
await channel.send({ text: 'Response timed out.' }).catch(logger_1.logger.error);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
settle();
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
const safetyTimer = setTimeout(() => {
|
|
281
|
+
logger_1.logger.warn(`[TelegramHandler:${projectName}] Safety timeout — releasing queue after 300s`);
|
|
282
|
+
monitor.stop().catch(() => { });
|
|
283
|
+
settle();
|
|
284
|
+
}, TIMEOUT_MS);
|
|
285
|
+
// Register the monitor so /stop can access and stop it
|
|
286
|
+
deps.activeMonitors?.set(projectName, monitor);
|
|
287
|
+
monitor.start().catch((err) => {
|
|
288
|
+
logger_1.logger.error(`[TelegramHandler:${projectName}] monitor.start() failed:`, err?.message || err);
|
|
289
|
+
settle();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/** Split long text into Telegram-safe chunks (max 4096 chars). */
|
|
296
|
+
async function sendTextChunked(channel, text) {
|
|
297
|
+
const MAX_LENGTH = 4096;
|
|
298
|
+
let remaining = text;
|
|
299
|
+
while (remaining.length > 0) {
|
|
300
|
+
const chunk = remaining.slice(0, MAX_LENGTH);
|
|
301
|
+
remaining = remaining.slice(MAX_LENGTH);
|
|
302
|
+
await channel.send({ text: chunk }).catch(logger_1.logger.error);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Telegram /project command handler.
|
|
4
|
+
*
|
|
5
|
+
* Allows users to bind a Telegram chat to an Antigravity workspace
|
|
6
|
+
* via inline keyboard buttons, similar to Discord's /project slash command.
|
|
7
|
+
*
|
|
8
|
+
* User flow:
|
|
9
|
+
* /project → show workspace list as buttons → user taps → chat bound
|
|
10
|
+
* /project list → show workspace list (same as bare /project)
|
|
11
|
+
* /project unbind → remove current binding
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.TG_PROJECT_SELECT_ID = void 0;
|
|
15
|
+
exports.parseTelegramProjectCommand = parseTelegramProjectCommand;
|
|
16
|
+
exports.handleTelegramProjectCommand = handleTelegramProjectCommand;
|
|
17
|
+
exports.handleTelegramProjectSelect = handleTelegramProjectSelect;
|
|
18
|
+
exports.createTelegramSelectHandler = createTelegramSelectHandler;
|
|
19
|
+
const logger_1 = require("../utils/logger");
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Constants
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
exports.TG_PROJECT_SELECT_ID = 'tg_project_select';
|
|
24
|
+
/**
|
|
25
|
+
* Parse a Telegram message text for the /project command.
|
|
26
|
+
* Returns null if the text is not a /project command.
|
|
27
|
+
*
|
|
28
|
+
* Accepted formats:
|
|
29
|
+
* /project
|
|
30
|
+
* /project list
|
|
31
|
+
* /project unbind
|
|
32
|
+
* /project@BotName
|
|
33
|
+
* /project@BotName list
|
|
34
|
+
*/
|
|
35
|
+
function parseTelegramProjectCommand(text) {
|
|
36
|
+
const trimmed = text.trim();
|
|
37
|
+
// Match /project optionally followed by @BotName and an optional subcommand
|
|
38
|
+
const match = trimmed.match(/^\/project(?:@\S+)?(?:\s+(\S+))?$/i);
|
|
39
|
+
if (!match)
|
|
40
|
+
return null;
|
|
41
|
+
const sub = match[1]?.toLowerCase();
|
|
42
|
+
if (sub === 'unbind') {
|
|
43
|
+
return { subcommand: 'unbind' };
|
|
44
|
+
}
|
|
45
|
+
// Default (no subcommand or "list") → show workspace list
|
|
46
|
+
return { subcommand: 'list' };
|
|
47
|
+
}
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Command handler
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
/**
|
|
52
|
+
* Handle a /project command from Telegram.
|
|
53
|
+
*/
|
|
54
|
+
async function handleTelegramProjectCommand(deps, message, parsed) {
|
|
55
|
+
const chatId = message.channel.id;
|
|
56
|
+
if (parsed.subcommand === 'unbind') {
|
|
57
|
+
const deleted = deps.telegramBindingRepo.deleteByChatId(chatId);
|
|
58
|
+
if (deleted) {
|
|
59
|
+
await message.reply({ text: 'Workspace binding removed.' }).catch(logger_1.logger.error);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
await message.reply({ text: 'No workspace is bound to this chat.' }).catch(logger_1.logger.error);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// subcommand === 'list'
|
|
67
|
+
const workspaces = deps.workspaceService.scanWorkspaces();
|
|
68
|
+
if (workspaces.length === 0) {
|
|
69
|
+
await message.reply({
|
|
70
|
+
text: 'No workspaces found. Create a workspace directory first.',
|
|
71
|
+
}).catch(logger_1.logger.error);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const currentBinding = deps.telegramBindingRepo.findByChatId(chatId);
|
|
75
|
+
const currentPath = currentBinding?.workspacePath;
|
|
76
|
+
const selectMenu = {
|
|
77
|
+
type: 'selectMenu',
|
|
78
|
+
customId: exports.TG_PROJECT_SELECT_ID,
|
|
79
|
+
placeholder: 'Select a workspace',
|
|
80
|
+
options: workspaces.map((name) => ({
|
|
81
|
+
label: name === currentPath ? `${name} (current)` : name,
|
|
82
|
+
value: name,
|
|
83
|
+
})),
|
|
84
|
+
};
|
|
85
|
+
const header = currentPath
|
|
86
|
+
? `Current workspace: <b>${currentPath}</b>\nSelect a workspace to switch:`
|
|
87
|
+
: 'Select a workspace to bind to this chat:';
|
|
88
|
+
await message.reply({
|
|
89
|
+
text: header,
|
|
90
|
+
components: [{ components: [selectMenu] }],
|
|
91
|
+
}).catch(logger_1.logger.error);
|
|
92
|
+
}
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Select interaction handler
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
/**
|
|
97
|
+
* Handle a workspace selection callback from inline keyboard.
|
|
98
|
+
*/
|
|
99
|
+
async function handleTelegramProjectSelect(deps, interaction) {
|
|
100
|
+
const selectedWorkspace = interaction.values[0];
|
|
101
|
+
if (!selectedWorkspace)
|
|
102
|
+
return;
|
|
103
|
+
const chatId = interaction.channel.id;
|
|
104
|
+
// Validate workspace exists
|
|
105
|
+
const workspaces = deps.workspaceService.scanWorkspaces();
|
|
106
|
+
if (!workspaces.includes(selectedWorkspace)) {
|
|
107
|
+
await interaction.reply({
|
|
108
|
+
text: `Workspace "${selectedWorkspace}" not found.`,
|
|
109
|
+
}).catch(logger_1.logger.error);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
deps.telegramBindingRepo.upsert({
|
|
113
|
+
chatId,
|
|
114
|
+
workspacePath: selectedWorkspace,
|
|
115
|
+
});
|
|
116
|
+
await interaction.update({
|
|
117
|
+
text: `Workspace bound: <b>${selectedWorkspace}</b>\nSend a message to start chatting with Antigravity.`,
|
|
118
|
+
}).catch(logger_1.logger.error);
|
|
119
|
+
logger_1.logger.info(`[TelegramProject] Chat ${chatId} bound to workspace: ${selectedWorkspace}`);
|
|
120
|
+
}
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Factory
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
/**
|
|
125
|
+
* Create a select interaction handler that routes by customId.
|
|
126
|
+
* Returns a function suitable for EventRouter's onSelectInteraction.
|
|
127
|
+
*/
|
|
128
|
+
function createTelegramSelectHandler(deps) {
|
|
129
|
+
return async (interaction) => {
|
|
130
|
+
if (interaction.customId === exports.TG_PROJECT_SELECT_ID) {
|
|
131
|
+
await handleTelegramProjectSelect(deps, interaction);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// Unknown select interaction — ignore
|
|
135
|
+
logger_1.logger.debug(`[TelegramSelect] Unhandled customId: ${interaction.customId}`);
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WorkspaceQueue = void 0;
|
|
4
|
+
const logger_1 = require("../utils/logger");
|
|
5
|
+
/**
|
|
6
|
+
* Per-workspace prompt queue.
|
|
7
|
+
* Serializes tasks per workspace path to prevent concurrent sends
|
|
8
|
+
* to the same Antigravity workspace.
|
|
9
|
+
*/
|
|
10
|
+
class WorkspaceQueue {
|
|
11
|
+
queues = new Map();
|
|
12
|
+
depths = new Map();
|
|
13
|
+
/**
|
|
14
|
+
* Enqueue a task for a given workspace. Tasks for the same workspace
|
|
15
|
+
* execute serially; tasks for different workspaces run concurrently.
|
|
16
|
+
*/
|
|
17
|
+
enqueue(workspacePath, task) {
|
|
18
|
+
// .catch: ensure a prior rejection never stalls the chain
|
|
19
|
+
const current = (this.queues.get(workspacePath) ?? Promise.resolve()).catch(() => { });
|
|
20
|
+
const next = current.then(async () => {
|
|
21
|
+
try {
|
|
22
|
+
await task();
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
logger_1.logger.error('[WorkspaceQueue] task error:', err?.message || err);
|
|
26
|
+
}
|
|
27
|
+
}).finally(() => {
|
|
28
|
+
// Clean up if this is still the latest promise in the chain
|
|
29
|
+
if (this.queues.get(workspacePath) === next) {
|
|
30
|
+
this.queues.delete(workspacePath);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
this.queues.set(workspacePath, next);
|
|
34
|
+
return next;
|
|
35
|
+
}
|
|
36
|
+
/** Get current queue depth for a workspace. */
|
|
37
|
+
getDepth(workspacePath) {
|
|
38
|
+
return this.depths.get(workspacePath) ?? 0;
|
|
39
|
+
}
|
|
40
|
+
/** Increment queue depth. Returns the new depth. */
|
|
41
|
+
incrementDepth(workspacePath) {
|
|
42
|
+
const current = this.depths.get(workspacePath) ?? 0;
|
|
43
|
+
const next = current + 1;
|
|
44
|
+
this.depths.set(workspacePath, next);
|
|
45
|
+
return next;
|
|
46
|
+
}
|
|
47
|
+
/** Decrement queue depth. Returns the new depth (min 0). Cleans up Map entries when depth reaches 0. */
|
|
48
|
+
decrementDepth(workspacePath) {
|
|
49
|
+
const current = this.depths.get(workspacePath) ?? 1;
|
|
50
|
+
const next = Math.max(0, current - 1);
|
|
51
|
+
if (next === 0) {
|
|
52
|
+
this.depths.delete(workspacePath);
|
|
53
|
+
this.queues.delete(workspacePath);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
this.depths.set(workspacePath, next);
|
|
57
|
+
}
|
|
58
|
+
return next;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.WorkspaceQueue = WorkspaceQueue;
|
|
@@ -23,9 +23,10 @@ class JoinCommandHandler {
|
|
|
23
23
|
pool;
|
|
24
24
|
workspaceService;
|
|
25
25
|
client;
|
|
26
|
+
extractionMode;
|
|
26
27
|
/** Active ResponseMonitors per workspace (for AI response mirroring) */
|
|
27
28
|
activeResponseMonitors = new Map();
|
|
28
|
-
constructor(chatSessionService, chatSessionRepo, bindingRepo, channelManager, pool, workspaceService, client) {
|
|
29
|
+
constructor(chatSessionService, chatSessionRepo, bindingRepo, channelManager, pool, workspaceService, client, extractionMode) {
|
|
29
30
|
this.chatSessionService = chatSessionService;
|
|
30
31
|
this.chatSessionRepo = chatSessionRepo;
|
|
31
32
|
this.bindingRepo = bindingRepo;
|
|
@@ -33,6 +34,7 @@ class JoinCommandHandler {
|
|
|
33
34
|
this.pool = pool;
|
|
34
35
|
this.workspaceService = workspaceService;
|
|
35
36
|
this.client = client;
|
|
37
|
+
this.extractionMode = extractionMode;
|
|
36
38
|
}
|
|
37
39
|
/**
|
|
38
40
|
* Resolve a project name (from DB) to its full absolute path.
|
|
@@ -272,6 +274,7 @@ class JoinCommandHandler {
|
|
|
272
274
|
cdpService: cdp,
|
|
273
275
|
pollIntervalMs: 2000,
|
|
274
276
|
maxDurationMs: 300000,
|
|
277
|
+
extractionMode: this.extractionMode,
|
|
275
278
|
onComplete: (finalText) => {
|
|
276
279
|
this.activeResponseMonitors.delete(projectName);
|
|
277
280
|
if (!finalText || finalText.trim().length === 0)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TelegramBindingRepository = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Repository for persisting Telegram chat to workspace directory bindings in SQLite.
|
|
6
|
+
* Only one workspace can be bound per chat (UNIQUE constraint).
|
|
7
|
+
*/
|
|
8
|
+
class TelegramBindingRepository {
|
|
9
|
+
db;
|
|
10
|
+
constructor(db) {
|
|
11
|
+
this.db = db;
|
|
12
|
+
this.initialize();
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Initialize table (create if not exists)
|
|
16
|
+
*/
|
|
17
|
+
initialize() {
|
|
18
|
+
this.db.exec(`
|
|
19
|
+
CREATE TABLE IF NOT EXISTS telegram_bindings (
|
|
20
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
21
|
+
chat_id TEXT NOT NULL UNIQUE,
|
|
22
|
+
workspace_path TEXT NOT NULL,
|
|
23
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
24
|
+
)
|
|
25
|
+
`);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Create a new binding
|
|
29
|
+
*/
|
|
30
|
+
create(input) {
|
|
31
|
+
const stmt = this.db.prepare(`
|
|
32
|
+
INSERT INTO telegram_bindings (chat_id, workspace_path)
|
|
33
|
+
VALUES (?, ?)
|
|
34
|
+
`);
|
|
35
|
+
const result = stmt.run(input.chatId, input.workspacePath);
|
|
36
|
+
return {
|
|
37
|
+
id: result.lastInsertRowid,
|
|
38
|
+
chatId: input.chatId,
|
|
39
|
+
workspacePath: input.workspacePath,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Find binding by chat ID
|
|
44
|
+
*/
|
|
45
|
+
findByChatId(chatId) {
|
|
46
|
+
const row = this.db.prepare('SELECT * FROM telegram_bindings WHERE chat_id = ?').get(chatId);
|
|
47
|
+
if (!row)
|
|
48
|
+
return undefined;
|
|
49
|
+
return this.mapRow(row);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Find bindings by workspace path
|
|
53
|
+
*/
|
|
54
|
+
findByWorkspacePath(workspacePath) {
|
|
55
|
+
const rows = this.db.prepare('SELECT * FROM telegram_bindings WHERE workspace_path = ? ORDER BY id ASC').all(workspacePath);
|
|
56
|
+
return rows.map(this.mapRow);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get all bindings
|
|
60
|
+
*/
|
|
61
|
+
findAll() {
|
|
62
|
+
const rows = this.db.prepare('SELECT * FROM telegram_bindings ORDER BY id ASC').all();
|
|
63
|
+
return rows.map(this.mapRow);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Delete binding by chat ID
|
|
67
|
+
*/
|
|
68
|
+
deleteByChatId(chatId) {
|
|
69
|
+
const result = this.db.prepare('DELETE FROM telegram_bindings WHERE chat_id = ?').run(chatId);
|
|
70
|
+
return result.changes > 0;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Create or update a chat binding (upsert)
|
|
74
|
+
*/
|
|
75
|
+
upsert(input) {
|
|
76
|
+
const stmt = this.db.prepare(`
|
|
77
|
+
INSERT INTO telegram_bindings (chat_id, workspace_path)
|
|
78
|
+
VALUES (?, ?)
|
|
79
|
+
ON CONFLICT(chat_id) DO UPDATE SET
|
|
80
|
+
workspace_path = excluded.workspace_path
|
|
81
|
+
`);
|
|
82
|
+
stmt.run(input.chatId, input.workspacePath);
|
|
83
|
+
return this.findByChatId(input.chatId);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Map a DB row to TelegramBindingRecord
|
|
87
|
+
*/
|
|
88
|
+
mapRow(row) {
|
|
89
|
+
return {
|
|
90
|
+
id: row.id,
|
|
91
|
+
chatId: row.chat_id,
|
|
92
|
+
workspacePath: row.workspace_path,
|
|
93
|
+
createdAt: row.created_at,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
exports.TelegramBindingRepository = TelegramBindingRepository;
|