lazy-gravity 0.0.2 → 0.0.3

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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +224 -0
  3. package/dist/bin/cli.js +79 -0
  4. package/dist/bin/commands/doctor.js +156 -0
  5. package/dist/bin/commands/open.js +145 -0
  6. package/dist/bin/commands/setup.js +366 -0
  7. package/dist/bin/commands/start.js +15 -0
  8. package/dist/bot/index.js +914 -0
  9. package/dist/commands/chatCommandHandler.js +145 -0
  10. package/dist/commands/cleanupCommandHandler.js +396 -0
  11. package/dist/commands/messageParser.js +28 -0
  12. package/dist/commands/registerSlashCommands.js +149 -0
  13. package/dist/commands/slashCommandHandler.js +104 -0
  14. package/dist/commands/workspaceCommandHandler.js +230 -0
  15. package/dist/database/chatSessionRepository.js +88 -0
  16. package/dist/database/scheduleRepository.js +119 -0
  17. package/dist/database/templateRepository.js +103 -0
  18. package/dist/database/workspaceBindingRepository.js +109 -0
  19. package/dist/events/interactionCreateHandler.js +286 -0
  20. package/dist/events/messageCreateHandler.js +154 -0
  21. package/dist/index.js +10 -0
  22. package/dist/middleware/auth.js +10 -0
  23. package/dist/middleware/sanitize.js +20 -0
  24. package/dist/services/antigravityLauncher.js +89 -0
  25. package/dist/services/approvalDetector.js +384 -0
  26. package/dist/services/autoAcceptService.js +80 -0
  27. package/dist/services/cdpBridgeManager.js +204 -0
  28. package/dist/services/cdpConnectionPool.js +157 -0
  29. package/dist/services/cdpService.js +1311 -0
  30. package/dist/services/channelManager.js +118 -0
  31. package/dist/services/chatSessionService.js +516 -0
  32. package/dist/services/modeService.js +73 -0
  33. package/dist/services/modelService.js +63 -0
  34. package/dist/services/processManager.js +61 -0
  35. package/dist/services/progressSender.js +61 -0
  36. package/dist/services/promptDispatcher.js +17 -0
  37. package/dist/services/quotaService.js +185 -0
  38. package/dist/services/responseMonitor.js +645 -0
  39. package/dist/services/scheduleService.js +134 -0
  40. package/dist/services/screenshotService.js +85 -0
  41. package/dist/services/titleGeneratorService.js +113 -0
  42. package/dist/services/workspaceService.js +64 -0
  43. package/dist/ui/autoAcceptUi.js +34 -0
  44. package/dist/ui/modeUi.js +34 -0
  45. package/dist/ui/modelsUi.js +97 -0
  46. package/dist/ui/screenshotUi.js +51 -0
  47. package/dist/ui/templateUi.js +67 -0
  48. package/dist/utils/cdpPorts.js +5 -0
  49. package/dist/utils/config.js +20 -0
  50. package/dist/utils/configLoader.js +160 -0
  51. package/dist/utils/discordFormatter.js +167 -0
  52. package/dist/utils/i18n.js +77 -0
  53. package/dist/utils/imageHandler.js +154 -0
  54. package/dist/utils/lockfile.js +113 -0
  55. package/dist/utils/logger.js +32 -0
  56. package/dist/utils/logo.js +13 -0
  57. package/dist/utils/metadataExtractor.js +15 -0
  58. package/dist/utils/processLogBuffer.js +98 -0
  59. package/dist/utils/streamMessageFormatter.js +90 -0
  60. package/package.json +73 -5
@@ -0,0 +1,914 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.startBot = exports.getResponseDeliveryModeForTest = void 0;
7
+ exports.createSerialTaskQueueForTest = createSerialTaskQueueForTest;
8
+ const i18n_1 = require("../utils/i18n");
9
+ const logger_1 = require("../utils/logger");
10
+ const discord_js_1 = require("discord.js");
11
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
12
+ const config_1 = require("../utils/config");
13
+ const slashCommandHandler_1 = require("../commands/slashCommandHandler");
14
+ const registerSlashCommands_1 = require("../commands/registerSlashCommands");
15
+ const modeService_1 = require("../services/modeService");
16
+ const modelService_1 = require("../services/modelService");
17
+ const templateRepository_1 = require("../database/templateRepository");
18
+ const workspaceBindingRepository_1 = require("../database/workspaceBindingRepository");
19
+ const chatSessionRepository_1 = require("../database/chatSessionRepository");
20
+ const workspaceService_1 = require("../services/workspaceService");
21
+ const workspaceCommandHandler_1 = require("../commands/workspaceCommandHandler");
22
+ const chatCommandHandler_1 = require("../commands/chatCommandHandler");
23
+ const cleanupCommandHandler_1 = require("../commands/cleanupCommandHandler");
24
+ const channelManager_1 = require("../services/channelManager");
25
+ const titleGeneratorService_1 = require("../services/titleGeneratorService");
26
+ const chatSessionService_1 = require("../services/chatSessionService");
27
+ const responseMonitor_1 = require("../services/responseMonitor");
28
+ const antigravityLauncher_1 = require("../services/antigravityLauncher");
29
+ const promptDispatcher_1 = require("../services/promptDispatcher");
30
+ const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
31
+ const streamMessageFormatter_1 = require("../utils/streamMessageFormatter");
32
+ const discordFormatter_1 = require("../utils/discordFormatter");
33
+ const processLogBuffer_1 = require("../utils/processLogBuffer");
34
+ const imageHandler_1 = require("../utils/imageHandler");
35
+ const modeUi_1 = require("../ui/modeUi");
36
+ const modelsUi_1 = require("../ui/modelsUi");
37
+ const templateUi_1 = require("../ui/templateUi");
38
+ const autoAcceptUi_1 = require("../ui/autoAcceptUi");
39
+ const screenshotUi_1 = require("../ui/screenshotUi");
40
+ const interactionCreateHandler_1 = require("../events/interactionCreateHandler");
41
+ const messageCreateHandler_1 = require("../events/messageCreateHandler");
42
+ // =============================================================================
43
+ // Embed color palette (color-coded by phase)
44
+ // =============================================================================
45
+ const PHASE_COLORS = {
46
+ sending: 0x5865F2, // Blue
47
+ thinking: 0x9B59B6, // Purple
48
+ generating: 0xF39C12, // Gold
49
+ complete: 0x2ECC71, // Green
50
+ timeout: 0xE74C3C, // Red
51
+ error: 0xC0392B, // Dark Red
52
+ };
53
+ const PHASE_ICONS = {
54
+ sending: '📡',
55
+ thinking: '🧠',
56
+ generating: '✍️',
57
+ complete: '✅',
58
+ timeout: '⏰',
59
+ error: '❌',
60
+ };
61
+ const MAX_OUTBOUND_GENERATED_IMAGES = 4;
62
+ const RESPONSE_DELIVERY_MODE = (0, config_1.resolveResponseDeliveryMode)();
63
+ const getResponseDeliveryModeForTest = () => RESPONSE_DELIVERY_MODE;
64
+ exports.getResponseDeliveryModeForTest = getResponseDeliveryModeForTest;
65
+ function createSerialTaskQueueForTest(queueName, traceId) {
66
+ let queue = Promise.resolve();
67
+ let queueDepth = 0;
68
+ let taskSeq = 0;
69
+ return (task, label = 'queue-task') => {
70
+ taskSeq += 1;
71
+ const seq = taskSeq;
72
+ queueDepth += 1;
73
+ queue = queue.then(async () => {
74
+ try {
75
+ await task();
76
+ }
77
+ catch (err) {
78
+ logger_1.logger.error(`[sendQueue:${traceId}:${queueName}] error #${seq} label=${label}:`, err?.message || err);
79
+ }
80
+ finally {
81
+ queueDepth = Math.max(0, queueDepth - 1);
82
+ }
83
+ });
84
+ return queue;
85
+ };
86
+ }
87
+ /**
88
+ * Send a Discord message (prompt) to Antigravity, wait for the response, and relay it back to Discord
89
+ *
90
+ * Message strategy:
91
+ * - Send new messages per phase instead of editing, to preserve history
92
+ * - Visualize the flow of planning/analysis/execution confirmation/implementation as logs
93
+ */
94
+ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService, modelService, inboundImages = [], options) {
95
+ // Add reaction to acknowledge command receipt
96
+ await message.react('👀').catch(() => { });
97
+ const channel = (message.channel && 'send' in message.channel) ? message.channel : null;
98
+ const monitorTraceId = `${message.channelId}:${message.id}`;
99
+ const enqueueGeneral = createSerialTaskQueueForTest('general', monitorTraceId);
100
+ const enqueueResponse = createSerialTaskQueueForTest('response', monitorTraceId);
101
+ const enqueueActivity = createSerialTaskQueueForTest('activity', monitorTraceId);
102
+ const sendEmbed = (title, description, color, fields, footerText) => enqueueGeneral(async () => {
103
+ if (!channel)
104
+ return;
105
+ const embed = new discord_js_1.EmbedBuilder()
106
+ .setTitle(title)
107
+ .setDescription(description)
108
+ .setColor(color)
109
+ .setTimestamp();
110
+ if (fields && fields.length > 0) {
111
+ embed.addFields(...fields);
112
+ }
113
+ if (footerText) {
114
+ embed.setFooter({ text: footerText });
115
+ }
116
+ await channel.send({ embeds: [embed] }).catch(() => { });
117
+ }, 'send-embed');
118
+ const shouldTryGeneratedImages = (inputPrompt, responseText) => {
119
+ const prompt = (inputPrompt || '').toLowerCase();
120
+ const response = (responseText || '').toLowerCase();
121
+ const imageIntentPattern = /(image|images|png|jpg|jpeg|gif|webp|illustration|diagram|render)/i;
122
+ const imageUrlPattern = /https?:\/\/\S+\.(png|jpg|jpeg|gif|webp)/i;
123
+ if (imageIntentPattern.test(prompt))
124
+ return true;
125
+ if (response.includes('![') || imageUrlPattern.test(response))
126
+ return true;
127
+ return false;
128
+ };
129
+ const sendGeneratedImages = async (responseText) => {
130
+ if (!channel)
131
+ return;
132
+ if (!shouldTryGeneratedImages(prompt, responseText))
133
+ return;
134
+ const extracted = await cdp.extractLatestResponseImages(MAX_OUTBOUND_GENERATED_IMAGES);
135
+ if (extracted.length === 0)
136
+ return;
137
+ const files = [];
138
+ for (let i = 0; i < extracted.length; i++) {
139
+ const attachment = await (0, imageHandler_1.toDiscordAttachment)(extracted[i], i);
140
+ if (attachment)
141
+ files.push(attachment);
142
+ }
143
+ if (files.length === 0)
144
+ return;
145
+ await enqueueGeneral(async () => {
146
+ await channel.send({
147
+ content: (0, i18n_1.t)(`🖼️ Detected generated images (${files.length})`),
148
+ files,
149
+ }).catch(() => { });
150
+ }, 'send-generated-images');
151
+ };
152
+ const tryEmergencyExtractText = async () => {
153
+ try {
154
+ const contextId = cdp.getPrimaryContextId();
155
+ const expression = `(() => {
156
+ const panel = document.querySelector('.antigravity-agent-side-panel');
157
+ const scope = panel || document;
158
+
159
+ const candidateSelectors = [
160
+ '.rendered-markdown',
161
+ '.leading-relaxed.select-text',
162
+ '.flex.flex-col.gap-y-3',
163
+ '[data-message-author-role="assistant"]',
164
+ '[data-message-role="assistant"]',
165
+ '[class*="assistant-message"]',
166
+ '[class*="message-content"]',
167
+ '[class*="markdown-body"]',
168
+ '.prose',
169
+ ];
170
+
171
+ const looksLikeActivity = (text) => {
172
+ const normalized = (text || '').trim().toLowerCase();
173
+ if (!normalized) return true;
174
+ const activityPattern = /^(?:analy[sz]ing|reading|writing|running|searching|planning|thinking|processing|loading|executing|testing|debugging|analyzed|read|wrote|ran)/i;
175
+ return activityPattern.test(normalized) && normalized.length <= 220;
176
+ };
177
+
178
+ const clean = (text) => (text || '').replace(/\\r/g, '').replace(/\\n{3,}/g, '\\n\\n').trim();
179
+
180
+ const candidates = [];
181
+ const seen = new Set();
182
+ for (const selector of candidateSelectors) {
183
+ const nodes = scope.querySelectorAll(selector);
184
+ for (const node of nodes) {
185
+ if (!node || seen.has(node)) continue;
186
+ seen.add(node);
187
+ candidates.push(node);
188
+ }
189
+ }
190
+
191
+ for (let i = candidates.length - 1; i >= 0; i--) {
192
+ const node = candidates[i];
193
+ const text = clean(node.innerText || node.textContent || '');
194
+ if (!text || text.length < 20) continue;
195
+ if (looksLikeActivity(text)) continue;
196
+ if (/^(good|bad)$/i.test(text)) continue;
197
+ return text;
198
+ }
199
+
200
+ return '';
201
+ })()`;
202
+ const callParams = {
203
+ expression,
204
+ returnByValue: true,
205
+ awaitPromise: true,
206
+ };
207
+ if (contextId !== null)
208
+ callParams.contextId = contextId;
209
+ const res = await cdp.call('Runtime.evaluate', callParams);
210
+ const value = res?.result?.value;
211
+ return typeof value === 'string' ? value.trim() : '';
212
+ }
213
+ catch {
214
+ return '';
215
+ }
216
+ };
217
+ const clearWatchingReaction = async () => {
218
+ const botId = message.client.user?.id;
219
+ if (botId) {
220
+ await message.reactions.resolve('👀')?.users.remove(botId).catch(() => { });
221
+ }
222
+ };
223
+ if (!cdp.isConnected()) {
224
+ await sendEmbed(`${PHASE_ICONS.error} Connection Error`, 'Not connected to Antigravity.\nStart with `open -a Antigravity --args --remote-debugging-port=9223`, then send a message to auto-connect.', PHASE_COLORS.error);
225
+ await clearWatchingReaction();
226
+ await message.react('❌').catch(() => { });
227
+ return;
228
+ }
229
+ const localMode = modeService.getCurrentMode();
230
+ const modeName = modeService_1.MODE_UI_NAMES[localMode] || localMode;
231
+ const currentModel = (await cdp.getCurrentModel()) || modelService.getCurrentModel();
232
+ const fastModel = currentModel;
233
+ const planModel = currentModel;
234
+ await sendEmbed(`${PHASE_ICONS.sending} [${modeName} - ${currentModel}${localMode === 'plan' ? ' (Thinking)' : ''}] Sending...`, (0, streamMessageFormatter_1.buildModeModelLines)(modeName, fastModel, planModel).join('\n'), PHASE_COLORS.sending);
235
+ let isFinalized = false;
236
+ let lastProgressText = '';
237
+ let lastActivityLogText = '';
238
+ const LIVE_RESPONSE_MAX_LEN = 3800;
239
+ const LIVE_ACTIVITY_MAX_LEN = 3800;
240
+ const processLogBuffer = new processLogBuffer_1.ProcessLogBuffer({
241
+ maxChars: LIVE_ACTIVITY_MAX_LEN,
242
+ maxEntries: 120,
243
+ maxEntryLength: 220,
244
+ });
245
+ const liveResponseMessages = [];
246
+ const liveActivityMessages = [];
247
+ let lastLiveResponseKey = '';
248
+ let lastLiveActivityKey = '';
249
+ let liveResponseUpdateVersion = 0;
250
+ let liveActivityUpdateVersion = 0;
251
+ const ACTIVITY_PLACEHOLDER = (0, i18n_1.t)('Collecting process logs...');
252
+ const buildLiveResponseDescriptions = (text) => {
253
+ const normalized = (text || '').trim();
254
+ if (!normalized) {
255
+ return [(0, i18n_1.t)('Waiting for output...')];
256
+ }
257
+ return (0, streamMessageFormatter_1.splitForEmbedDescription)((0, discordFormatter_1.formatForDiscord)(normalized), LIVE_RESPONSE_MAX_LEN);
258
+ };
259
+ const buildLiveActivityDescriptions = (text) => {
260
+ const normalized = (text || '').trim();
261
+ if (!normalized)
262
+ return [ACTIVITY_PLACEHOLDER];
263
+ const formatted = (0, discordFormatter_1.formatForDiscord)(normalized);
264
+ return [(0, streamMessageFormatter_1.fitForSingleEmbedDescription)(formatted, LIVE_ACTIVITY_MAX_LEN)];
265
+ };
266
+ const appendProcessLogs = (text) => {
267
+ const normalized = (text || '').trim();
268
+ if (!normalized)
269
+ return processLogBuffer.snapshot();
270
+ return processLogBuffer.append(normalized);
271
+ };
272
+ const upsertLiveResponseEmbeds = (title, rawText, color, footerText, opts) => enqueueResponse(async () => {
273
+ if (opts?.skipWhenFinalized && isFinalized)
274
+ return;
275
+ if (opts?.expectedVersion !== undefined && opts.expectedVersion !== liveResponseUpdateVersion)
276
+ return;
277
+ if (!channel)
278
+ return;
279
+ const descriptions = buildLiveResponseDescriptions(rawText);
280
+ const renderKey = `${title}|${color}|${footerText}|${descriptions.join('\n<<<PAGE_BREAK>>>\n')}`;
281
+ if (renderKey === lastLiveResponseKey && liveResponseMessages.length > 0) {
282
+ return;
283
+ }
284
+ lastLiveResponseKey = renderKey;
285
+ for (let i = 0; i < descriptions.length; i++) {
286
+ const embed = new discord_js_1.EmbedBuilder()
287
+ .setTitle(descriptions.length > 1 ? `${title} (${i + 1}/${descriptions.length})` : title)
288
+ .setDescription(descriptions[i])
289
+ .setColor(color)
290
+ .setFooter({ text: footerText })
291
+ .setTimestamp();
292
+ if (!liveResponseMessages[i]) {
293
+ liveResponseMessages[i] = await channel.send({ embeds: [embed] }).catch(() => null);
294
+ continue;
295
+ }
296
+ await liveResponseMessages[i].edit({ embeds: [embed] }).catch(async () => {
297
+ liveResponseMessages[i] = await channel.send({ embeds: [embed] }).catch(() => null);
298
+ });
299
+ }
300
+ // Delete excess messages if page count decreased
301
+ while (liveResponseMessages.length > descriptions.length) {
302
+ const extra = liveResponseMessages.pop();
303
+ if (!extra)
304
+ continue;
305
+ await extra.delete().catch(() => { });
306
+ }
307
+ }, `upsert-response:${opts?.source ?? 'unknown'}`);
308
+ const upsertLiveActivityEmbeds = (title, rawText, color, footerText, opts) => enqueueActivity(async () => {
309
+ if (opts?.skipWhenFinalized && isFinalized)
310
+ return;
311
+ if (opts?.expectedVersion !== undefined && opts.expectedVersion !== liveActivityUpdateVersion)
312
+ return;
313
+ if (!channel)
314
+ return;
315
+ const descriptions = buildLiveActivityDescriptions(rawText);
316
+ const renderKey = `${title}|${color}|${footerText}|${descriptions.join('\n<<<PAGE_BREAK>>>\n')}`;
317
+ if (renderKey === lastLiveActivityKey && liveActivityMessages.length > 0) {
318
+ return;
319
+ }
320
+ lastLiveActivityKey = renderKey;
321
+ for (let i = 0; i < descriptions.length; i++) {
322
+ const embed = new discord_js_1.EmbedBuilder()
323
+ .setTitle(descriptions.length > 1 ? `${title} (${i + 1}/${descriptions.length})` : title)
324
+ .setDescription(descriptions[i])
325
+ .setColor(color)
326
+ .setFooter({ text: footerText })
327
+ .setTimestamp();
328
+ if (!liveActivityMessages[i]) {
329
+ liveActivityMessages[i] = await channel.send({ embeds: [embed] }).catch(() => null);
330
+ continue;
331
+ }
332
+ await liveActivityMessages[i].edit({ embeds: [embed] }).catch(async () => {
333
+ liveActivityMessages[i] = await channel.send({ embeds: [embed] }).catch(() => null);
334
+ });
335
+ }
336
+ while (liveActivityMessages.length > descriptions.length) {
337
+ const extra = liveActivityMessages.pop();
338
+ if (!extra)
339
+ continue;
340
+ await extra.delete().catch(() => { });
341
+ }
342
+ }, `upsert-activity:${opts?.source ?? 'unknown'}`);
343
+ try {
344
+ let injectResult;
345
+ if (inboundImages.length > 0) {
346
+ injectResult = await cdp.injectMessageWithImageFiles(prompt, inboundImages.map((image) => image.localPath));
347
+ if (!injectResult.ok) {
348
+ await sendEmbed((0, i18n_1.t)('🖼️ Attached image fallback'), (0, i18n_1.t)('Failed to attach image directly, resending via URL reference.'), PHASE_COLORS.thinking);
349
+ injectResult = await cdp.injectMessage((0, imageHandler_1.buildPromptWithAttachmentUrls)(prompt, inboundImages));
350
+ }
351
+ }
352
+ else {
353
+ injectResult = await cdp.injectMessage(prompt);
354
+ }
355
+ if (!injectResult.ok) {
356
+ isFinalized = true;
357
+ await sendEmbed(`${PHASE_ICONS.error} Message Injection Failed`, `Failed to send message: ${injectResult.error}`, PHASE_COLORS.error);
358
+ await clearWatchingReaction();
359
+ await message.react('❌').catch(() => { });
360
+ return;
361
+ }
362
+ const startTime = Date.now();
363
+ await upsertLiveActivityEmbeds(`${PHASE_ICONS.thinking} Process Log`, '', PHASE_COLORS.thinking, (0, i18n_1.t)('⏱️ Elapsed: 0s | Process log'), { source: 'initial' });
364
+ const monitor = new responseMonitor_1.ResponseMonitor({
365
+ cdpService: cdp,
366
+ pollIntervalMs: 2000,
367
+ maxDurationMs: 300000,
368
+ stopGoneConfirmCount: 3,
369
+ onPhaseChange: (_phase, _text) => {
370
+ // Phase transitions are already logged inside ResponseMonitor.setPhase()
371
+ },
372
+ onProcessLog: (logText) => {
373
+ if (isFinalized)
374
+ return;
375
+ if (logText && logText.trim().length > 0) {
376
+ lastActivityLogText = appendProcessLogs(logText);
377
+ }
378
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
379
+ liveActivityUpdateVersion += 1;
380
+ const activityVersion = liveActivityUpdateVersion;
381
+ upsertLiveActivityEmbeds(`${PHASE_ICONS.thinking} Process Log`, lastActivityLogText || ACTIVITY_PLACEHOLDER, PHASE_COLORS.thinking, (0, i18n_1.t)(`⏱️ Elapsed: ${elapsed}s | Process log`), {
382
+ source: 'process-log',
383
+ expectedVersion: activityVersion,
384
+ skipWhenFinalized: true,
385
+ }).catch(() => { });
386
+ },
387
+ onProgress: (text) => {
388
+ if (isFinalized)
389
+ return;
390
+ // TODO: Re-enable live output streaming after RESPONSE_TEXT reliably excludes process logs.
391
+ const separated = (0, discordFormatter_1.splitOutputAndLogs)(text);
392
+ if (separated.output && separated.output.trim().length > 0) {
393
+ lastProgressText = separated.output;
394
+ }
395
+ },
396
+ onComplete: async (finalText) => {
397
+ isFinalized = true;
398
+ try {
399
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
400
+ const responseText = (finalText && finalText.trim().length > 0)
401
+ ? finalText
402
+ : lastProgressText;
403
+ const emergencyText = (!responseText || responseText.trim().length === 0)
404
+ ? await tryEmergencyExtractText()
405
+ : '';
406
+ const finalResponseText = responseText && responseText.trim().length > 0
407
+ ? responseText
408
+ : emergencyText;
409
+ const separated = (0, discordFormatter_1.splitOutputAndLogs)(finalResponseText);
410
+ const finalOutputText = separated.output || finalResponseText;
411
+ // Process logs are now collected by onProcessLog callback directly;
412
+ // sanitizeActivityLines is NOT applied because it would strip the very
413
+ // content we want to display (activity messages, tool names, etc.)
414
+ const finalLogText = lastActivityLogText || processLogBuffer.snapshot();
415
+ if (finalLogText && finalLogText.trim().length > 0) {
416
+ logger_1.logger.divider('Process Log');
417
+ console.info(finalLogText);
418
+ }
419
+ if (finalOutputText && finalOutputText.trim().length > 0) {
420
+ logger_1.logger.divider(`Output (${finalOutputText.length} chars)`);
421
+ console.info(finalOutputText);
422
+ }
423
+ logger_1.logger.divider();
424
+ liveActivityUpdateVersion += 1;
425
+ const activityVersion = liveActivityUpdateVersion;
426
+ await upsertLiveActivityEmbeds(`${PHASE_ICONS.thinking} Process Log`, finalLogText || ACTIVITY_PLACEHOLDER, PHASE_COLORS.thinking, (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Process log`), {
427
+ source: 'complete',
428
+ expectedVersion: activityVersion,
429
+ });
430
+ liveResponseUpdateVersion += 1;
431
+ const responseVersion = liveResponseUpdateVersion;
432
+ if (finalOutputText && finalOutputText.trim().length > 0) {
433
+ await upsertLiveResponseEmbeds(`${PHASE_ICONS.complete} Final Output`, finalOutputText, PHASE_COLORS.complete, (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Complete`), {
434
+ source: 'complete',
435
+ expectedVersion: responseVersion,
436
+ });
437
+ }
438
+ else {
439
+ await upsertLiveResponseEmbeds(`${PHASE_ICONS.complete} Complete`, (0, i18n_1.t)('Failed to extract response. Use `/screenshot` to verify.'), PHASE_COLORS.complete, (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Complete`), {
440
+ source: 'complete',
441
+ expectedVersion: responseVersion,
442
+ });
443
+ }
444
+ if (options && message.guild) {
445
+ try {
446
+ const sessionInfo = await options.chatSessionService.getCurrentSessionInfo(cdp);
447
+ if (sessionInfo && sessionInfo.hasActiveChat && sessionInfo.title && sessionInfo.title !== (0, i18n_1.t)('(Untitled)')) {
448
+ const session = options.chatSessionRepo.findByChannelId(message.channelId);
449
+ const workspaceDirName = session
450
+ ? bridge.pool.extractDirName(session.workspacePath)
451
+ : cdp.getCurrentWorkspaceName();
452
+ if (workspaceDirName) {
453
+ (0, cdpBridgeManager_1.registerApprovalSessionChannel)(bridge, workspaceDirName, sessionInfo.title, message.channel);
454
+ }
455
+ const newName = options.titleGenerator.sanitizeForChannelName(sessionInfo.title);
456
+ if (session && session.displayName !== sessionInfo.title) {
457
+ const formattedName = `${session.sessionNumber}-${newName}`;
458
+ await options.channelManager.renameChannel(message.guild, message.channelId, formattedName);
459
+ options.chatSessionRepo.updateDisplayName(message.channelId, sessionInfo.title);
460
+ }
461
+ }
462
+ }
463
+ catch (e) {
464
+ logger_1.logger.error('[Rename] Failed to get title from Antigravity and rename:', e);
465
+ }
466
+ }
467
+ if (monitor.getPhase() === 'quotaReached' || monitor.getQuotaDetected()) {
468
+ await sendEmbed('⚠️ Model Quota Reached', 'Model quota limit reached. Please wait or switch to a different model with `/model`.', 0xFF6B6B, undefined, 'Quota Reached — consider switching models');
469
+ await clearWatchingReaction();
470
+ await message.react('⚠️').catch(() => { });
471
+ return;
472
+ }
473
+ await sendGeneratedImages(finalOutputText || '');
474
+ await clearWatchingReaction();
475
+ await message.react(finalOutputText && finalOutputText.trim().length > 0 ? '✅' : '⚠️').catch(() => { });
476
+ }
477
+ catch (error) {
478
+ logger_1.logger.error(`[sendPromptToAntigravity:${monitorTraceId}] onComplete failed:`, error);
479
+ }
480
+ },
481
+ onTimeout: async (lastText) => {
482
+ isFinalized = true;
483
+ try {
484
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
485
+ const timeoutText = (lastText && lastText.trim().length > 0)
486
+ ? lastText
487
+ : lastProgressText;
488
+ const separated = (0, discordFormatter_1.splitOutputAndLogs)(timeoutText || '');
489
+ const sanitizedTimeoutLogs = lastActivityLogText || processLogBuffer.snapshot();
490
+ const payload = separated.output && separated.output.trim().length > 0
491
+ ? (0, i18n_1.t)(`${separated.output}\n\n[Monitor Ended] Timeout after 5 minutes.`)
492
+ : 'Monitor ended after 5 minutes. No text was retrieved.';
493
+ liveResponseUpdateVersion += 1;
494
+ const responseVersion = liveResponseUpdateVersion;
495
+ await upsertLiveResponseEmbeds(`${PHASE_ICONS.timeout} Timeout`, payload, PHASE_COLORS.timeout, `⏱️ Elapsed: ${elapsed}s | Timeout`, {
496
+ source: 'timeout',
497
+ expectedVersion: responseVersion,
498
+ });
499
+ liveActivityUpdateVersion += 1;
500
+ const activityVersion = liveActivityUpdateVersion;
501
+ await upsertLiveActivityEmbeds(`${PHASE_ICONS.thinking} Process Log`, sanitizedTimeoutLogs || ACTIVITY_PLACEHOLDER, PHASE_COLORS.thinking, (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Process log`), {
502
+ source: 'timeout',
503
+ expectedVersion: activityVersion,
504
+ });
505
+ await clearWatchingReaction();
506
+ await message.react('⚠️').catch(() => { });
507
+ }
508
+ catch (error) {
509
+ logger_1.logger.error(`[sendPromptToAntigravity:${monitorTraceId}] onTimeout failed:`, error);
510
+ }
511
+ },
512
+ });
513
+ await monitor.start();
514
+ }
515
+ catch (e) {
516
+ isFinalized = true;
517
+ await sendEmbed(`${PHASE_ICONS.error} Error`, (0, i18n_1.t)(`Error occurred during processing: ${e.message}`), PHASE_COLORS.error);
518
+ await clearWatchingReaction();
519
+ await message.react('❌').catch(() => { });
520
+ }
521
+ }
522
+ // =============================================================================
523
+ // Bot main entry point
524
+ // =============================================================================
525
+ const startBot = async () => {
526
+ const config = (0, config_1.loadConfig)();
527
+ const dbPath = process.env.NODE_ENV === 'test' ? ':memory:' : 'antigravity.db';
528
+ const db = new better_sqlite3_1.default(dbPath);
529
+ const modeService = new modeService_1.ModeService();
530
+ const modelService = new modelService_1.ModelService();
531
+ const templateRepo = new templateRepository_1.TemplateRepository(db);
532
+ const workspaceBindingRepo = new workspaceBindingRepository_1.WorkspaceBindingRepository(db);
533
+ const chatSessionRepo = new chatSessionRepository_1.ChatSessionRepository(db);
534
+ const workspaceService = new workspaceService_1.WorkspaceService(config.workspaceBaseDir);
535
+ const channelManager = new channelManager_1.ChannelManager();
536
+ // Auto-launch Antigravity with CDP port if not already running
537
+ await (0, antigravityLauncher_1.ensureAntigravityRunning)();
538
+ // Initialize CDP bridge (lazy connection: pool creation only)
539
+ const bridge = (0, cdpBridgeManager_1.initCdpBridge)(config.autoApproveFileEdits);
540
+ // Initialize CDP-dependent services (constructor CDP dependency removed)
541
+ const chatSessionService = new chatSessionService_1.ChatSessionService();
542
+ const titleGenerator = new titleGeneratorService_1.TitleGeneratorService();
543
+ const promptDispatcher = new promptDispatcher_1.PromptDispatcher({
544
+ bridge,
545
+ modeService,
546
+ modelService,
547
+ sendPromptImpl: sendPromptToAntigravity,
548
+ });
549
+ // Initialize command handlers
550
+ const wsHandler = new workspaceCommandHandler_1.WorkspaceCommandHandler(workspaceBindingRepo, chatSessionRepo, workspaceService, channelManager);
551
+ const chatHandler = new chatCommandHandler_1.ChatCommandHandler(chatSessionService, chatSessionRepo, workspaceBindingRepo, channelManager, workspaceService, bridge.pool);
552
+ const cleanupHandler = new cleanupCommandHandler_1.CleanupCommandHandler(chatSessionRepo, workspaceBindingRepo);
553
+ const slashCommandHandler = new slashCommandHandler_1.SlashCommandHandler(templateRepo);
554
+ const client = new discord_js_1.Client({
555
+ intents: [
556
+ discord_js_1.GatewayIntentBits.Guilds,
557
+ discord_js_1.GatewayIntentBits.GuildMessages,
558
+ discord_js_1.GatewayIntentBits.MessageContent,
559
+ ]
560
+ });
561
+ client.once(discord_js_1.Events.ClientReady, async (readyClient) => {
562
+ logger_1.logger.info(`Ready! Logged in as ${readyClient.user.tag}`);
563
+ try {
564
+ await (0, registerSlashCommands_1.registerSlashCommands)(config.discordToken, config.clientId, config.guildId);
565
+ }
566
+ catch (error) {
567
+ logger_1.logger.warn('Failed to register slash commands, but text commands remain available.');
568
+ }
569
+ });
570
+ // [Discord Interactions API] Slash command interaction handler
571
+ client.on(discord_js_1.Events.InteractionCreate, (0, interactionCreateHandler_1.createInteractionCreateHandler)({
572
+ config,
573
+ bridge,
574
+ cleanupHandler,
575
+ modeService,
576
+ modelService,
577
+ slashCommandHandler,
578
+ wsHandler,
579
+ chatHandler,
580
+ client,
581
+ sendModeUI: modeUi_1.sendModeUI,
582
+ sendModelsUI: modelsUi_1.sendModelsUI,
583
+ sendAutoAcceptUI: autoAcceptUi_1.sendAutoAcceptUI,
584
+ getCurrentCdp: cdpBridgeManager_1.getCurrentCdp,
585
+ parseApprovalCustomId: cdpBridgeManager_1.parseApprovalCustomId,
586
+ 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),
587
+ handleTemplateUse: async (interaction, templateId) => {
588
+ const template = templateRepo.findById(templateId);
589
+ if (!template) {
590
+ await interaction.followUp({
591
+ content: 'Template not found. It may have been deleted.',
592
+ flags: discord_js_1.MessageFlags.Ephemeral,
593
+ });
594
+ return;
595
+ }
596
+ // Resolve CDP via workspace binding (same flow as text messages)
597
+ const channelId = interaction.channelId;
598
+ const workspacePath = wsHandler.getWorkspaceForChannel(channelId);
599
+ let cdp = null;
600
+ if (workspacePath) {
601
+ try {
602
+ cdp = await bridge.pool.getOrConnect(workspacePath);
603
+ const dirName = bridge.pool.extractDirName(workspacePath);
604
+ bridge.lastActiveWorkspace = dirName;
605
+ bridge.lastActiveChannel = interaction.channel;
606
+ (0, cdpBridgeManager_1.registerApprovalWorkspaceChannel)(bridge, dirName, interaction.channel);
607
+ const session = chatSessionRepo.findByChannelId(channelId);
608
+ if (session?.displayName) {
609
+ (0, cdpBridgeManager_1.registerApprovalSessionChannel)(bridge, dirName, session.displayName, interaction.channel);
610
+ }
611
+ (0, cdpBridgeManager_1.ensureApprovalDetector)(bridge, cdp, dirName, client);
612
+ }
613
+ catch (e) {
614
+ await interaction.followUp({
615
+ content: `Failed to connect to workspace: ${e.message}`,
616
+ flags: discord_js_1.MessageFlags.Ephemeral,
617
+ });
618
+ return;
619
+ }
620
+ }
621
+ else {
622
+ cdp = (0, cdpBridgeManager_1.getCurrentCdp)(bridge);
623
+ }
624
+ if (!cdp) {
625
+ await interaction.followUp({
626
+ content: 'Not connected to CDP. Please connect to a project first.',
627
+ flags: discord_js_1.MessageFlags.Ephemeral,
628
+ });
629
+ return;
630
+ }
631
+ const followUp = await interaction.followUp({
632
+ content: `Executing template **${template.name}**...`,
633
+ });
634
+ if (followUp instanceof discord_js_1.Message) {
635
+ await promptDispatcher.send({
636
+ message: followUp,
637
+ prompt: template.prompt,
638
+ cdp,
639
+ inboundImages: [],
640
+ options: {
641
+ chatSessionService,
642
+ chatSessionRepo,
643
+ channelManager,
644
+ titleGenerator,
645
+ },
646
+ });
647
+ }
648
+ },
649
+ }));
650
+ // [Text message handler]
651
+ client.on(discord_js_1.Events.MessageCreate, (0, messageCreateHandler_1.createMessageCreateHandler)({
652
+ config,
653
+ bridge,
654
+ modeService,
655
+ modelService,
656
+ slashCommandHandler,
657
+ wsHandler,
658
+ chatSessionService,
659
+ chatSessionRepo,
660
+ channelManager,
661
+ titleGenerator,
662
+ client,
663
+ sendPromptToAntigravity: async (_bridge, message, prompt, cdp, _modeService, _modelService, inboundImages = [], options) => promptDispatcher.send({
664
+ message,
665
+ prompt,
666
+ cdp,
667
+ inboundImages,
668
+ options,
669
+ }),
670
+ autoRenameChannel,
671
+ handleScreenshot: screenshotUi_1.handleScreenshot,
672
+ }));
673
+ await client.login(config.discordToken);
674
+ };
675
+ exports.startBot = startBot;
676
+ /**
677
+ * Auto-rename channel on first message send
678
+ */
679
+ async function autoRenameChannel(message, chatSessionRepo, titleGenerator, channelManager, cdp) {
680
+ const session = chatSessionRepo.findByChannelId(message.channelId);
681
+ if (!session || session.isRenamed)
682
+ return;
683
+ const guild = message.guild;
684
+ if (!guild)
685
+ return;
686
+ try {
687
+ const title = await titleGenerator.generateTitle(message.content, cdp);
688
+ const newName = `${session.sessionNumber}-${title}`;
689
+ await channelManager.renameChannel(guild, message.channelId, newName);
690
+ chatSessionRepo.updateDisplayName(message.channelId, title);
691
+ }
692
+ catch (err) {
693
+ logger_1.logger.error('[AutoRename] Rename failed:', err);
694
+ }
695
+ }
696
+ /**
697
+ * Handle Discord Interactions API slash commands
698
+ */
699
+ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, chatHandler, cleanupHandler, modeService, modelService, autoAcceptService, _client, promptDispatcher, templateRepo) {
700
+ const commandName = interaction.commandName;
701
+ switch (commandName) {
702
+ case 'help': {
703
+ const embed = new discord_js_1.EmbedBuilder()
704
+ .setTitle('📖 LazyGravity Commands')
705
+ .setColor(0x5865F2)
706
+ .setDescription('Commands for controlling Antigravity from Discord.')
707
+ .addFields({
708
+ name: '💬 Chat', value: [
709
+ '`/new` — Start a new chat session',
710
+ '`/chat` — Show current session info + list',
711
+ ].join('\n')
712
+ }, {
713
+ name: '⏹️ Control', value: [
714
+ '`/stop` — Interrupt active LLM generation',
715
+ '`/screenshot` — Capture Antigravity screen',
716
+ ].join('\n')
717
+ }, {
718
+ name: '⚙️ Settings', value: [
719
+ '`/mode` — Display and change execution mode',
720
+ '`/model [name]` — Display and change LLM model',
721
+ ].join('\n')
722
+ }, {
723
+ name: '📁 Projects', value: [
724
+ '`/project` — Display project list',
725
+ '`/project create <name>` — Create a new project',
726
+ ].join('\n')
727
+ }, {
728
+ name: '📝 Templates', value: [
729
+ '`/template list` — Show templates with execute buttons (click to run)',
730
+ '`/template add <name> <prompt>` — Register a template',
731
+ '`/template delete <name>` — Delete a template',
732
+ ].join('\n')
733
+ }, {
734
+ name: '🔧 System', value: [
735
+ '`/status` — Display overall bot status',
736
+ '`/autoaccept` — Toggle auto-approve mode for approval dialogs via buttons',
737
+ '`/cleanup [days]` — Clean up unused channels/categories',
738
+ '`/help` — Show this help',
739
+ ].join('\n')
740
+ })
741
+ .setFooter({ text: 'Text messages are sent directly to Antigravity' })
742
+ .setTimestamp();
743
+ await interaction.editReply({ embeds: [embed] });
744
+ break;
745
+ }
746
+ case 'mode': {
747
+ await (0, modeUi_1.sendModeUI)(interaction, modeService);
748
+ break;
749
+ }
750
+ case 'model': {
751
+ const modelName = interaction.options.getString('name');
752
+ if (!modelName) {
753
+ await (0, modelsUi_1.sendModelsUI)(interaction, {
754
+ getCurrentCdp: () => (0, cdpBridgeManager_1.getCurrentCdp)(bridge),
755
+ fetchQuota: async () => bridge.quota.fetchQuota(),
756
+ });
757
+ }
758
+ else {
759
+ const cdp = (0, cdpBridgeManager_1.getCurrentCdp)(bridge);
760
+ if (!cdp) {
761
+ await interaction.editReply({ content: 'Not connected to CDP.' });
762
+ break;
763
+ }
764
+ const res = await cdp.setUiModel(modelName);
765
+ if (res.ok) {
766
+ await interaction.editReply({ content: `Model changed to **${res.model}**.` });
767
+ }
768
+ else {
769
+ await interaction.editReply({ content: res.error || 'Failed to change model.' });
770
+ }
771
+ }
772
+ break;
773
+ }
774
+ case 'template': {
775
+ const subcommand = interaction.options.getSubcommand();
776
+ if (subcommand === 'list') {
777
+ const templates = templateRepo.findAll();
778
+ await (0, templateUi_1.sendTemplateUI)(interaction, templates);
779
+ break;
780
+ }
781
+ let args;
782
+ switch (subcommand) {
783
+ case 'add': {
784
+ const name = interaction.options.getString('name', true);
785
+ const prompt = interaction.options.getString('prompt', true);
786
+ args = ['add', name, prompt];
787
+ break;
788
+ }
789
+ case 'delete': {
790
+ const name = interaction.options.getString('name', true);
791
+ args = ['delete', name];
792
+ break;
793
+ }
794
+ default:
795
+ args = [];
796
+ }
797
+ const result = await handler.handleCommand('template', args);
798
+ await interaction.editReply({ content: result.message });
799
+ break;
800
+ }
801
+ case 'status': {
802
+ const activeNames = bridge.pool.getActiveWorkspaceNames();
803
+ const currentModel = (() => {
804
+ const cdp = (0, cdpBridgeManager_1.getCurrentCdp)(bridge);
805
+ return cdp ? 'CDP Connected' : 'Disconnected';
806
+ })();
807
+ const currentMode = modeService.getCurrentMode();
808
+ const embed = new discord_js_1.EmbedBuilder()
809
+ .setTitle('🔧 Bot Status')
810
+ .setColor(activeNames.length > 0 ? 0x00CC88 : 0x888888)
811
+ .addFields({ name: 'CDP Connection', value: activeNames.length > 0 ? `🟢 ${activeNames.length} project(s) connected` : '⚪ Disconnected', inline: true }, { name: 'Mode', value: modeService_1.MODE_DISPLAY_NAMES[currentMode] || currentMode, inline: true }, { name: 'Auto Approve', value: autoAcceptService.isEnabled() ? '🟢 ON' : '⚪ OFF', inline: true })
812
+ .setTimestamp();
813
+ if (activeNames.length > 0) {
814
+ const lines = activeNames.map((name) => {
815
+ const cdp = bridge.pool.getConnected(name);
816
+ const contexts = cdp ? cdp.getContexts().length : 0;
817
+ const detectorActive = bridge.pool.getApprovalDetector(name)?.isActive() ? ' [Detecting]' : '';
818
+ return `• **${name}** — Contexts: ${contexts}${detectorActive}`;
819
+ });
820
+ embed.setDescription(`**Connected Projects:**\n${lines.join('\n')}`);
821
+ }
822
+ else {
823
+ embed.setDescription('Send a message to auto-connect to a project.');
824
+ }
825
+ await interaction.editReply({ embeds: [embed] });
826
+ break;
827
+ }
828
+ case 'autoaccept': {
829
+ const requestedMode = interaction.options.getString('mode');
830
+ if (!requestedMode) {
831
+ await (0, autoAcceptUi_1.sendAutoAcceptUI)(interaction, autoAcceptService);
832
+ break;
833
+ }
834
+ const result = autoAcceptService.handle(requestedMode);
835
+ await interaction.editReply({ content: result.message });
836
+ break;
837
+ }
838
+ case 'screenshot': {
839
+ await (0, screenshotUi_1.handleScreenshot)(interaction, (0, cdpBridgeManager_1.getCurrentCdp)(bridge));
840
+ break;
841
+ }
842
+ case 'stop': {
843
+ const cdp = (0, cdpBridgeManager_1.getCurrentCdp)(bridge);
844
+ if (!cdp) {
845
+ await interaction.editReply({ content: '⚠️ Not connected to CDP. Please connect to a project first.' });
846
+ break;
847
+ }
848
+ try {
849
+ const contextId = cdp.getPrimaryContextId();
850
+ const callParams = {
851
+ expression: responseMonitor_1.RESPONSE_SELECTORS.CLICK_STOP_BUTTON,
852
+ returnByValue: true,
853
+ awaitPromise: false,
854
+ };
855
+ if (contextId !== null) {
856
+ callParams.contextId = contextId;
857
+ }
858
+ const result = await cdp.call('Runtime.evaluate', callParams);
859
+ const value = result?.result?.value;
860
+ if (value?.ok) {
861
+ const embed = new discord_js_1.EmbedBuilder()
862
+ .setTitle('⏹️ Generation Interrupted')
863
+ .setDescription('AI response generation was safely stopped.')
864
+ .setColor(0xE74C3C)
865
+ .setTimestamp();
866
+ await interaction.editReply({ embeds: [embed] });
867
+ }
868
+ else {
869
+ const embed = new discord_js_1.EmbedBuilder()
870
+ .setTitle('⚠️ Could Not Stop')
871
+ .setDescription(value?.error || 'Stop button not found. The LLM may not be running.')
872
+ .setColor(0xF39C12)
873
+ .setTimestamp();
874
+ await interaction.editReply({ embeds: [embed] });
875
+ }
876
+ }
877
+ catch (e) {
878
+ await interaction.editReply({ content: `❌ Error during stop processing: ${e.message}` });
879
+ }
880
+ break;
881
+ }
882
+ case 'project': {
883
+ const wsSub = interaction.options.getSubcommand(false);
884
+ if (wsSub === 'create') {
885
+ if (!interaction.guild) {
886
+ await interaction.editReply({ content: 'This command can only be used in a server.' });
887
+ break;
888
+ }
889
+ await wsHandler.handleCreate(interaction, interaction.guild);
890
+ }
891
+ else {
892
+ // /project list or /project (default)
893
+ await wsHandler.handleShow(interaction);
894
+ }
895
+ break;
896
+ }
897
+ case 'new': {
898
+ await chatHandler.handleNew(interaction);
899
+ break;
900
+ }
901
+ case 'chat': {
902
+ await chatHandler.handleChat(interaction);
903
+ break;
904
+ }
905
+ case 'cleanup': {
906
+ await cleanupHandler.handleCleanup(interaction);
907
+ break;
908
+ }
909
+ default:
910
+ await interaction.editReply({
911
+ content: `Unknown command: /${commandName}`,
912
+ });
913
+ }
914
+ }