lazy-gravity 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +76 -15
  2. package/dist/bin/commands/doctor.js +19 -2
  3. package/dist/bin/commands/setup.js +286 -70
  4. package/dist/bot/eventRouter.js +70 -0
  5. package/dist/bot/index.js +353 -147
  6. package/dist/bot/telegramCommands.js +428 -0
  7. package/dist/bot/telegramMessageHandler.js +304 -0
  8. package/dist/bot/telegramProjectCommand.js +137 -0
  9. package/dist/bot/workspaceQueue.js +61 -0
  10. package/dist/commands/joinCommandHandler.js +4 -1
  11. package/dist/database/telegramBindingRepository.js +97 -0
  12. package/dist/database/userPreferenceRepository.js +46 -1
  13. package/dist/events/interactionCreateHandler.js +36 -0
  14. package/dist/events/messageCreateHandler.js +11 -7
  15. package/dist/handlers/approvalButtonAction.js +99 -0
  16. package/dist/handlers/autoAcceptButtonAction.js +43 -0
  17. package/dist/handlers/buttonHandler.js +55 -0
  18. package/dist/handlers/commandHandler.js +44 -0
  19. package/dist/handlers/errorPopupButtonAction.js +137 -0
  20. package/dist/handlers/messageHandler.js +70 -0
  21. package/dist/handlers/modeSelectAction.js +63 -0
  22. package/dist/handlers/modelButtonAction.js +102 -0
  23. package/dist/handlers/planningButtonAction.js +118 -0
  24. package/dist/handlers/selectHandler.js +41 -0
  25. package/dist/handlers/templateButtonAction.js +54 -0
  26. package/dist/platform/adapter.js +8 -0
  27. package/dist/platform/discord/discordAdapter.js +99 -0
  28. package/dist/platform/discord/index.js +15 -0
  29. package/dist/platform/discord/wrappers.js +331 -0
  30. package/dist/platform/index.js +18 -0
  31. package/dist/platform/richContentBuilder.js +76 -0
  32. package/dist/platform/telegram/index.js +16 -0
  33. package/dist/platform/telegram/telegramAdapter.js +195 -0
  34. package/dist/platform/telegram/telegramFormatter.js +134 -0
  35. package/dist/platform/telegram/wrappers.js +329 -0
  36. package/dist/platform/types.js +28 -0
  37. package/dist/services/approvalDetector.js +15 -2
  38. package/dist/services/cdpBridgeManager.js +91 -146
  39. package/dist/services/defaultModelApplicator.js +54 -0
  40. package/dist/services/modeService.js +16 -1
  41. package/dist/services/modelService.js +57 -16
  42. package/dist/services/notificationSender.js +149 -0
  43. package/dist/services/responseMonitor.js +1 -2
  44. package/dist/ui/autoAcceptUi.js +37 -0
  45. package/dist/ui/modeUi.js +38 -1
  46. package/dist/ui/modelsUi.js +96 -0
  47. package/dist/ui/outputUi.js +32 -0
  48. package/dist/ui/projectListUi.js +55 -0
  49. package/dist/ui/screenshotUi.js +26 -0
  50. package/dist/ui/sessionPickerUi.js +35 -1
  51. package/dist/ui/templateUi.js +41 -0
  52. package/dist/utils/configLoader.js +63 -12
  53. package/dist/utils/lockfile.js +5 -5
  54. package/dist/utils/logger.js +7 -0
  55. package/dist/utils/telegramImageHandler.js +127 -0
  56. package/package.json +4 -2
@@ -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;