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.
Files changed (34) hide show
  1. package/README.md +18 -6
  2. package/dist/bin/cli.js +18 -18
  3. package/dist/bin/commands/doctor.js +2 -1
  4. package/dist/bin/commands/start.js +25 -2
  5. package/dist/bot/index.js +346 -152
  6. package/dist/commands/joinCommandHandler.js +302 -0
  7. package/dist/commands/joinDetachCommandHandler.js +285 -0
  8. package/dist/commands/registerSlashCommands.js +35 -0
  9. package/dist/database/chatSessionRepository.js +10 -0
  10. package/dist/database/userPreferenceRepository.js +72 -0
  11. package/dist/events/interactionCreateHandler.js +58 -36
  12. package/dist/events/messageCreateHandler.js +158 -53
  13. package/dist/services/antigravityLauncher.js +4 -3
  14. package/dist/services/approvalDetector.js +6 -0
  15. package/dist/services/cdpBridgeManager.js +184 -84
  16. package/dist/services/cdpConnectionPool.js +79 -51
  17. package/dist/services/cdpService.js +149 -51
  18. package/dist/services/chatSessionService.js +229 -8
  19. package/dist/services/errorPopupDetector.js +6 -0
  20. package/dist/services/planningDetector.js +6 -0
  21. package/dist/services/responseMonitor.js +125 -24
  22. package/dist/services/updateCheckService.js +147 -0
  23. package/dist/services/userMessageDetector.js +221 -0
  24. package/dist/ui/modeUi.js +11 -1
  25. package/dist/ui/outputUi.js +30 -0
  26. package/dist/ui/sessionPickerUi.js +48 -0
  27. package/dist/utils/antigravityPaths.js +94 -0
  28. package/dist/utils/configLoader.js +10 -0
  29. package/dist/utils/discordButtonUtils.js +33 -0
  30. package/dist/utils/logBuffer.js +47 -0
  31. package/dist/utils/logger.js +80 -20
  32. package/dist/utils/pathUtils.js +57 -0
  33. package/dist/utils/plainTextFormatter.js +70 -0
  34. package/package.json +4 -4
@@ -0,0 +1,147 @@
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.COOLDOWN_MS = exports.UPDATE_CHECK_FILE = void 0;
37
+ exports.shouldCheckForUpdates = shouldCheckForUpdates;
38
+ exports.fetchLatestVersion = fetchLatestVersion;
39
+ exports.checkForUpdates = checkForUpdates;
40
+ const https = __importStar(require("https"));
41
+ const fs = __importStar(require("fs"));
42
+ const os = __importStar(require("os"));
43
+ const path = __importStar(require("path"));
44
+ const CONFIG_DIR = '.lazy-gravity';
45
+ exports.UPDATE_CHECK_FILE = 'update-check.json';
46
+ exports.COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24 hours
47
+ const REGISTRY_URL = 'https://registry.npmjs.org/lazy-gravity/latest';
48
+ const REQUEST_TIMEOUT_MS = 5000;
49
+ function getCachePath() {
50
+ return path.join(os.homedir(), CONFIG_DIR, exports.UPDATE_CHECK_FILE);
51
+ }
52
+ /**
53
+ * Determine whether enough time has elapsed since the last update check.
54
+ * Returns true if we should query the registry.
55
+ */
56
+ function shouldCheckForUpdates() {
57
+ const cachePath = getCachePath();
58
+ try {
59
+ if (!fs.existsSync(cachePath))
60
+ return true;
61
+ const raw = fs.readFileSync(cachePath, 'utf-8');
62
+ const cache = JSON.parse(raw);
63
+ return Date.now() - cache.lastCheck >= exports.COOLDOWN_MS;
64
+ }
65
+ catch {
66
+ return true;
67
+ }
68
+ }
69
+ /**
70
+ * Query the npm registry for the latest published version.
71
+ */
72
+ function fetchLatestVersion() {
73
+ return new Promise((resolve, reject) => {
74
+ const req = https.get(REGISTRY_URL, (res) => {
75
+ if (res.statusCode !== 200) {
76
+ reject(new Error(`HTTP ${res.statusCode}`));
77
+ return;
78
+ }
79
+ let body = '';
80
+ res.on('data', (chunk) => {
81
+ body += chunk;
82
+ });
83
+ res.on('end', () => {
84
+ try {
85
+ const data = JSON.parse(body);
86
+ resolve(data.version);
87
+ }
88
+ catch (err) {
89
+ reject(err);
90
+ }
91
+ });
92
+ });
93
+ req.on('error', reject);
94
+ req.on('timeout', () => {
95
+ req.destroy();
96
+ reject(new Error('Request timed out'));
97
+ });
98
+ });
99
+ }
100
+ function writeCache() {
101
+ const cachePath = getCachePath();
102
+ const dir = path.dirname(cachePath);
103
+ try {
104
+ if (!fs.existsSync(dir)) {
105
+ fs.mkdirSync(dir, { recursive: true });
106
+ }
107
+ const cache = { lastCheck: Date.now() };
108
+ fs.writeFileSync(cachePath, JSON.stringify(cache), 'utf-8');
109
+ }
110
+ catch {
111
+ // Silently ignore cache write failures
112
+ }
113
+ }
114
+ /**
115
+ * Compare two semver strings. Returns:
116
+ * -1 if a < b, 0 if a === b, 1 if a > b
117
+ */
118
+ function compareSemver(a, b) {
119
+ const partsA = a.split('.').map(Number);
120
+ const partsB = b.split('.').map(Number);
121
+ for (let i = 0; i < 3; i++) {
122
+ const diff = (partsA[i] ?? 0) - (partsB[i] ?? 0);
123
+ if (diff < 0)
124
+ return -1;
125
+ if (diff > 0)
126
+ return 1;
127
+ }
128
+ return 0;
129
+ }
130
+ /**
131
+ * Non-blocking update check. Call at startup (fire-and-forget).
132
+ * Respects a 24-hour cooldown via a local cache file.
133
+ */
134
+ async function checkForUpdates(currentVersion) {
135
+ if (!shouldCheckForUpdates())
136
+ return;
137
+ try {
138
+ const latest = await fetchLatestVersion();
139
+ writeCache();
140
+ if (compareSemver(currentVersion, latest) < 0) {
141
+ console.info(`\n Update available: ${currentVersion} \u2192 ${latest} \u2014 run \x1b[36mnpm i -g lazy-gravity\x1b[0m\n`);
142
+ }
143
+ }
144
+ catch {
145
+ // Silently ignore — update check should never block startup
146
+ }
147
+ }
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UserMessageDetector = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const logger_1 = require("../utils/logger");
6
+ /**
7
+ * Script to detect the latest user message in the Antigravity chat.
8
+ *
9
+ * Antigravity user message DOM structure:
10
+ * <div class="bg-gray-500/15 p-2 rounded-lg w-full text-sm select-text">
11
+ * <div class="flex flex-row items-end gap-2">
12
+ * <div class="flex-1 flex flex-col gap-2">
13
+ * <div>
14
+ * <div class="whitespace-pre-wrap text-sm" style="word-break: break-word;">
15
+ * {user message text}
16
+ * </div>
17
+ * </div>
18
+ * </div>
19
+ * <div> <!-- undo button --> </div>
20
+ * </div>
21
+ * </div>
22
+ */
23
+ const DETECT_USER_MESSAGE_SCRIPT = `(() => {
24
+ const panel = document.querySelector('.antigravity-agent-side-panel');
25
+ const scope = panel || document;
26
+
27
+ // Strategy A (primary): Query .whitespace-pre-wrap elements directly inside
28
+ // user bubble containers. This avoids the parent-container problem where
29
+ // querySelectorAll matches a wrapper that contains multiple bubbles.
30
+ const textEls = scope.querySelectorAll(
31
+ '[class*="bg-gray-500/15"][class*="select-text"] .whitespace-pre-wrap'
32
+ );
33
+
34
+ if (textEls.length > 0) {
35
+ const lastTextEl = textEls[textEls.length - 1];
36
+ const text = (lastTextEl.textContent || '').trim();
37
+ if (text.length > 0) return { text };
38
+ }
39
+
40
+ // Strategy B (fallback): Find individual bubble containers, filtering out
41
+ // any element that itself contains nested bubble elements (i.e., a parent wrapper).
42
+ const userBubbles = Array.from(scope.querySelectorAll(
43
+ '[class*="bg-gray-500/15"][class*="rounded-lg"][class*="select-text"]'
44
+ )).filter(el => !el.querySelector('[class*="bg-gray-500/15"][class*="select-text"]'));
45
+
46
+ if (userBubbles.length === 0) return null;
47
+
48
+ const lastBubble = userBubbles[userBubbles.length - 1];
49
+ const textEl = lastBubble.querySelector('.whitespace-pre-wrap')
50
+ || lastBubble.querySelector('[style*="word-break"]');
51
+
52
+ const text = textEl
53
+ ? (textEl.textContent || '').trim()
54
+ : (lastBubble.textContent || '').trim();
55
+
56
+ if (!text || text.length < 1) return null;
57
+
58
+ return { text };
59
+ })()`;
60
+ /**
61
+ * Normalize text for echo hash comparison.
62
+ * Trims, collapses whitespace, and takes first 200 chars.
63
+ */
64
+ function normalizeForHash(text) {
65
+ return text.trim().replace(/\s+/g, ' ').slice(0, 200);
66
+ }
67
+ /**
68
+ * Compute a short hash for echo prevention.
69
+ */
70
+ function computeEchoHash(text) {
71
+ return (0, node_crypto_1.createHash)('sha256').update(normalizeForHash(text)).digest('hex').slice(0, 16);
72
+ }
73
+ /**
74
+ * Detects user messages posted directly in the Antigravity UI (e.g., from a PC).
75
+ * Follows the ApprovalDetector polling pattern.
76
+ */
77
+ class UserMessageDetector {
78
+ cdpService;
79
+ pollIntervalMs;
80
+ onUserMessage;
81
+ pollTimer = null;
82
+ isRunning = false;
83
+ /** Hash of the last detected message (for duplicate prevention) */
84
+ lastDetectedHash = null;
85
+ /** Set of echo hashes — messages sent by LazyGravity that should be ignored */
86
+ echoHashes = new Set();
87
+ /** Set of all previously detected message hashes (defense-in-depth dedup) */
88
+ seenHashes = new Set();
89
+ static MAX_SEEN_HASHES = 50;
90
+ /** True during the first poll — seeds existing DOM state without firing callback */
91
+ isPriming = false;
92
+ constructor(options) {
93
+ this.cdpService = options.cdpService;
94
+ this.pollIntervalMs = options.pollIntervalMs ?? 2000;
95
+ this.onUserMessage = options.onUserMessage;
96
+ }
97
+ /**
98
+ * Register a message hash as an echo (sent by LazyGravity).
99
+ * When this message is detected in the DOM, it will be skipped.
100
+ */
101
+ addEchoHash(text) {
102
+ const hash = computeEchoHash(text);
103
+ this.echoHashes.add(hash);
104
+ // Auto-cleanup: remove after 60s to prevent memory leak
105
+ setTimeout(() => {
106
+ this.echoHashes.delete(hash);
107
+ }, 60000);
108
+ }
109
+ /** Start monitoring. The first poll seeds the current DOM state without firing the callback. */
110
+ start() {
111
+ if (this.isRunning)
112
+ return;
113
+ this.isRunning = true;
114
+ this.lastDetectedHash = null;
115
+ this.seenHashes.clear();
116
+ this.isPriming = true;
117
+ // echoHashes are intentionally NOT cleared — they have their own 60s TTL
118
+ // and keeping them prevents false echo pickup during rapid stop/start cycles.
119
+ this.schedulePoll();
120
+ }
121
+ /** Stop monitoring. */
122
+ stop() {
123
+ this.isRunning = false;
124
+ if (this.pollTimer) {
125
+ clearTimeout(this.pollTimer);
126
+ this.pollTimer = null;
127
+ }
128
+ }
129
+ /** Returns whether monitoring is currently active. */
130
+ isActive() {
131
+ return this.isRunning;
132
+ }
133
+ /** Add a hash to the seenHashes set, evicting the oldest entry if at capacity. */
134
+ addToSeenHashes(hash) {
135
+ if (this.seenHashes.size >= UserMessageDetector.MAX_SEEN_HASHES) {
136
+ // Evict the oldest entry (first inserted)
137
+ const oldest = this.seenHashes.values().next().value;
138
+ if (oldest !== undefined) {
139
+ this.seenHashes.delete(oldest);
140
+ }
141
+ }
142
+ this.seenHashes.add(hash);
143
+ }
144
+ /** Schedule the next poll. */
145
+ schedulePoll() {
146
+ if (!this.isRunning)
147
+ return;
148
+ this.pollTimer = setTimeout(async () => {
149
+ await this.poll();
150
+ if (this.isRunning) {
151
+ this.schedulePoll();
152
+ }
153
+ }, this.pollIntervalMs);
154
+ }
155
+ /**
156
+ * Single poll iteration:
157
+ * 1. Get latest user message from DOM
158
+ * 2. Check for duplicates and echoes
159
+ * 3. Notify via callback on new detection
160
+ */
161
+ async poll() {
162
+ try {
163
+ const contextId = this.cdpService.getPrimaryContextId();
164
+ const callParams = {
165
+ expression: DETECT_USER_MESSAGE_SCRIPT,
166
+ returnByValue: true,
167
+ awaitPromise: false,
168
+ };
169
+ if (contextId !== null) {
170
+ callParams.contextId = contextId;
171
+ }
172
+ const result = await this.cdpService.call('Runtime.evaluate', callParams);
173
+ const info = result?.result?.value ?? null;
174
+ // Clear priming flag even if DOM is empty (e.g., new/empty chat)
175
+ if (this.isPriming && (!info || !info.text)) {
176
+ this.isPriming = false;
177
+ logger_1.logger.debug('[UserMessageDetector] Primed with empty DOM');
178
+ return;
179
+ }
180
+ if (info && info.text) {
181
+ const hash = computeEchoHash(info.text);
182
+ const preview = info.text.slice(0, 40);
183
+ // First poll: seed the current DOM state without firing callback
184
+ if (this.isPriming) {
185
+ this.isPriming = false;
186
+ this.lastDetectedHash = hash;
187
+ this.addToSeenHashes(hash);
188
+ logger_1.logger.debug(`[UserMessageDetector] Primed with existing message: "${preview}..."`);
189
+ return;
190
+ }
191
+ // Skip if same as last detected message
192
+ if (hash === this.lastDetectedHash)
193
+ return;
194
+ // Skip if already seen (defense-in-depth dedup)
195
+ if (this.seenHashes.has(hash)) {
196
+ logger_1.logger.debug(`[UserMessageDetector] seenHash hit, skipping: "${preview}..."`);
197
+ this.lastDetectedHash = hash;
198
+ return;
199
+ }
200
+ // Skip if this is an echo (sent by LazyGravity)
201
+ if (this.echoHashes.has(hash)) {
202
+ logger_1.logger.debug(`[UserMessageDetector] Echo hash match, skipping: "${preview}..."`);
203
+ this.lastDetectedHash = hash;
204
+ this.addToSeenHashes(hash);
205
+ return;
206
+ }
207
+ this.lastDetectedHash = hash;
208
+ this.addToSeenHashes(hash);
209
+ logger_1.logger.debug(`[UserMessageDetector] New message detected: "${preview}..."`);
210
+ this.onUserMessage(info);
211
+ }
212
+ }
213
+ catch (error) {
214
+ const message = error instanceof Error ? error.message : String(error);
215
+ if (message.includes('WebSocket is not connected'))
216
+ return;
217
+ logger_1.logger.error('[UserMessageDetector] Error during polling:', error);
218
+ }
219
+ }
220
+ }
221
+ exports.UserMessageDetector = UserMessageDetector;
package/dist/ui/modeUi.js CHANGED
@@ -6,7 +6,17 @@ const modeService_1 = require("../services/modeService");
6
6
  /**
7
7
  * Build and send the interactive UI for the /mode command (dropdown style)
8
8
  */
9
- async function sendModeUI(target, modeService) {
9
+ async function sendModeUI(target, modeService, deps) {
10
+ // If CDP is available, query the live mode and sync modeService
11
+ if (deps?.getCurrentCdp) {
12
+ const cdp = deps.getCurrentCdp();
13
+ if (cdp) {
14
+ const liveMode = await cdp.getCurrentMode();
15
+ if (liveMode) {
16
+ modeService.setMode(liveMode);
17
+ }
18
+ }
19
+ }
10
20
  const currentMode = modeService.getCurrentMode();
11
21
  const embed = new discord_js_1.EmbedBuilder()
12
22
  .setTitle('Mode Management')
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OUTPUT_BTN_PLAIN = exports.OUTPUT_BTN_EMBED = void 0;
4
+ exports.sendOutputUI = sendOutputUI;
5
+ const discord_js_1 = require("discord.js");
6
+ exports.OUTPUT_BTN_EMBED = 'output_btn_embed';
7
+ exports.OUTPUT_BTN_PLAIN = 'output_btn_plain';
8
+ async function sendOutputUI(target, currentFormat) {
9
+ const isEmbed = currentFormat === 'embed';
10
+ const embed = new discord_js_1.EmbedBuilder()
11
+ .setTitle('Output Format')
12
+ .setColor(isEmbed ? 0x5865F2 : 0x2ECC71)
13
+ .setDescription(`**Current Format:** ${isEmbed ? '📋 Embed' : '📝 Plain Text'}\n\n` +
14
+ 'Embed: Rich formatting with colored borders (default).\n' +
15
+ 'Plain Text: Simple text output, easy to copy on mobile.')
16
+ .setFooter({ text: 'Use buttons below to change format' })
17
+ .setTimestamp();
18
+ const row = new discord_js_1.ActionRowBuilder().addComponents(new discord_js_1.ButtonBuilder()
19
+ .setCustomId(exports.OUTPUT_BTN_EMBED)
20
+ .setLabel('Embed')
21
+ .setStyle(isEmbed ? discord_js_1.ButtonStyle.Primary : discord_js_1.ButtonStyle.Secondary), new discord_js_1.ButtonBuilder()
22
+ .setCustomId(exports.OUTPUT_BTN_PLAIN)
23
+ .setLabel('Plain Text')
24
+ .setStyle(!isEmbed ? discord_js_1.ButtonStyle.Success : discord_js_1.ButtonStyle.Secondary));
25
+ await target.editReply({
26
+ content: '',
27
+ embeds: [embed],
28
+ components: [row],
29
+ });
30
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SESSION_SELECT_ID = void 0;
4
+ exports.isSessionSelectId = isSessionSelectId;
5
+ exports.buildSessionPickerUI = buildSessionPickerUI;
6
+ const discord_js_1 = require("discord.js");
7
+ const i18n_1 = require("../utils/i18n");
8
+ /** Select menu custom ID for session picker */
9
+ exports.SESSION_SELECT_ID = 'session_select';
10
+ /** Maximum items per select menu (Discord limit) */
11
+ const MAX_SELECT_OPTIONS = 25;
12
+ /**
13
+ * Check if a customId belongs to the session select menu.
14
+ */
15
+ function isSessionSelectId(customId) {
16
+ return customId === exports.SESSION_SELECT_ID;
17
+ }
18
+ /**
19
+ * Build the session picker UI with a select menu.
20
+ *
21
+ * @param sessions - List of sessions from the side panel
22
+ * @returns Object with embeds and components arrays ready for Discord reply
23
+ */
24
+ function buildSessionPickerUI(sessions) {
25
+ const embed = new discord_js_1.EmbedBuilder()
26
+ .setTitle((0, i18n_1.t)('🔗 Join Session'))
27
+ .setColor(0x5865F2)
28
+ .setTimestamp();
29
+ if (sessions.length === 0) {
30
+ embed.setDescription((0, i18n_1.t)('No sessions found in the Antigravity side panel.'));
31
+ return { embeds: [embed], components: [] };
32
+ }
33
+ embed.setDescription((0, i18n_1.t)(`Select a session to join (${sessions.length} found)`));
34
+ const pageItems = sessions.slice(0, MAX_SELECT_OPTIONS);
35
+ const options = pageItems.map((session) => ({
36
+ label: session.title.slice(0, 100),
37
+ value: session.title.slice(0, 100),
38
+ description: session.isActive ? (0, i18n_1.t)('Current') : undefined,
39
+ }));
40
+ const selectMenu = new discord_js_1.StringSelectMenuBuilder()
41
+ .setCustomId(exports.SESSION_SELECT_ID)
42
+ .setPlaceholder((0, i18n_1.t)('Select a session...'))
43
+ .addOptions(options);
44
+ const components = [
45
+ new discord_js_1.ActionRowBuilder().addComponents(selectMenu),
46
+ ];
47
+ return { embeds: [embed], components };
48
+ }
@@ -0,0 +1,94 @@
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.getAntigravityCliPath = getAntigravityCliPath;
37
+ exports.getAntigravityFallback = getAntigravityFallback;
38
+ exports.getAntigravityCdpHint = getAntigravityCdpHint;
39
+ const os = __importStar(require("os"));
40
+ const path = __importStar(require("path"));
41
+ const APP_NAME = 'Antigravity';
42
+ /**
43
+ * Get the Antigravity CLI binary path for the current platform.
44
+ *
45
+ * - macOS: /Applications/Antigravity.app/Contents/Resources/app/bin/antigravity
46
+ * - Windows: %LOCALAPPDATA%\Programs\Antigravity\Antigravity.exe
47
+ * - Linux: antigravity (assumed in PATH)
48
+ */
49
+ function getAntigravityCliPath() {
50
+ switch (process.platform) {
51
+ case 'darwin':
52
+ return '/Applications/Antigravity.app/Contents/Resources/app/bin/antigravity';
53
+ case 'win32': {
54
+ const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
55
+ return path.join(localAppData, 'Programs', APP_NAME, `${APP_NAME}.exe`);
56
+ }
57
+ default:
58
+ return APP_NAME.toLowerCase();
59
+ }
60
+ }
61
+ /**
62
+ * Get fallback launch command and args for opening a workspace.
63
+ *
64
+ * - macOS: open -a Antigravity <path>
65
+ * - Windows: use full exe path with shell (handles spaces in paths)
66
+ * - Linux: antigravity <path>
67
+ */
68
+ function getAntigravityFallback(workspacePath) {
69
+ switch (process.platform) {
70
+ case 'darwin':
71
+ return { command: 'open', args: ['-a', APP_NAME, workspacePath] };
72
+ case 'win32': {
73
+ const exePath = getAntigravityCliPath();
74
+ return { command: exePath, args: [workspacePath], options: { shell: true } };
75
+ }
76
+ default:
77
+ return { command: APP_NAME.toLowerCase(), args: [workspacePath] };
78
+ }
79
+ }
80
+ /**
81
+ * Get a platform-appropriate hint for starting Antigravity with CDP.
82
+ *
83
+ * Used in user-facing messages (Discord embeds, CLI doctor, logs).
84
+ */
85
+ function getAntigravityCdpHint(port = 9222) {
86
+ switch (process.platform) {
87
+ case 'darwin':
88
+ return `open -a ${APP_NAME} --args --remote-debugging-port=${port}`;
89
+ case 'win32':
90
+ return `${APP_NAME}.exe --remote-debugging-port=${port}`;
91
+ default:
92
+ return `${APP_NAME.toLowerCase()} --remote-debugging-port=${port}`;
93
+ }
94
+ }
@@ -92,6 +92,7 @@ function mergeConfig(persisted) {
92
92
  const workspaceBaseDir = expandTilde(rawDir);
93
93
  const guildId = process.env.GUILD_ID ?? persisted.guildId ?? undefined;
94
94
  const autoApproveFileEdits = resolveBoolean(process.env.AUTO_APPROVE_FILE_EDITS, persisted.autoApproveFileEdits, false);
95
+ const logLevel = resolveLogLevel(process.env.LOG_LEVEL, persisted.logLevel);
95
96
  const extractionMode = resolveExtractionMode(process.env.EXTRACTION_MODE, persisted.extractionMode);
96
97
  return {
97
98
  discordToken: token,
@@ -100,6 +101,7 @@ function mergeConfig(persisted) {
100
101
  allowedUserIds,
101
102
  workspaceBaseDir,
102
103
  autoApproveFileEdits,
104
+ logLevel,
103
105
  extractionMode,
104
106
  };
105
107
  }
@@ -116,6 +118,14 @@ function resolveAllowedUserIds(persisted) {
116
118
  }
117
119
  return [];
118
120
  }
121
+ const VALID_LOG_LEVELS = ['debug', 'info', 'warn', 'error', 'none'];
122
+ function resolveLogLevel(envValue, persistedValue) {
123
+ const raw = envValue?.toLowerCase() ?? persistedValue;
124
+ if (raw && VALID_LOG_LEVELS.includes(raw)) {
125
+ return raw;
126
+ }
127
+ return 'info';
128
+ }
119
129
  function resolveExtractionMode(envValue, persistedValue) {
120
130
  const raw = envValue ?? persistedValue;
121
131
  if (raw === 'legacy')
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.disableAllButtons = disableAllButtons;
4
+ const discord_js_1 = require("discord.js");
5
+ /**
6
+ * Disable all buttons in message component rows.
7
+ * Shared utility used by interaction handlers and detector callbacks.
8
+ */
9
+ function disableAllButtons(components) {
10
+ return components
11
+ .map((row) => {
12
+ const rowAny = row;
13
+ if (!Array.isArray(rowAny.components))
14
+ return null;
15
+ const nextRow = new discord_js_1.ActionRowBuilder();
16
+ const disabledButtons = rowAny.components
17
+ .map((component) => {
18
+ const componentType = component?.type ?? component?.data?.type;
19
+ if (componentType !== 2)
20
+ return null;
21
+ const payload = typeof component?.toJSON === 'function'
22
+ ? component.toJSON()
23
+ : component;
24
+ return discord_js_1.ButtonBuilder.from(payload).setDisabled(true);
25
+ })
26
+ .filter((button) => button !== null);
27
+ if (disabledButtons.length === 0)
28
+ return null;
29
+ nextRow.addComponents(...disabledButtons);
30
+ return nextRow;
31
+ })
32
+ .filter((row) => row !== null);
33
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.logBuffer = exports.LogBuffer = void 0;
4
+ const MAX_ENTRIES = 200;
5
+ // Strip ANSI escape codes for clean buffer storage
6
+ const ANSI_REGEX = /\x1b\[[0-9;]*m/g;
7
+ function stripAnsi(text) {
8
+ return text.replace(ANSI_REGEX, '');
9
+ }
10
+ class LogBuffer {
11
+ buffer = [];
12
+ head = 0;
13
+ count = 0;
14
+ append(level, message) {
15
+ const entry = {
16
+ timestamp: new Date().toISOString(),
17
+ level,
18
+ message: stripAnsi(message),
19
+ };
20
+ if (this.count < MAX_ENTRIES) {
21
+ this.buffer.push(entry);
22
+ this.count++;
23
+ }
24
+ else {
25
+ this.buffer[this.head] = entry;
26
+ }
27
+ this.head = (this.head + 1) % MAX_ENTRIES;
28
+ }
29
+ getRecent(count, levelFilter) {
30
+ const all = [];
31
+ for (let i = 0; i < this.count; i++) {
32
+ const idx = (this.head - this.count + i + MAX_ENTRIES * 2) % MAX_ENTRIES;
33
+ all.push(this.buffer[idx]);
34
+ }
35
+ const filtered = levelFilter
36
+ ? all.filter((e) => e.level === levelFilter)
37
+ : all;
38
+ return filtered.slice(-count);
39
+ }
40
+ clear() {
41
+ this.buffer.length = 0;
42
+ this.head = 0;
43
+ this.count = 0;
44
+ }
45
+ }
46
+ exports.LogBuffer = LogBuffer;
47
+ exports.logBuffer = new LogBuffer();