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,134 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ScheduleService = void 0;
37
+ const cron = __importStar(require("node-cron"));
38
+ /**
39
+ * Service class for managing scheduled jobs.
40
+ *
41
+ * - On bot startup, loads schedules from SQLite and re-registers them with node-cron
42
+ * - Handles adding, removing, and listing schedules
43
+ * - Provides bulk stop of all schedules (e.g. on shutdown)
44
+ */
45
+ class ScheduleService {
46
+ repo;
47
+ /** Map managing active cron tasks (schedule ID -> ScheduledTask) */
48
+ activeTasks = new Map();
49
+ constructor(repo) {
50
+ this.repo = repo;
51
+ }
52
+ /**
53
+ * Called on bot startup. Loads all enabled schedules from DB and registers/resumes them with node-cron.
54
+ *
55
+ * @param jobCallback - Callback invoked when each job executes
56
+ * @returns Number of restored schedules
57
+ */
58
+ restoreAll(jobCallback) {
59
+ const enabledSchedules = this.repo.findEnabled();
60
+ for (const schedule of enabledSchedules) {
61
+ this.registerCronTask(schedule, jobCallback);
62
+ }
63
+ return enabledSchedules.length;
64
+ }
65
+ /**
66
+ * Add a new schedule.
67
+ * Processes in order: cron expression validation -> DB save -> node-cron registration.
68
+ *
69
+ * @param cronExpression - Cron expression
70
+ * @param prompt - Prompt to execute
71
+ * @param workspacePath - Target workspace path
72
+ * @param jobCallback - Callback for job execution
73
+ * @returns Created schedule record
74
+ * @throws On invalid cron expression
75
+ */
76
+ addSchedule(cronExpression, prompt, workspacePath, jobCallback) {
77
+ // Validate cron expression
78
+ if (!cron.validate(cronExpression)) {
79
+ throw new Error(`Invalid cron expression: ${cronExpression}`);
80
+ }
81
+ // Save to DB
82
+ const record = this.repo.create({
83
+ cronExpression,
84
+ prompt,
85
+ workspacePath,
86
+ enabled: true,
87
+ });
88
+ // Register with node-cron
89
+ this.registerCronTask(record, jobCallback);
90
+ return record;
91
+ }
92
+ /**
93
+ * Remove a schedule.
94
+ * Stops the running cron job and deletes it from the DB.
95
+ *
96
+ * @param scheduleId - ID of the schedule to remove
97
+ * @returns Whether the removal was successful
98
+ */
99
+ removeSchedule(scheduleId) {
100
+ // Stop the running cron job
101
+ const task = this.activeTasks.get(scheduleId);
102
+ if (task) {
103
+ task.stop();
104
+ this.activeTasks.delete(scheduleId);
105
+ }
106
+ // Delete from DB
107
+ return this.repo.delete(scheduleId);
108
+ }
109
+ /**
110
+ * Stop all running cron jobs (called on bot shutdown)
111
+ */
112
+ stopAll() {
113
+ for (const [id, task] of this.activeTasks) {
114
+ task.stop();
115
+ }
116
+ this.activeTasks.clear();
117
+ }
118
+ /**
119
+ * Get a list of all schedules
120
+ */
121
+ listSchedules() {
122
+ return this.repo.findAll();
123
+ }
124
+ /**
125
+ * Internal method to register a task with node-cron
126
+ */
127
+ registerCronTask(schedule, jobCallback) {
128
+ const task = cron.schedule(schedule.cronExpression, () => {
129
+ jobCallback(schedule);
130
+ });
131
+ this.activeTasks.set(schedule.id, task);
132
+ }
133
+ }
134
+ exports.ScheduleService = ScheduleService;
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ScreenshotService = void 0;
4
+ const logger_1 = require("../utils/logger");
5
+ /**
6
+ * Service for capturing Antigravity UI screenshots
7
+ *
8
+ * Uses the Chrome DevTools Protocol Page.captureScreenshot command
9
+ * to capture the current browser screen and return it as a Buffer sendable to Discord.
10
+ */
11
+ class ScreenshotService {
12
+ cdpService;
13
+ constructor(options) {
14
+ this.cdpService = options.cdpService;
15
+ }
16
+ /**
17
+ * Capture the current screen.
18
+ *
19
+ * @param options Capture options
20
+ * @returns Capture result (Buffer on success, error message on failure)
21
+ */
22
+ async capture(options = {}) {
23
+ try {
24
+ const params = {
25
+ format: options.format ?? 'png',
26
+ };
27
+ if (options.quality !== undefined) {
28
+ params.quality = options.quality;
29
+ }
30
+ if (options.clip) {
31
+ params.clip = options.clip;
32
+ }
33
+ if (options.captureBeyondViewport !== undefined) {
34
+ params.captureBeyondViewport = options.captureBeyondViewport;
35
+ }
36
+ const result = await this.cdpService.call('Page.captureScreenshot', params);
37
+ const base64Data = result?.data ?? '';
38
+ if (!base64Data) {
39
+ return {
40
+ success: false,
41
+ error: 'Screenshot data was empty.',
42
+ };
43
+ }
44
+ const buffer = Buffer.from(base64Data, 'base64');
45
+ return {
46
+ success: true,
47
+ buffer,
48
+ };
49
+ }
50
+ catch (error) {
51
+ const message = error instanceof Error ? error.message : String(error);
52
+ logger_1.logger.error('[ScreenshotService] Error during capture:', error);
53
+ return {
54
+ success: false,
55
+ error: message,
56
+ };
57
+ }
58
+ }
59
+ /**
60
+ * Return a Base64-encoded image string (for use in Discord embeds).
61
+ *
62
+ * @param options Capture options
63
+ * @returns Base64-encoded image string (null on failure)
64
+ */
65
+ async getBase64(options = {}) {
66
+ try {
67
+ const params = {
68
+ format: options.format ?? 'png',
69
+ };
70
+ if (options.quality !== undefined) {
71
+ params.quality = options.quality;
72
+ }
73
+ if (options.clip) {
74
+ params.clip = options.clip;
75
+ }
76
+ const result = await this.cdpService.call('Page.captureScreenshot', params);
77
+ return result?.data ?? null;
78
+ }
79
+ catch (error) {
80
+ logger_1.logger.error('[ScreenshotService] Error while getting Base64:', error);
81
+ return null;
82
+ }
83
+ }
84
+ }
85
+ exports.ScreenshotService = ScreenshotService;
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TitleGeneratorService = void 0;
4
+ /**
5
+ * Script to generate channel name titles using Gemini Flash within the Antigravity UI.
6
+ * Makes LLM calls via Antigravity's internal API (language_server).
7
+ * Infers API endpoint and token from existing DOM components.
8
+ */
9
+ const GENERATE_TITLE_SCRIPT = `(async (userPrompt) => {
10
+ try {
11
+ // Generate title using Antigravity's internal fetch API
12
+ // Look for configuration from __NEXT_DATA__ or window.__remixContext
13
+ const configs = [
14
+ window.__NEXT_DATA__,
15
+ window.__remixContext,
16
+ window.__APP_CONFIG__,
17
+ ].filter(Boolean);
18
+
19
+ // Fallback: extract leading text from prompt
20
+ const fallbackTitle = userPrompt
21
+ .replace(/^\\[.*?\\]\\n?/, '')
22
+ .substring(0, 40)
23
+ .trim();
24
+
25
+ return { ok: true, title: fallbackTitle, method: 'text-extract' };
26
+ } catch (e) {
27
+ return { ok: false, error: e.message };
28
+ }
29
+ })`;
30
+ /**
31
+ * Service for generating chat session titles.
32
+ *
33
+ * Strategy:
34
+ * 1. Call Antigravity's Gemini Flash API via CdpService (future implementation)
35
+ * 2. Fallback: extract and sanitize leading text from the user prompt
36
+ */
37
+ class TitleGeneratorService {
38
+ /**
39
+ * Generate a short title from the user's prompt
40
+ * @param prompt User's prompt
41
+ * @param cdpService Optional CdpService instance
42
+ */
43
+ async generateTitle(prompt, cdpService) {
44
+ // Attempt to use Antigravity's LLM via CDP
45
+ if (cdpService) {
46
+ try {
47
+ const title = await this.generateViaCdp(prompt, cdpService);
48
+ if (title)
49
+ return title;
50
+ }
51
+ catch {
52
+ // Fall through to fallback
53
+ }
54
+ }
55
+ // Fallback: text extraction
56
+ return this.extractTitleFromText(prompt);
57
+ }
58
+ /**
59
+ * Generate a title by calling Antigravity's LLM API via CDP
60
+ */
61
+ async generateViaCdp(prompt, cdpService) {
62
+ try {
63
+ const contextId = cdpService.getPrimaryContextId();
64
+ const cleanPrompt = this.stripWorkspacePrefix(prompt);
65
+ const callParams = {
66
+ expression: `${GENERATE_TITLE_SCRIPT}(${JSON.stringify(cleanPrompt)})`,
67
+ returnByValue: true,
68
+ awaitPromise: true,
69
+ };
70
+ if (contextId !== null) {
71
+ callParams.contextId = contextId;
72
+ }
73
+ const result = await cdpService.call('Runtime.evaluate', callParams);
74
+ const value = result?.result?.value;
75
+ if (value?.ok && value?.title) {
76
+ return this.sanitizeForChannelName(value.title);
77
+ }
78
+ }
79
+ catch {
80
+ // Fall through to fallback
81
+ }
82
+ return null;
83
+ }
84
+ /**
85
+ * Extract a title from the prompt text (fallback)
86
+ */
87
+ extractTitleFromText(prompt) {
88
+ const cleanPrompt = this.stripWorkspacePrefix(prompt);
89
+ const truncated = cleanPrompt.substring(0, 40).trim();
90
+ return this.sanitizeForChannelName(truncated) || 'untitled';
91
+ }
92
+ /**
93
+ * Strip the workspace prefix
94
+ */
95
+ stripWorkspacePrefix(prompt) {
96
+ return prompt.replace(/^\[ワークスペース:.*?\]\n?/, '');
97
+ }
98
+ /**
99
+ * Sanitize text into a format suitable for Discord channel names
100
+ */
101
+ sanitizeForChannelName(text) {
102
+ const sanitized = text
103
+ .toLowerCase()
104
+ .replace(/\s+/g, '-')
105
+ // Allowed in Discord channel names: alphanumeric, hyphen, underscore, CJK characters
106
+ .replace(/[^a-z0-9\-_\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uff00-\uff9f\u4e00-\u9faf]/g, '-')
107
+ .replace(/-{2,}/g, '-')
108
+ .replace(/^-+|-+$/g, '')
109
+ .substring(0, 80);
110
+ return sanitized || 'untitled';
111
+ }
112
+ }
113
+ exports.TitleGeneratorService = TitleGeneratorService;
@@ -0,0 +1,64 @@
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.WorkspaceService = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const sanitize_1 = require("../middleware/sanitize");
9
+ /**
10
+ * Service for workspace filesystem operations and path validation.
11
+ * Manages directories under WORKSPACE_BASE_DIR.
12
+ */
13
+ class WorkspaceService {
14
+ baseDir;
15
+ constructor(baseDir) {
16
+ this.baseDir = baseDir;
17
+ }
18
+ /**
19
+ * Ensure the base directory exists, creating it if necessary
20
+ */
21
+ ensureBaseDir() {
22
+ if (!fs_1.default.existsSync(this.baseDir)) {
23
+ fs_1.default.mkdirSync(this.baseDir, { recursive: true });
24
+ }
25
+ }
26
+ /**
27
+ * Return a list of subdirectories in the base directory
28
+ */
29
+ scanWorkspaces() {
30
+ this.ensureBaseDir();
31
+ const entries = fs_1.default.readdirSync(this.baseDir, { withFileTypes: true });
32
+ return entries
33
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
34
+ .map((entry) => entry.name)
35
+ .sort();
36
+ }
37
+ /**
38
+ * Validate a relative path and return a safe absolute path
39
+ * @throws On path traversal detection
40
+ */
41
+ validatePath(relativePath) {
42
+ return (0, sanitize_1.resolveSafePath)(relativePath, this.baseDir);
43
+ }
44
+ /**
45
+ * Get the base directory path
46
+ */
47
+ getBaseDir() {
48
+ return this.baseDir;
49
+ }
50
+ /**
51
+ * Return the absolute path of the specified workspace
52
+ */
53
+ getWorkspacePath(workspaceName) {
54
+ return this.validatePath(workspaceName);
55
+ }
56
+ /**
57
+ * Check if the specified workspace exists
58
+ */
59
+ exists(workspaceName) {
60
+ const fullPath = this.validatePath(workspaceName);
61
+ return fs_1.default.existsSync(fullPath) && fs_1.default.statSync(fullPath).isDirectory();
62
+ }
63
+ }
64
+ exports.WorkspaceService = WorkspaceService;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AUTOACCEPT_BTN_REFRESH = exports.AUTOACCEPT_BTN_OFF = exports.AUTOACCEPT_BTN_ON = void 0;
4
+ exports.sendAutoAcceptUI = sendAutoAcceptUI;
5
+ const discord_js_1 = require("discord.js");
6
+ exports.AUTOACCEPT_BTN_ON = 'autoaccept_btn_on';
7
+ exports.AUTOACCEPT_BTN_OFF = 'autoaccept_btn_off';
8
+ exports.AUTOACCEPT_BTN_REFRESH = 'autoaccept_btn_refresh';
9
+ async function sendAutoAcceptUI(target, autoAcceptService) {
10
+ const enabled = autoAcceptService.isEnabled();
11
+ const embed = new discord_js_1.EmbedBuilder()
12
+ .setTitle('Auto-accept Management')
13
+ .setColor(enabled ? 0x2ECC71 : 0x95A5A6)
14
+ .setDescription(`**Current Status:** ${enabled ? '🟢 ON' : '⚪ OFF'}\n\n` +
15
+ 'ON: approval dialogs are automatically allowed.\n' +
16
+ 'OFF: approval dialogs require manual action.')
17
+ .setFooter({ text: 'Use buttons below to change mode' })
18
+ .setTimestamp();
19
+ const row = new discord_js_1.ActionRowBuilder().addComponents(new discord_js_1.ButtonBuilder()
20
+ .setCustomId(exports.AUTOACCEPT_BTN_ON)
21
+ .setLabel('Turn ON')
22
+ .setStyle(enabled ? discord_js_1.ButtonStyle.Success : discord_js_1.ButtonStyle.Secondary), new discord_js_1.ButtonBuilder()
23
+ .setCustomId(exports.AUTOACCEPT_BTN_OFF)
24
+ .setLabel('Turn OFF')
25
+ .setStyle(!enabled ? discord_js_1.ButtonStyle.Danger : discord_js_1.ButtonStyle.Secondary), new discord_js_1.ButtonBuilder()
26
+ .setCustomId(exports.AUTOACCEPT_BTN_REFRESH)
27
+ .setLabel('Refresh')
28
+ .setStyle(discord_js_1.ButtonStyle.Primary));
29
+ await target.editReply({
30
+ content: '',
31
+ embeds: [embed],
32
+ components: [row],
33
+ });
34
+ }
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sendModeUI = sendModeUI;
4
+ const discord_js_1 = require("discord.js");
5
+ const modeService_1 = require("../services/modeService");
6
+ /**
7
+ * Build and send the interactive UI for the /mode command (dropdown style)
8
+ */
9
+ async function sendModeUI(target, modeService) {
10
+ const currentMode = modeService.getCurrentMode();
11
+ const embed = new discord_js_1.EmbedBuilder()
12
+ .setTitle('Mode Management')
13
+ .setColor(0x57F287)
14
+ .setDescription(`**Current Mode:** ${modeService_1.MODE_DISPLAY_NAMES[currentMode] || currentMode}\n` +
15
+ `${modeService_1.MODE_DESCRIPTIONS[currentMode] || ''}\n\n` +
16
+ `**Available Modes (${modeService_1.AVAILABLE_MODES.length})**\n` +
17
+ modeService_1.AVAILABLE_MODES.map(m => {
18
+ const icon = m === currentMode ? '[x]' : '[ ]';
19
+ return `${icon} **${modeService_1.MODE_DISPLAY_NAMES[m] || m}** — ${modeService_1.MODE_DESCRIPTIONS[m] || ''}`;
20
+ }).join('\n'))
21
+ .setFooter({ text: 'Select a mode from the dropdown below' })
22
+ .setTimestamp();
23
+ const selectMenu = new discord_js_1.StringSelectMenuBuilder()
24
+ .setCustomId('mode_select')
25
+ .setPlaceholder('Select a mode...')
26
+ .addOptions(modeService_1.AVAILABLE_MODES.map(m => ({
27
+ label: modeService_1.MODE_DISPLAY_NAMES[m] || m,
28
+ description: modeService_1.MODE_DESCRIPTIONS[m] || '',
29
+ value: m,
30
+ default: m === currentMode,
31
+ })));
32
+ const row = new discord_js_1.ActionRowBuilder().addComponents(selectMenu);
33
+ await target.editReply({ content: '', embeds: [embed], components: [row] });
34
+ }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sendModelsUI = sendModelsUI;
4
+ const discord_js_1 = require("discord.js");
5
+ /**
6
+ * Build and send the interactive UI for the /models command
7
+ */
8
+ async function sendModelsUI(target, deps) {
9
+ const cdp = deps.getCurrentCdp();
10
+ if (!cdp) {
11
+ await target.editReply({ content: 'Not connected to CDP.' });
12
+ return;
13
+ }
14
+ const models = await cdp.getUiModels();
15
+ const currentModel = await cdp.getCurrentModel();
16
+ const quotaData = await deps.fetchQuota();
17
+ if (models.length === 0) {
18
+ await target.editReply({ content: 'Failed to retrieve model list from Antigravity.' });
19
+ return;
20
+ }
21
+ function formatQuota(mName, current) {
22
+ if (!mName)
23
+ return `${current ? '[x]' : '[ ]'} Unknown`;
24
+ const normalize = (s) => s.toLowerCase().replace(/[\s\-_]/g, '');
25
+ const nName = normalize(mName);
26
+ const q = quotaData.find(q => {
27
+ const nLabel = normalize(q.label);
28
+ const nModel = normalize(q.model || '');
29
+ return nLabel === nName || nModel === nName
30
+ || nName.includes(nLabel) || nLabel.includes(nName)
31
+ || (nModel && (nName.includes(nModel) || nModel.includes(nName)));
32
+ });
33
+ if (!q || !q.quotaInfo)
34
+ return `${current ? '[x]' : '[ ]'} ${mName}`;
35
+ const rem = q.quotaInfo.remainingFraction;
36
+ const resetTime = q.quotaInfo.resetTime ? new Date(q.quotaInfo.resetTime) : null;
37
+ const diffMs = resetTime ? resetTime.getTime() - Date.now() : 0;
38
+ let timeStr = 'Ready';
39
+ if (diffMs > 0) {
40
+ const mins = Math.ceil(diffMs / 60000);
41
+ if (mins < 60)
42
+ timeStr = `${mins}m`;
43
+ else
44
+ timeStr = `${Math.floor(mins / 60)}h ${mins % 60}m`;
45
+ }
46
+ if (rem !== undefined && rem !== null) {
47
+ const percent = Math.round(rem * 100);
48
+ let icon = '🟢';
49
+ if (percent <= 20)
50
+ icon = '🔴';
51
+ else if (percent <= 50)
52
+ icon = '🟡';
53
+ return `${current ? '[x]' : '[ ]'} ${mName} ${icon} ${percent}% (⏱️ ${timeStr})`;
54
+ }
55
+ return `${current ? '[x]' : '[ ]'} ${mName} (⏱️ ${timeStr})`;
56
+ }
57
+ const currentModelFormatted = currentModel ? formatQuota(currentModel, true) : 'Unknown';
58
+ const embed = new discord_js_1.EmbedBuilder()
59
+ .setTitle('Model Management')
60
+ .setColor(0x5865F2)
61
+ .setDescription(`**Current Model:**\n${currentModelFormatted}\n\n` +
62
+ `**Available Models (${models.length})**\n` +
63
+ models.map(m => formatQuota(m, m === currentModel)).join('\n'))
64
+ .setFooter({ text: 'Latest quota information retrieved' })
65
+ .setTimestamp();
66
+ const rows = [];
67
+ let currentRow = new discord_js_1.ActionRowBuilder();
68
+ for (const mName of models.slice(0, 24)) {
69
+ if (currentRow.components.length === 5) {
70
+ rows.push(currentRow);
71
+ currentRow = new discord_js_1.ActionRowBuilder();
72
+ }
73
+ const safeName = mName.length > 80 ? mName.substring(0, 77) + '...' : mName;
74
+ currentRow.addComponents(new discord_js_1.ButtonBuilder()
75
+ .setCustomId(`model_btn_${mName}`)
76
+ .setLabel(safeName)
77
+ .setStyle(mName === currentModel ? discord_js_1.ButtonStyle.Success : discord_js_1.ButtonStyle.Secondary));
78
+ }
79
+ if (currentRow.components.length < 5) {
80
+ currentRow.addComponents(new discord_js_1.ButtonBuilder()
81
+ .setCustomId('model_refresh_btn')
82
+ .setLabel('Refresh')
83
+ .setStyle(discord_js_1.ButtonStyle.Primary));
84
+ rows.push(currentRow);
85
+ }
86
+ else {
87
+ rows.push(currentRow);
88
+ if (rows.length < 5) {
89
+ const refreshRow = new discord_js_1.ActionRowBuilder().addComponents(new discord_js_1.ButtonBuilder()
90
+ .setCustomId('model_refresh_btn')
91
+ .setLabel('Refresh')
92
+ .setStyle(discord_js_1.ButtonStyle.Primary));
93
+ rows.push(refreshRow);
94
+ }
95
+ }
96
+ await target.editReply({ content: '', embeds: [embed], components: rows });
97
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleScreenshot = handleScreenshot;
4
+ const discord_js_1 = require("discord.js");
5
+ const screenshotService_1 = require("../services/screenshotService");
6
+ /**
7
+ * Capture a screenshot and send it to Discord
8
+ */
9
+ async function handleScreenshot(target, cdp) {
10
+ if (!cdp) {
11
+ const content = 'Not connected to Antigravity.';
12
+ if (target instanceof discord_js_1.Message) {
13
+ await target.reply(content);
14
+ }
15
+ else {
16
+ await target.editReply({ content });
17
+ }
18
+ return;
19
+ }
20
+ try {
21
+ const screenshot = new screenshotService_1.ScreenshotService({ cdpService: cdp });
22
+ const result = await screenshot.capture({ format: 'png' });
23
+ if (result.success && result.buffer) {
24
+ const attachment = new discord_js_1.AttachmentBuilder(result.buffer, { name: 'screenshot.png' });
25
+ if (target instanceof discord_js_1.Message) {
26
+ await target.reply({ files: [attachment] });
27
+ }
28
+ else {
29
+ await target.editReply({ files: [attachment] });
30
+ }
31
+ }
32
+ else {
33
+ const content = `Screenshot failed: ${result.error}`;
34
+ if (target instanceof discord_js_1.Message) {
35
+ await target.reply(content);
36
+ }
37
+ else {
38
+ await target.editReply({ content });
39
+ }
40
+ }
41
+ }
42
+ catch (e) {
43
+ const content = `Screenshot error: ${e.message}`;
44
+ if (target instanceof discord_js_1.Message) {
45
+ await target.reply(content);
46
+ }
47
+ else {
48
+ await target.editReply({ content });
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TEMPLATE_BTN_PREFIX = void 0;
4
+ exports.parseTemplateButtonId = parseTemplateButtonId;
5
+ exports.sendTemplateUI = sendTemplateUI;
6
+ const discord_js_1 = require("discord.js");
7
+ /** Button customId prefix. Format: template_btn_<id> */
8
+ exports.TEMPLATE_BTN_PREFIX = 'template_btn_';
9
+ const MAX_PROMPT_PREVIEW_LEN = 60;
10
+ const MAX_BUTTONS = 25;
11
+ /**
12
+ * Extract template ID from a button customId.
13
+ * Returns NaN if the customId does not match the expected format.
14
+ */
15
+ function parseTemplateButtonId(customId) {
16
+ if (!customId.startsWith(exports.TEMPLATE_BTN_PREFIX))
17
+ return NaN;
18
+ return parseInt(customId.slice(exports.TEMPLATE_BTN_PREFIX.length), 10);
19
+ }
20
+ /**
21
+ * Build and send the template list UI with clickable buttons.
22
+ * Follows the same pattern as modelsUi.ts.
23
+ */
24
+ async function sendTemplateUI(target, templates) {
25
+ if (templates.length === 0) {
26
+ const embed = new discord_js_1.EmbedBuilder()
27
+ .setTitle('Template Management')
28
+ .setColor(0x57F287)
29
+ .setDescription('No templates registered.\n\n' +
30
+ 'Use `/template add name:<name> prompt:<prompt>` to add one.')
31
+ .setTimestamp();
32
+ await target.editReply({ content: '', embeds: [embed], components: [] });
33
+ return;
34
+ }
35
+ const truncate = (text, max) => text.length > max ? `${text.substring(0, max - 3)}...` : text;
36
+ const displayTemplates = templates.slice(0, MAX_BUTTONS);
37
+ const hasMore = templates.length > MAX_BUTTONS;
38
+ const description = displayTemplates
39
+ .map((tpl, i) => `**${i + 1}. ${tpl.name}**\n> ${truncate(tpl.prompt, MAX_PROMPT_PREVIEW_LEN)}`)
40
+ .join('\n\n');
41
+ const footerText = hasMore
42
+ ? `${templates.length - MAX_BUTTONS} templates are hidden. Use /template use <name> to execute directly.`
43
+ : 'Click a button to execute the template';
44
+ const embed = new discord_js_1.EmbedBuilder()
45
+ .setTitle('Template Management')
46
+ .setColor(0x57F287)
47
+ .setDescription(`**Registered Templates (${templates.length})**\n\n${description}`)
48
+ .setFooter({ text: footerText })
49
+ .setTimestamp();
50
+ const rows = [];
51
+ let currentRow = new discord_js_1.ActionRowBuilder();
52
+ for (const tpl of displayTemplates) {
53
+ if (currentRow.components.length === 5) {
54
+ rows.push(currentRow);
55
+ currentRow = new discord_js_1.ActionRowBuilder();
56
+ }
57
+ const safeLabel = tpl.name.length > 80 ? `${tpl.name.substring(0, 77)}...` : tpl.name;
58
+ currentRow.addComponents(new discord_js_1.ButtonBuilder()
59
+ .setCustomId(`${exports.TEMPLATE_BTN_PREFIX}${tpl.id}`)
60
+ .setLabel(safeLabel)
61
+ .setStyle(discord_js_1.ButtonStyle.Primary));
62
+ }
63
+ if (currentRow.components.length > 0) {
64
+ rows.push(currentRow);
65
+ }
66
+ await target.editReply({ content: '', embeds: [embed], components: rows });
67
+ }