lazy-gravity 0.1.0 → 0.2.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 +18 -6
- package/dist/bin/cli.js +18 -18
- package/dist/bin/commands/doctor.js +2 -1
- package/dist/bin/commands/start.js +25 -2
- package/dist/bot/index.js +346 -152
- package/dist/commands/joinCommandHandler.js +302 -0
- package/dist/commands/joinDetachCommandHandler.js +285 -0
- package/dist/commands/registerSlashCommands.js +35 -0
- package/dist/database/chatSessionRepository.js +10 -0
- package/dist/database/userPreferenceRepository.js +72 -0
- package/dist/events/interactionCreateHandler.js +58 -36
- package/dist/events/messageCreateHandler.js +158 -53
- package/dist/services/antigravityLauncher.js +4 -3
- package/dist/services/approvalDetector.js +6 -0
- package/dist/services/cdpBridgeManager.js +184 -84
- package/dist/services/cdpConnectionPool.js +79 -51
- package/dist/services/cdpService.js +149 -51
- package/dist/services/chatSessionService.js +229 -8
- package/dist/services/errorPopupDetector.js +6 -0
- package/dist/services/planningDetector.js +6 -0
- package/dist/services/responseMonitor.js +125 -24
- package/dist/services/updateCheckService.js +147 -0
- package/dist/services/userMessageDetector.js +221 -0
- package/dist/ui/modeUi.js +11 -1
- package/dist/ui/outputUi.js +30 -0
- package/dist/ui/sessionPickerUi.js +48 -0
- package/dist/utils/antigravityPaths.js +94 -0
- package/dist/utils/configLoader.js +10 -0
- package/dist/utils/discordButtonUtils.js +33 -0
- package/dist/utils/logBuffer.js +47 -0
- package/dist/utils/logger.js +80 -20
- package/dist/utils/pathUtils.js +57 -0
- package/dist/utils/plainTextFormatter.js +70 -0
- package/package.json +4 -4
package/dist/bot/index.js
CHANGED
|
@@ -40,6 +40,7 @@ exports.startBot = exports.getResponseDeliveryModeForTest = void 0;
|
|
|
40
40
|
exports.createSerialTaskQueueForTest = createSerialTaskQueueForTest;
|
|
41
41
|
const i18n_1 = require("../utils/i18n");
|
|
42
42
|
const logger_1 = require("../utils/logger");
|
|
43
|
+
const logBuffer_1 = require("../utils/logBuffer");
|
|
43
44
|
const discord_js_1 = require("discord.js");
|
|
44
45
|
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
45
46
|
const config_1 = require("../utils/config");
|
|
@@ -56,9 +57,11 @@ const chatCommandHandler_1 = require("../commands/chatCommandHandler");
|
|
|
56
57
|
const cleanupCommandHandler_1 = require("../commands/cleanupCommandHandler");
|
|
57
58
|
const channelManager_1 = require("../services/channelManager");
|
|
58
59
|
const titleGeneratorService_1 = require("../services/titleGeneratorService");
|
|
60
|
+
const joinCommandHandler_1 = require("../commands/joinCommandHandler");
|
|
59
61
|
const chatSessionService_1 = require("../services/chatSessionService");
|
|
60
62
|
const responseMonitor_1 = require("../services/responseMonitor");
|
|
61
63
|
const antigravityLauncher_1 = require("../services/antigravityLauncher");
|
|
64
|
+
const pathUtils_1 = require("../utils/pathUtils");
|
|
62
65
|
const promptDispatcher_1 = require("../services/promptDispatcher");
|
|
63
66
|
const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
|
|
64
67
|
const streamMessageFormatter_1 = require("../utils/streamMessageFormatter");
|
|
@@ -69,7 +72,10 @@ const modeUi_1 = require("../ui/modeUi");
|
|
|
69
72
|
const modelsUi_1 = require("../ui/modelsUi");
|
|
70
73
|
const templateUi_1 = require("../ui/templateUi");
|
|
71
74
|
const autoAcceptUi_1 = require("../ui/autoAcceptUi");
|
|
75
|
+
const outputUi_1 = require("../ui/outputUi");
|
|
72
76
|
const screenshotUi_1 = require("../ui/screenshotUi");
|
|
77
|
+
const userPreferenceRepository_1 = require("../database/userPreferenceRepository");
|
|
78
|
+
const plainTextFormatter_1 = require("../utils/plainTextFormatter");
|
|
73
79
|
const interactionCreateHandler_1 = require("../events/interactionCreateHandler");
|
|
74
80
|
const messageCreateHandler_1 = require("../events/messageCreateHandler");
|
|
75
81
|
// =============================================================================
|
|
@@ -127,6 +133,17 @@ function createSerialTaskQueueForTest(queueName, traceId) {
|
|
|
127
133
|
* - Visualize the flow of planning/analysis/execution confirmation/implementation as logs
|
|
128
134
|
*/
|
|
129
135
|
async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService, modelService, inboundImages = [], options) {
|
|
136
|
+
// Completion signal — called exactly once when the entire prompt lifecycle ends
|
|
137
|
+
let completionSignaled = false;
|
|
138
|
+
const signalCompletion = (exitPath) => {
|
|
139
|
+
if (completionSignaled)
|
|
140
|
+
return;
|
|
141
|
+
completionSignaled = true;
|
|
142
|
+
logger_1.logger.debug(`[sendPrompt:${message.channelId}] signalCompletion via ${exitPath}`);
|
|
143
|
+
options?.onFullCompletion?.();
|
|
144
|
+
};
|
|
145
|
+
// Resolve output format once at the start (no mid-response switches)
|
|
146
|
+
const outputFormat = options?.userPrefRepo?.getOutputFormat(message.author.id) ?? 'embed';
|
|
130
147
|
// Add reaction to acknowledge command receipt
|
|
131
148
|
await message.react('👀').catch(() => { });
|
|
132
149
|
const channel = (message.channel && 'send' in message.channel) ? message.channel : null;
|
|
@@ -137,6 +154,13 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
|
|
|
137
154
|
const sendEmbed = (title, description, color, fields, footerText) => enqueueGeneral(async () => {
|
|
138
155
|
if (!channel)
|
|
139
156
|
return;
|
|
157
|
+
if (outputFormat === 'plain') {
|
|
158
|
+
const chunks = (0, plainTextFormatter_1.formatAsPlainText)({ title, description, fields, footerText });
|
|
159
|
+
for (const chunk of chunks) {
|
|
160
|
+
await channel.send({ content: chunk }).catch(() => { });
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
140
164
|
const embed = new discord_js_1.EmbedBuilder()
|
|
141
165
|
.setTitle(title)
|
|
142
166
|
.setDescription(description)
|
|
@@ -256,9 +280,10 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
|
|
|
256
280
|
}
|
|
257
281
|
};
|
|
258
282
|
if (!cdp.isConnected()) {
|
|
259
|
-
await sendEmbed(`${PHASE_ICONS.error} Connection Error`,
|
|
283
|
+
await sendEmbed(`${PHASE_ICONS.error} Connection Error`, `Not connected to Antigravity.\nStart with \`${(0, pathUtils_1.getAntigravityCdpHint)(9223)}\`, then send a message to auto-connect.`, PHASE_COLORS.error);
|
|
260
284
|
await clearWatchingReaction();
|
|
261
285
|
await message.react('❌').catch(() => { });
|
|
286
|
+
signalCompletion('cdp-disconnected');
|
|
262
287
|
return;
|
|
263
288
|
}
|
|
264
289
|
const localMode = modeService.getCurrentMode();
|
|
@@ -311,6 +336,30 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
|
|
|
311
336
|
return;
|
|
312
337
|
if (!channel)
|
|
313
338
|
return;
|
|
339
|
+
if (outputFormat === 'plain') {
|
|
340
|
+
const formatted = (0, discordFormatter_1.formatForDiscord)((rawText || '').trim());
|
|
341
|
+
const plainChunks = (0, plainTextFormatter_1.splitPlainText)(`**${title}**\n${formatted}\n_${footerText}_`);
|
|
342
|
+
const renderKey = `${title}|plain|${footerText}|${plainChunks.join('\n<<<PAGE_BREAK>>>\n')}`;
|
|
343
|
+
if (renderKey === lastLiveResponseKey && liveResponseMessages.length > 0)
|
|
344
|
+
return;
|
|
345
|
+
lastLiveResponseKey = renderKey;
|
|
346
|
+
for (let i = 0; i < plainChunks.length; i++) {
|
|
347
|
+
if (!liveResponseMessages[i]) {
|
|
348
|
+
liveResponseMessages[i] = await channel.send({ content: plainChunks[i] }).catch(() => null);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
await liveResponseMessages[i].edit({ content: plainChunks[i] }).catch(async () => {
|
|
352
|
+
liveResponseMessages[i] = await channel.send({ content: plainChunks[i] }).catch(() => null);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
while (liveResponseMessages.length > plainChunks.length) {
|
|
356
|
+
const extra = liveResponseMessages.pop();
|
|
357
|
+
if (!extra)
|
|
358
|
+
continue;
|
|
359
|
+
await extra.delete().catch(() => { });
|
|
360
|
+
}
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
314
363
|
const descriptions = buildLiveResponseDescriptions(rawText);
|
|
315
364
|
const renderKey = `${title}|${color}|${footerText}|${descriptions.join('\n<<<PAGE_BREAK>>>\n')}`;
|
|
316
365
|
if (renderKey === lastLiveResponseKey && liveResponseMessages.length > 0) {
|
|
@@ -347,6 +396,31 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
|
|
|
347
396
|
return;
|
|
348
397
|
if (!channel)
|
|
349
398
|
return;
|
|
399
|
+
if (outputFormat === 'plain') {
|
|
400
|
+
const formatted = (0, discordFormatter_1.formatForDiscord)((rawText || '').trim());
|
|
401
|
+
const plainContent = `**${title}**\n${formatted}\n_${footerText}_`;
|
|
402
|
+
const plainChunks = (0, plainTextFormatter_1.splitPlainText)(plainContent);
|
|
403
|
+
const renderKey = `${title}|plain|${footerText}|${plainChunks.join('\n<<<PAGE_BREAK>>>\n')}`;
|
|
404
|
+
if (renderKey === lastLiveActivityKey && liveActivityMessages.length > 0)
|
|
405
|
+
return;
|
|
406
|
+
lastLiveActivityKey = renderKey;
|
|
407
|
+
for (let i = 0; i < plainChunks.length; i++) {
|
|
408
|
+
if (!liveActivityMessages[i]) {
|
|
409
|
+
liveActivityMessages[i] = await channel.send({ content: plainChunks[i] }).catch(() => null);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
await liveActivityMessages[i].edit({ content: plainChunks[i] }).catch(async () => {
|
|
413
|
+
liveActivityMessages[i] = await channel.send({ content: plainChunks[i] }).catch(() => null);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
while (liveActivityMessages.length > plainChunks.length) {
|
|
417
|
+
const extra = liveActivityMessages.pop();
|
|
418
|
+
if (!extra)
|
|
419
|
+
continue;
|
|
420
|
+
await extra.delete().catch(() => { });
|
|
421
|
+
}
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
350
424
|
const descriptions = buildLiveActivityDescriptions(rawText);
|
|
351
425
|
const renderKey = `${title}|${color}|${footerText}|${descriptions.join('\n<<<PAGE_BREAK>>>\n')}`;
|
|
352
426
|
if (renderKey === lastLiveActivityKey && liveActivityMessages.length > 0) {
|
|
@@ -392,6 +466,7 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
|
|
|
392
466
|
await sendEmbed(`${PHASE_ICONS.error} Message Injection Failed`, `Failed to send message: ${injectResult.error}`, PHASE_COLORS.error);
|
|
393
467
|
await clearWatchingReaction();
|
|
394
468
|
await message.react('❌').catch(() => { });
|
|
469
|
+
signalCompletion('inject-failed');
|
|
395
470
|
return;
|
|
396
471
|
}
|
|
397
472
|
const startTime = Date.now();
|
|
@@ -430,122 +505,127 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
|
|
|
430
505
|
},
|
|
431
506
|
onComplete: async (finalText) => {
|
|
432
507
|
isFinalized = true;
|
|
433
|
-
// If the user explicitly pressed /stop, skip output display entirely
|
|
434
|
-
const wasStoppedByUser = userStopRequestedChannels.delete(message.channelId);
|
|
435
|
-
if (wasStoppedByUser) {
|
|
436
|
-
logger_1.logger.info(`[sendPromptToAntigravity:${monitorTraceId}] Stopped by user — skipping output`);
|
|
437
|
-
await clearWatchingReaction();
|
|
438
|
-
await message.react('⏹️').catch(() => { });
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
508
|
try {
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
509
|
+
// If the user explicitly pressed /stop, skip output display entirely
|
|
510
|
+
const wasStoppedByUser = userStopRequestedChannels.delete(message.channelId);
|
|
511
|
+
if (wasStoppedByUser) {
|
|
512
|
+
logger_1.logger.info(`[sendPromptToAntigravity:${monitorTraceId}] Stopped by user — skipping output`);
|
|
513
|
+
await clearWatchingReaction();
|
|
514
|
+
await message.react('⏹️').catch(() => { });
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
519
|
+
const isQuotaError = monitor.getPhase() === 'quotaReached' || monitor.getQuotaDetected();
|
|
520
|
+
// Quota early exit — skip text extraction, output logging, and embed entirely
|
|
521
|
+
if (isQuotaError) {
|
|
522
|
+
const finalLogText = lastActivityLogText || processLogBuffer.snapshot();
|
|
523
|
+
if (finalLogText && finalLogText.trim().length > 0) {
|
|
524
|
+
logger_1.logger.divider('Process Log');
|
|
525
|
+
console.info(finalLogText);
|
|
526
|
+
}
|
|
527
|
+
logger_1.logger.divider();
|
|
528
|
+
liveActivityUpdateVersion += 1;
|
|
529
|
+
await upsertLiveActivityEmbeds(`${PHASE_ICONS.thinking} Process Log`, finalLogText || ACTIVITY_PLACEHOLDER, PHASE_COLORS.thinking, (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Process log`), {
|
|
530
|
+
source: 'complete',
|
|
531
|
+
expectedVersion: liveActivityUpdateVersion,
|
|
532
|
+
});
|
|
533
|
+
liveResponseUpdateVersion += 1;
|
|
534
|
+
await upsertLiveResponseEmbeds('⚠️ Model Quota Reached', 'Model quota limit reached. Please wait or switch to a different model.', 0xFF6B6B, (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Quota Reached`), {
|
|
535
|
+
source: 'complete',
|
|
536
|
+
expectedVersion: liveResponseUpdateVersion,
|
|
537
|
+
});
|
|
538
|
+
try {
|
|
539
|
+
const modelsPayload = await (0, modelsUi_1.buildModelsUI)(cdp, () => bridge.quota.fetchQuota());
|
|
540
|
+
if (modelsPayload && channel) {
|
|
541
|
+
await channel.send({ ...modelsPayload });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
catch (e) {
|
|
545
|
+
logger_1.logger.error('[Quota] Failed to send model selection UI:', e);
|
|
546
|
+
}
|
|
547
|
+
await clearWatchingReaction();
|
|
548
|
+
await message.react('⚠️').catch(() => { });
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
// Normal path — extract final text
|
|
552
|
+
const responseText = (finalText && finalText.trim().length > 0)
|
|
553
|
+
? finalText
|
|
554
|
+
: lastProgressText;
|
|
555
|
+
const emergencyText = (!responseText || responseText.trim().length === 0)
|
|
556
|
+
? await tryEmergencyExtractText()
|
|
557
|
+
: '';
|
|
558
|
+
const finalResponseText = responseText && responseText.trim().length > 0
|
|
559
|
+
? responseText
|
|
560
|
+
: emergencyText;
|
|
561
|
+
const separated = (0, discordFormatter_1.splitOutputAndLogs)(finalResponseText);
|
|
562
|
+
const finalOutputText = separated.output || finalResponseText;
|
|
563
|
+
// Process logs are now collected by onProcessLog callback directly;
|
|
564
|
+
// sanitizeActivityLines is NOT applied because it would strip the very
|
|
565
|
+
// content we want to display (activity messages, tool names, etc.)
|
|
446
566
|
const finalLogText = lastActivityLogText || processLogBuffer.snapshot();
|
|
447
567
|
if (finalLogText && finalLogText.trim().length > 0) {
|
|
448
568
|
logger_1.logger.divider('Process Log');
|
|
449
569
|
console.info(finalLogText);
|
|
450
570
|
}
|
|
571
|
+
if (finalOutputText && finalOutputText.trim().length > 0) {
|
|
572
|
+
logger_1.logger.divider(`Output (${finalOutputText.length} chars)`);
|
|
573
|
+
console.info(finalOutputText);
|
|
574
|
+
}
|
|
451
575
|
logger_1.logger.divider();
|
|
452
576
|
liveActivityUpdateVersion += 1;
|
|
577
|
+
const activityVersion = liveActivityUpdateVersion;
|
|
453
578
|
await upsertLiveActivityEmbeds(`${PHASE_ICONS.thinking} Process Log`, finalLogText || ACTIVITY_PLACEHOLDER, PHASE_COLORS.thinking, (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Process log`), {
|
|
454
579
|
source: 'complete',
|
|
455
|
-
expectedVersion:
|
|
580
|
+
expectedVersion: activityVersion,
|
|
456
581
|
});
|
|
457
582
|
liveResponseUpdateVersion += 1;
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
if (modelsPayload && channel) {
|
|
465
|
-
await channel.send({ ...modelsPayload });
|
|
466
|
-
}
|
|
583
|
+
const responseVersion = liveResponseUpdateVersion;
|
|
584
|
+
if (finalOutputText && finalOutputText.trim().length > 0) {
|
|
585
|
+
await upsertLiveResponseEmbeds(`${PHASE_ICONS.complete} Final Output`, finalOutputText, PHASE_COLORS.complete, (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Complete`), {
|
|
586
|
+
source: 'complete',
|
|
587
|
+
expectedVersion: responseVersion,
|
|
588
|
+
});
|
|
467
589
|
}
|
|
468
|
-
|
|
469
|
-
|
|
590
|
+
else {
|
|
591
|
+
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`), {
|
|
592
|
+
source: 'complete',
|
|
593
|
+
expectedVersion: responseVersion,
|
|
594
|
+
});
|
|
470
595
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
// sanitizeActivityLines is NOT applied because it would strip the very
|
|
489
|
-
// content we want to display (activity messages, tool names, etc.)
|
|
490
|
-
const finalLogText = lastActivityLogText || processLogBuffer.snapshot();
|
|
491
|
-
if (finalLogText && finalLogText.trim().length > 0) {
|
|
492
|
-
logger_1.logger.divider('Process Log');
|
|
493
|
-
console.info(finalLogText);
|
|
494
|
-
}
|
|
495
|
-
if (finalOutputText && finalOutputText.trim().length > 0) {
|
|
496
|
-
logger_1.logger.divider(`Output (${finalOutputText.length} chars)`);
|
|
497
|
-
console.info(finalOutputText);
|
|
498
|
-
}
|
|
499
|
-
logger_1.logger.divider();
|
|
500
|
-
liveActivityUpdateVersion += 1;
|
|
501
|
-
const activityVersion = liveActivityUpdateVersion;
|
|
502
|
-
await upsertLiveActivityEmbeds(`${PHASE_ICONS.thinking} Process Log`, finalLogText || ACTIVITY_PLACEHOLDER, PHASE_COLORS.thinking, (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Process log`), {
|
|
503
|
-
source: 'complete',
|
|
504
|
-
expectedVersion: activityVersion,
|
|
505
|
-
});
|
|
506
|
-
liveResponseUpdateVersion += 1;
|
|
507
|
-
const responseVersion = liveResponseUpdateVersion;
|
|
508
|
-
if (finalOutputText && finalOutputText.trim().length > 0) {
|
|
509
|
-
await upsertLiveResponseEmbeds(`${PHASE_ICONS.complete} Final Output`, finalOutputText, PHASE_COLORS.complete, (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Complete`), {
|
|
510
|
-
source: 'complete',
|
|
511
|
-
expectedVersion: responseVersion,
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
else {
|
|
515
|
-
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`), {
|
|
516
|
-
source: 'complete',
|
|
517
|
-
expectedVersion: responseVersion,
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
if (options && message.guild) {
|
|
521
|
-
try {
|
|
522
|
-
const sessionInfo = await options.chatSessionService.getCurrentSessionInfo(cdp);
|
|
523
|
-
if (sessionInfo && sessionInfo.hasActiveChat && sessionInfo.title && sessionInfo.title !== (0, i18n_1.t)('(Untitled)')) {
|
|
524
|
-
const session = options.chatSessionRepo.findByChannelId(message.channelId);
|
|
525
|
-
const workspaceDirName = session
|
|
526
|
-
? bridge.pool.extractDirName(session.workspacePath)
|
|
527
|
-
: cdp.getCurrentWorkspaceName();
|
|
528
|
-
if (workspaceDirName) {
|
|
529
|
-
(0, cdpBridgeManager_1.registerApprovalSessionChannel)(bridge, workspaceDirName, sessionInfo.title, message.channel);
|
|
530
|
-
}
|
|
531
|
-
const newName = options.titleGenerator.sanitizeForChannelName(sessionInfo.title);
|
|
532
|
-
if (session && session.displayName !== sessionInfo.title) {
|
|
533
|
-
const formattedName = `${session.sessionNumber}-${newName}`;
|
|
534
|
-
await options.channelManager.renameChannel(message.guild, message.channelId, formattedName);
|
|
535
|
-
options.chatSessionRepo.updateDisplayName(message.channelId, sessionInfo.title);
|
|
596
|
+
if (options && message.guild) {
|
|
597
|
+
try {
|
|
598
|
+
const sessionInfo = await options.chatSessionService.getCurrentSessionInfo(cdp);
|
|
599
|
+
if (sessionInfo && sessionInfo.hasActiveChat && sessionInfo.title && sessionInfo.title !== (0, i18n_1.t)('(Untitled)')) {
|
|
600
|
+
const session = options.chatSessionRepo.findByChannelId(message.channelId);
|
|
601
|
+
const projectName = session
|
|
602
|
+
? bridge.pool.extractProjectName(session.workspacePath)
|
|
603
|
+
: cdp.getCurrentWorkspaceName();
|
|
604
|
+
if (projectName) {
|
|
605
|
+
(0, cdpBridgeManager_1.registerApprovalSessionChannel)(bridge, projectName, sessionInfo.title, message.channel);
|
|
606
|
+
}
|
|
607
|
+
const newName = options.titleGenerator.sanitizeForChannelName(sessionInfo.title);
|
|
608
|
+
if (session && session.displayName !== sessionInfo.title) {
|
|
609
|
+
const formattedName = `${session.sessionNumber}-${newName}`;
|
|
610
|
+
await options.channelManager.renameChannel(message.guild, message.channelId, formattedName);
|
|
611
|
+
options.chatSessionRepo.updateDisplayName(message.channelId, sessionInfo.title);
|
|
612
|
+
}
|
|
536
613
|
}
|
|
537
614
|
}
|
|
615
|
+
catch (e) {
|
|
616
|
+
logger_1.logger.error('[Rename] Failed to get title from Antigravity and rename:', e);
|
|
617
|
+
}
|
|
538
618
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
619
|
+
await sendGeneratedImages(finalOutputText || '');
|
|
620
|
+
await clearWatchingReaction();
|
|
621
|
+
await message.react(finalOutputText && finalOutputText.trim().length > 0 ? '✅' : '⚠️').catch(() => { });
|
|
622
|
+
}
|
|
623
|
+
catch (error) {
|
|
624
|
+
logger_1.logger.error(`[sendPromptToAntigravity:${monitorTraceId}] onComplete failed:`, error);
|
|
542
625
|
}
|
|
543
|
-
await sendGeneratedImages(finalOutputText || '');
|
|
544
|
-
await clearWatchingReaction();
|
|
545
|
-
await message.react(finalOutputText && finalOutputText.trim().length > 0 ? '✅' : '⚠️').catch(() => { });
|
|
546
626
|
}
|
|
547
|
-
|
|
548
|
-
|
|
627
|
+
finally {
|
|
628
|
+
signalCompletion('onComplete');
|
|
549
629
|
}
|
|
550
630
|
},
|
|
551
631
|
onTimeout: async (lastText) => {
|
|
@@ -578,6 +658,9 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
|
|
|
578
658
|
catch (error) {
|
|
579
659
|
logger_1.logger.error(`[sendPromptToAntigravity:${monitorTraceId}] onTimeout failed:`, error);
|
|
580
660
|
}
|
|
661
|
+
finally {
|
|
662
|
+
signalCompletion('onTimeout');
|
|
663
|
+
}
|
|
581
664
|
},
|
|
582
665
|
});
|
|
583
666
|
await monitor.start();
|
|
@@ -602,18 +685,21 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
|
|
|
602
685
|
await sendEmbed(`${PHASE_ICONS.error} Error`, (0, i18n_1.t)(`Error occurred during processing: ${e.message}`), PHASE_COLORS.error);
|
|
603
686
|
await clearWatchingReaction();
|
|
604
687
|
await message.react('❌').catch(() => { });
|
|
688
|
+
signalCompletion('top-level-catch');
|
|
605
689
|
}
|
|
606
690
|
}
|
|
607
691
|
// =============================================================================
|
|
608
692
|
// Bot main entry point
|
|
609
693
|
// =============================================================================
|
|
610
|
-
const startBot = async () => {
|
|
694
|
+
const startBot = async (cliLogLevel) => {
|
|
611
695
|
const config = (0, config_1.loadConfig)();
|
|
696
|
+
logger_1.logger.setLogLevel(cliLogLevel ?? config.logLevel);
|
|
612
697
|
const dbPath = process.env.NODE_ENV === 'test' ? ':memory:' : 'antigravity.db';
|
|
613
698
|
const db = new better_sqlite3_1.default(dbPath);
|
|
614
699
|
const modeService = new modeService_1.ModeService();
|
|
615
700
|
const modelService = new modelService_1.ModelService();
|
|
616
701
|
const templateRepo = new templateRepository_1.TemplateRepository(db);
|
|
702
|
+
const userPrefRepo = new userPreferenceRepository_1.UserPreferenceRepository(db);
|
|
617
703
|
const workspaceBindingRepo = new workspaceBindingRepository_1.WorkspaceBindingRepository(db);
|
|
618
704
|
const chatSessionRepo = new chatSessionRepository_1.ChatSessionRepository(db);
|
|
619
705
|
const workspaceService = new workspaceService_1.WorkspaceService(config.workspaceBaseDir);
|
|
@@ -631,7 +717,7 @@ const startBot = async () => {
|
|
|
631
717
|
modelService,
|
|
632
718
|
sendPromptImpl: sendPromptToAntigravity,
|
|
633
719
|
});
|
|
634
|
-
// Initialize command handlers
|
|
720
|
+
// Initialize command handlers (joinHandler is created after client, see below)
|
|
635
721
|
const wsHandler = new workspaceCommandHandler_1.WorkspaceCommandHandler(workspaceBindingRepo, chatSessionRepo, workspaceService, channelManager);
|
|
636
722
|
const chatHandler = new chatCommandHandler_1.ChatCommandHandler(chatSessionService, chatSessionRepo, workspaceBindingRepo, channelManager, workspaceService, bridge.pool);
|
|
637
723
|
const cleanupHandler = new cleanupCommandHandler_1.CleanupCommandHandler(chatSessionRepo, workspaceBindingRepo);
|
|
@@ -643,6 +729,7 @@ const startBot = async () => {
|
|
|
643
729
|
discord_js_1.GatewayIntentBits.MessageContent,
|
|
644
730
|
]
|
|
645
731
|
});
|
|
732
|
+
const joinHandler = new joinCommandHandler_1.JoinCommandHandler(chatSessionService, chatSessionRepo, workspaceBindingRepo, channelManager, bridge.pool, workspaceService, client);
|
|
646
733
|
client.once(discord_js_1.Events.ClientReady, async (readyClient) => {
|
|
647
734
|
logger_1.logger.info(`Ready! Logged in as ${readyClient.user.tag} | extractionMode=${config.extractionMode}`);
|
|
648
735
|
try {
|
|
@@ -700,7 +787,9 @@ const startBot = async () => {
|
|
|
700
787
|
parseApprovalCustomId: cdpBridgeManager_1.parseApprovalCustomId,
|
|
701
788
|
parseErrorPopupCustomId: cdpBridgeManager_1.parseErrorPopupCustomId,
|
|
702
789
|
parsePlanningCustomId: cdpBridgeManager_1.parsePlanningCustomId,
|
|
703
|
-
|
|
790
|
+
joinHandler,
|
|
791
|
+
userPrefRepo,
|
|
792
|
+
handleSlashInteraction: async (interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg) => handleSlashInteraction(interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg, promptDispatcher, templateRepo, joinHandler, userPrefRepo),
|
|
704
793
|
handleTemplateUse: async (interaction, templateId) => {
|
|
705
794
|
const template = templateRepo.findById(templateId);
|
|
706
795
|
if (!template) {
|
|
@@ -717,17 +806,17 @@ const startBot = async () => {
|
|
|
717
806
|
if (workspacePath) {
|
|
718
807
|
try {
|
|
719
808
|
cdp = await bridge.pool.getOrConnect(workspacePath);
|
|
720
|
-
const
|
|
721
|
-
bridge.lastActiveWorkspace =
|
|
809
|
+
const projectName = bridge.pool.extractProjectName(workspacePath);
|
|
810
|
+
bridge.lastActiveWorkspace = projectName;
|
|
722
811
|
bridge.lastActiveChannel = interaction.channel;
|
|
723
|
-
(0, cdpBridgeManager_1.registerApprovalWorkspaceChannel)(bridge,
|
|
812
|
+
(0, cdpBridgeManager_1.registerApprovalWorkspaceChannel)(bridge, projectName, interaction.channel);
|
|
724
813
|
const session = chatSessionRepo.findByChannelId(channelId);
|
|
725
814
|
if (session?.displayName) {
|
|
726
|
-
(0, cdpBridgeManager_1.registerApprovalSessionChannel)(bridge,
|
|
815
|
+
(0, cdpBridgeManager_1.registerApprovalSessionChannel)(bridge, projectName, session.displayName, interaction.channel);
|
|
727
816
|
}
|
|
728
|
-
(0, cdpBridgeManager_1.ensureApprovalDetector)(bridge, cdp,
|
|
729
|
-
(0, cdpBridgeManager_1.ensureErrorPopupDetector)(bridge, cdp,
|
|
730
|
-
(0, cdpBridgeManager_1.ensurePlanningDetector)(bridge, cdp,
|
|
817
|
+
(0, cdpBridgeManager_1.ensureApprovalDetector)(bridge, cdp, projectName, client);
|
|
818
|
+
(0, cdpBridgeManager_1.ensureErrorPopupDetector)(bridge, cdp, projectName, client);
|
|
819
|
+
(0, cdpBridgeManager_1.ensurePlanningDetector)(bridge, cdp, projectName, client);
|
|
731
820
|
}
|
|
732
821
|
catch (e) {
|
|
733
822
|
await interaction.followUp({
|
|
@@ -761,6 +850,7 @@ const startBot = async () => {
|
|
|
761
850
|
chatSessionRepo,
|
|
762
851
|
channelManager,
|
|
763
852
|
titleGenerator,
|
|
853
|
+
userPrefRepo,
|
|
764
854
|
},
|
|
765
855
|
});
|
|
766
856
|
}
|
|
@@ -788,6 +878,7 @@ const startBot = async () => {
|
|
|
788
878
|
}),
|
|
789
879
|
autoRenameChannel,
|
|
790
880
|
handleScreenshot: screenshotUi_1.handleScreenshot,
|
|
881
|
+
userPrefRepo,
|
|
791
882
|
}));
|
|
792
883
|
await client.login(config.discordToken);
|
|
793
884
|
};
|
|
@@ -815,55 +906,82 @@ async function autoRenameChannel(message, chatSessionRepo, titleGenerator, chann
|
|
|
815
906
|
/**
|
|
816
907
|
* Handle Discord Interactions API slash commands
|
|
817
908
|
*/
|
|
818
|
-
async function handleSlashInteraction(interaction, handler, bridge, wsHandler, chatHandler, cleanupHandler, modeService, modelService, autoAcceptService, _client, promptDispatcher, templateRepo) {
|
|
909
|
+
async function handleSlashInteraction(interaction, handler, bridge, wsHandler, chatHandler, cleanupHandler, modeService, modelService, autoAcceptService, _client, promptDispatcher, templateRepo, joinHandler, userPrefRepo) {
|
|
819
910
|
const commandName = interaction.commandName;
|
|
820
911
|
switch (commandName) {
|
|
821
912
|
case 'help': {
|
|
913
|
+
const helpFields = [
|
|
914
|
+
{
|
|
915
|
+
name: '💬 Chat', value: [
|
|
916
|
+
'`/new` — Start a new chat session',
|
|
917
|
+
'`/chat` — Show current session info + list',
|
|
918
|
+
].join('\n')
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
name: '🔗 Session', value: [
|
|
922
|
+
'`/join` — Join an existing Antigravity session',
|
|
923
|
+
'`/mirror` — Toggle PC→Discord mirroring ON/OFF',
|
|
924
|
+
].join('\n')
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
name: '⏹️ Control', value: [
|
|
928
|
+
'`/stop` — Interrupt active LLM generation',
|
|
929
|
+
'`/screenshot` — Capture Antigravity screen',
|
|
930
|
+
].join('\n')
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
name: '⚙️ Settings', value: [
|
|
934
|
+
'`/mode` — Display and change execution mode',
|
|
935
|
+
'`/model [name]` — Display and change LLM model',
|
|
936
|
+
'`/output [format]` — Toggle Embed / Plain Text output',
|
|
937
|
+
].join('\n')
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
name: '📁 Projects', value: [
|
|
941
|
+
'`/project` — Display project list',
|
|
942
|
+
'`/project create <name>` — Create a new project',
|
|
943
|
+
].join('\n')
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
name: '📝 Templates', value: [
|
|
947
|
+
'`/template list` — Show templates with execute buttons (click to run)',
|
|
948
|
+
'`/template add <name> <prompt>` — Register a template',
|
|
949
|
+
'`/template delete <name>` — Delete a template',
|
|
950
|
+
].join('\n')
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
name: '🔧 System', value: [
|
|
954
|
+
'`/status` — Display overall bot status',
|
|
955
|
+
'`/autoaccept` — Toggle auto-approve mode for approval dialogs via buttons',
|
|
956
|
+
'`/logs [lines] [level]` — View recent bot logs',
|
|
957
|
+
'`/cleanup [days]` — Clean up unused channels/categories',
|
|
958
|
+
'`/help` — Show this help',
|
|
959
|
+
].join('\n')
|
|
960
|
+
},
|
|
961
|
+
];
|
|
962
|
+
const helpOutputFormat = userPrefRepo?.getOutputFormat(interaction.user.id) ?? 'embed';
|
|
963
|
+
if (helpOutputFormat === 'plain') {
|
|
964
|
+
const chunks = (0, plainTextFormatter_1.formatAsPlainText)({
|
|
965
|
+
title: '📖 LazyGravity Commands',
|
|
966
|
+
description: 'Commands for controlling Antigravity from Discord.',
|
|
967
|
+
fields: helpFields,
|
|
968
|
+
footerText: 'Text messages are sent directly to Antigravity',
|
|
969
|
+
});
|
|
970
|
+
await interaction.editReply({ content: chunks[0] });
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
822
973
|
const embed = new discord_js_1.EmbedBuilder()
|
|
823
974
|
.setTitle('📖 LazyGravity Commands')
|
|
824
975
|
.setColor(0x5865F2)
|
|
825
976
|
.setDescription('Commands for controlling Antigravity from Discord.')
|
|
826
|
-
.addFields(
|
|
827
|
-
name: '💬 Chat', value: [
|
|
828
|
-
'`/new` — Start a new chat session',
|
|
829
|
-
'`/chat` — Show current session info + list',
|
|
830
|
-
].join('\n')
|
|
831
|
-
}, {
|
|
832
|
-
name: '⏹️ Control', value: [
|
|
833
|
-
'`/stop` — Interrupt active LLM generation',
|
|
834
|
-
'`/screenshot` — Capture Antigravity screen',
|
|
835
|
-
].join('\n')
|
|
836
|
-
}, {
|
|
837
|
-
name: '⚙️ Settings', value: [
|
|
838
|
-
'`/mode` — Display and change execution mode',
|
|
839
|
-
'`/model [name]` — Display and change LLM model',
|
|
840
|
-
].join('\n')
|
|
841
|
-
}, {
|
|
842
|
-
name: '📁 Projects', value: [
|
|
843
|
-
'`/project` — Display project list',
|
|
844
|
-
'`/project create <name>` — Create a new project',
|
|
845
|
-
].join('\n')
|
|
846
|
-
}, {
|
|
847
|
-
name: '📝 Templates', value: [
|
|
848
|
-
'`/template list` — Show templates with execute buttons (click to run)',
|
|
849
|
-
'`/template add <name> <prompt>` — Register a template',
|
|
850
|
-
'`/template delete <name>` — Delete a template',
|
|
851
|
-
].join('\n')
|
|
852
|
-
}, {
|
|
853
|
-
name: '🔧 System', value: [
|
|
854
|
-
'`/status` — Display overall bot status',
|
|
855
|
-
'`/autoaccept` — Toggle auto-approve mode for approval dialogs via buttons',
|
|
856
|
-
'`/cleanup [days]` — Clean up unused channels/categories',
|
|
857
|
-
'`/help` — Show this help',
|
|
858
|
-
].join('\n')
|
|
859
|
-
})
|
|
977
|
+
.addFields(...helpFields)
|
|
860
978
|
.setFooter({ text: 'Text messages are sent directly to Antigravity' })
|
|
861
979
|
.setTimestamp();
|
|
862
980
|
await interaction.editReply({ embeds: [embed] });
|
|
863
981
|
break;
|
|
864
982
|
}
|
|
865
983
|
case 'mode': {
|
|
866
|
-
await (0, modeUi_1.sendModeUI)(interaction, modeService);
|
|
984
|
+
await (0, modeUi_1.sendModeUI)(interaction, modeService, { getCurrentCdp: () => (0, cdpBridgeManager_1.getCurrentCdp)(bridge) });
|
|
867
985
|
break;
|
|
868
986
|
}
|
|
869
987
|
case 'model': {
|
|
@@ -924,23 +1042,46 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
|
|
|
924
1042
|
return cdp ? 'CDP Connected' : 'Disconnected';
|
|
925
1043
|
})();
|
|
926
1044
|
const currentMode = modeService.getCurrentMode();
|
|
927
|
-
const
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1045
|
+
const mirroringWorkspaces = activeNames.filter((name) => bridge.pool.getUserMessageDetector(name)?.isActive());
|
|
1046
|
+
const mirrorStatus = mirroringWorkspaces.length > 0
|
|
1047
|
+
? `📡 ON (${mirroringWorkspaces.join(', ')})`
|
|
1048
|
+
: '⚪ OFF';
|
|
1049
|
+
const statusFields = [
|
|
1050
|
+
{ name: 'CDP Connection', value: activeNames.length > 0 ? `🟢 ${activeNames.length} project(s) connected` : '⚪ Disconnected', inline: true },
|
|
1051
|
+
{ name: 'Mode', value: modeService_1.MODE_DISPLAY_NAMES[currentMode] || currentMode, inline: true },
|
|
1052
|
+
{ name: 'Auto Approve', value: autoAcceptService.isEnabled() ? '🟢 ON' : '⚪ OFF', inline: true },
|
|
1053
|
+
{ name: 'Mirroring', value: mirrorStatus, inline: true },
|
|
1054
|
+
];
|
|
1055
|
+
let statusDescription = '';
|
|
932
1056
|
if (activeNames.length > 0) {
|
|
933
1057
|
const lines = activeNames.map((name) => {
|
|
934
1058
|
const cdp = bridge.pool.getConnected(name);
|
|
935
1059
|
const contexts = cdp ? cdp.getContexts().length : 0;
|
|
936
1060
|
const detectorActive = bridge.pool.getApprovalDetector(name)?.isActive() ? ' [Detecting]' : '';
|
|
937
|
-
|
|
1061
|
+
const mirrorActive = bridge.pool.getUserMessageDetector(name)?.isActive() ? ' [Mirror]' : '';
|
|
1062
|
+
return `• **${name}** — Contexts: ${contexts}${detectorActive}${mirrorActive}`;
|
|
938
1063
|
});
|
|
939
|
-
|
|
1064
|
+
statusDescription = `**Connected Projects:**\n${lines.join('\n')}`;
|
|
940
1065
|
}
|
|
941
1066
|
else {
|
|
942
|
-
|
|
1067
|
+
statusDescription = 'Send a message to auto-connect to a project.';
|
|
1068
|
+
}
|
|
1069
|
+
const statusOutputFormat = userPrefRepo?.getOutputFormat(interaction.user.id) ?? 'embed';
|
|
1070
|
+
if (statusOutputFormat === 'plain') {
|
|
1071
|
+
const chunks = (0, plainTextFormatter_1.formatAsPlainText)({
|
|
1072
|
+
title: '🔧 Bot Status',
|
|
1073
|
+
description: statusDescription,
|
|
1074
|
+
fields: statusFields,
|
|
1075
|
+
});
|
|
1076
|
+
await interaction.editReply({ content: chunks[0] });
|
|
1077
|
+
break;
|
|
943
1078
|
}
|
|
1079
|
+
const embed = new discord_js_1.EmbedBuilder()
|
|
1080
|
+
.setTitle('🔧 Bot Status')
|
|
1081
|
+
.setColor(activeNames.length > 0 ? 0x00CC88 : 0x888888)
|
|
1082
|
+
.addFields(...statusFields)
|
|
1083
|
+
.setDescription(statusDescription)
|
|
1084
|
+
.setTimestamp();
|
|
944
1085
|
await interaction.editReply({ embeds: [embed] });
|
|
945
1086
|
break;
|
|
946
1087
|
}
|
|
@@ -954,6 +1095,23 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
|
|
|
954
1095
|
await interaction.editReply({ content: result.message });
|
|
955
1096
|
break;
|
|
956
1097
|
}
|
|
1098
|
+
case 'output': {
|
|
1099
|
+
if (!userPrefRepo) {
|
|
1100
|
+
await interaction.editReply({ content: 'Output preference service not available.' });
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1103
|
+
const requestedFormat = interaction.options.getString('format');
|
|
1104
|
+
if (!requestedFormat) {
|
|
1105
|
+
const currentFormat = userPrefRepo.getOutputFormat(interaction.user.id);
|
|
1106
|
+
await (0, outputUi_1.sendOutputUI)(interaction, currentFormat);
|
|
1107
|
+
break;
|
|
1108
|
+
}
|
|
1109
|
+
const format = requestedFormat === 'plain' ? 'plain' : 'embed';
|
|
1110
|
+
userPrefRepo.setOutputFormat(interaction.user.id, format);
|
|
1111
|
+
const label = format === 'plain' ? 'Plain Text' : 'Embed';
|
|
1112
|
+
await interaction.editReply({ content: `Output format changed to **${label}**.` });
|
|
1113
|
+
break;
|
|
1114
|
+
}
|
|
957
1115
|
case 'screenshot': {
|
|
958
1116
|
await (0, screenshotUi_1.handleScreenshot)(interaction, (0, cdpBridgeManager_1.getCurrentCdp)(bridge));
|
|
959
1117
|
break;
|
|
@@ -1022,6 +1180,24 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
|
|
|
1022
1180
|
await chatHandler.handleChat(interaction);
|
|
1023
1181
|
break;
|
|
1024
1182
|
}
|
|
1183
|
+
case 'join': {
|
|
1184
|
+
if (joinHandler) {
|
|
1185
|
+
await joinHandler.handleJoin(interaction, bridge);
|
|
1186
|
+
}
|
|
1187
|
+
else {
|
|
1188
|
+
await interaction.editReply({ content: (0, i18n_1.t)('⚠️ Join handler not available.') });
|
|
1189
|
+
}
|
|
1190
|
+
break;
|
|
1191
|
+
}
|
|
1192
|
+
case 'mirror': {
|
|
1193
|
+
if (joinHandler) {
|
|
1194
|
+
await joinHandler.handleMirror(interaction, bridge);
|
|
1195
|
+
}
|
|
1196
|
+
else {
|
|
1197
|
+
await interaction.editReply({ content: (0, i18n_1.t)('⚠️ Mirror handler not available.') });
|
|
1198
|
+
}
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1025
1201
|
case 'cleanup': {
|
|
1026
1202
|
await cleanupHandler.handleCleanup(interaction);
|
|
1027
1203
|
break;
|
|
@@ -1031,6 +1207,24 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
|
|
|
1031
1207
|
await interaction.editReply({ content: `🏓 Pong! API Latency is **${apiLatency}ms**.` });
|
|
1032
1208
|
break;
|
|
1033
1209
|
}
|
|
1210
|
+
case 'logs': {
|
|
1211
|
+
const lines = interaction.options.getInteger('lines') ?? 50;
|
|
1212
|
+
const level = interaction.options.getString('level');
|
|
1213
|
+
const entries = logBuffer_1.logBuffer.getRecent(lines, level ?? undefined);
|
|
1214
|
+
if (entries.length === 0) {
|
|
1215
|
+
await interaction.editReply({ content: 'No log entries found.' });
|
|
1216
|
+
break;
|
|
1217
|
+
}
|
|
1218
|
+
const formatted = entries
|
|
1219
|
+
.map((e) => `${e.timestamp.slice(11, 19)} ${e.message}`)
|
|
1220
|
+
.join('\n');
|
|
1221
|
+
const MAX_CONTENT = 1900;
|
|
1222
|
+
const codeBlock = formatted.length <= MAX_CONTENT
|
|
1223
|
+
? `\`\`\`\n${formatted}\n\`\`\``
|
|
1224
|
+
: `\`\`\`\n${formatted.slice(0, MAX_CONTENT)}\n\`\`\`\n(truncated — showing ${MAX_CONTENT} chars of ${formatted.length})`;
|
|
1225
|
+
await interaction.editReply({ content: codeBlock });
|
|
1226
|
+
break;
|
|
1227
|
+
}
|
|
1034
1228
|
default:
|
|
1035
1229
|
await interaction.editReply({
|
|
1036
1230
|
content: `Unknown command: /${commandName}`,
|