lazy-gravity 0.3.0 → 0.4.1
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 +1 -0
- package/dist/bin/cli.js +0 -0
- package/dist/bin/commands/open.js +1 -1
- package/dist/bot/index.js +2 -0
- package/dist/bot/telegramCommands.js +51 -1
- package/dist/bot/telegramMessageHandler.js +6 -2
- package/dist/platform/telegram/wrappers.js +4 -0
- package/dist/services/cdpService.js +88 -2
- package/dist/services/chatSessionService.js +60 -11
- package/dist/services/screenshotService.js +2 -2
- package/dist/services/updateCheckService.js +15 -0
- package/package.json +3 -2
- package/dist/commands/joinDetachCommandHandler.js +0 -285
- package/dist/services/retryStore.js +0 -46
- package/dist/ui/buttonUtils.js +0 -33
- package/dist/utils/antigravityPaths.js +0 -94
- package/dist/utils/logFileTransport.js +0 -147
package/README.md
CHANGED
|
@@ -108,6 +108,7 @@ Telegram commands use underscores instead of subcommand syntax (Telegram does no
|
|
|
108
108
|
|
|
109
109
|
- `/project` — Manage workspace bindings (list, select, create)
|
|
110
110
|
- `/project_create <name>` — Create a new workspace directory
|
|
111
|
+
- `/new` — Start a new chat session
|
|
111
112
|
- `/template` — List prompt templates with execute buttons
|
|
112
113
|
- `/template_add <name> <prompt>` — Add a new prompt template
|
|
113
114
|
- `/template_delete <name>` — Delete a prompt template
|
package/dist/bin/cli.js
CHANGED
|
File without changes
|
|
@@ -81,7 +81,7 @@ function openMacOS(port) {
|
|
|
81
81
|
}
|
|
82
82
|
function openWindows(port) {
|
|
83
83
|
return new Promise((resolve, reject) => {
|
|
84
|
-
(0, child_process_1.execFile)(
|
|
84
|
+
(0, child_process_1.execFile)(APP_NAME, [`--remote-debugging-port=${port}`], { shell: true }, (err) => {
|
|
85
85
|
if (err) {
|
|
86
86
|
reject(new Error(`Failed to open ${APP_NAME}: ${err.message}`));
|
|
87
87
|
return;
|
package/dist/bot/index.js
CHANGED
|
@@ -965,6 +965,7 @@ const startBot = async (cliLogLevel) => {
|
|
|
965
965
|
activeMonitors,
|
|
966
966
|
botToken: config.telegramToken,
|
|
967
967
|
botApi: telegramBot.api,
|
|
968
|
+
chatSessionService,
|
|
968
969
|
});
|
|
969
970
|
// Compose select handlers: project select + mode select
|
|
970
971
|
const projectSelectHandler = (0, telegramProjectCommand_1.createTelegramSelectHandler)({
|
|
@@ -1020,6 +1021,7 @@ const startBot = async (cliLogLevel) => {
|
|
|
1020
1021
|
{ command: 'template_add', description: 'Add a prompt template' },
|
|
1021
1022
|
{ command: 'template_delete', description: 'Delete a prompt template' },
|
|
1022
1023
|
{ command: 'project_create', description: 'Create a new workspace' },
|
|
1024
|
+
{ command: 'new', description: 'Start a new chat session' },
|
|
1023
1025
|
{ command: 'logs', description: 'Show recent log entries' },
|
|
1024
1026
|
{ command: 'stop', description: 'Interrupt active LLM generation' },
|
|
1025
1027
|
{ command: 'help', description: 'Show available commands' },
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* /autoaccept — Toggle auto-accept for approval dialogs
|
|
16
16
|
* /template — List and execute prompt templates
|
|
17
17
|
* /logs — Show recent log entries
|
|
18
|
+
* /new — Start a new chat session
|
|
18
19
|
*/
|
|
19
20
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
21
|
if (k2 === undefined) k2 = k;
|
|
@@ -68,7 +69,7 @@ const logger_1 = require("../utils/logger");
|
|
|
68
69
|
// ---------------------------------------------------------------------------
|
|
69
70
|
// Known commands (used by both parser and /help output)
|
|
70
71
|
// ---------------------------------------------------------------------------
|
|
71
|
-
const KNOWN_COMMANDS = ['start', 'help', 'status', 'stop', 'ping', 'mode', 'model', 'screenshot', 'autoaccept', 'template', 'template_add', 'template_delete', 'project_create', 'logs'];
|
|
72
|
+
const KNOWN_COMMANDS = ['start', 'help', 'status', 'stop', 'ping', 'mode', 'model', 'screenshot', 'autoaccept', 'template', 'template_add', 'template_delete', 'project_create', 'logs', 'new'];
|
|
72
73
|
/**
|
|
73
74
|
* Parse a Telegram command from message text.
|
|
74
75
|
*
|
|
@@ -147,6 +148,9 @@ async function handleTelegramCommand(deps, message, parsed) {
|
|
|
147
148
|
case 'logs':
|
|
148
149
|
await handleLogs(message, parsed.args);
|
|
149
150
|
break;
|
|
151
|
+
case 'new':
|
|
152
|
+
await handleNew(deps, message);
|
|
153
|
+
break;
|
|
150
154
|
default:
|
|
151
155
|
// Should not happen — parser filters unknowns
|
|
152
156
|
break;
|
|
@@ -183,6 +187,7 @@ async function handleHelp(message) {
|
|
|
183
187
|
'/template_add — Add a prompt template',
|
|
184
188
|
'/template_delete — Delete a prompt template',
|
|
185
189
|
'/project_create — Create a new workspace',
|
|
190
|
+
'/new — Start a new chat session',
|
|
186
191
|
'/logs — Show recent log entries',
|
|
187
192
|
'/stop — Interrupt active LLM generation',
|
|
188
193
|
'/ping — Check bot latency',
|
|
@@ -409,6 +414,51 @@ async function handleLogs(message, args) {
|
|
|
409
414
|
const truncated = text.length > 4096 ? text.slice(0, 4090) + '\n...' : text;
|
|
410
415
|
await message.reply({ text: truncated }).catch(logger_1.logger.error);
|
|
411
416
|
}
|
|
417
|
+
async function handleNew(deps, message) {
|
|
418
|
+
if (!deps.chatSessionService) {
|
|
419
|
+
await message.reply({ text: 'Chat session service not available.' }).catch(logger_1.logger.error);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
// Resolve workspace binding for this chat
|
|
423
|
+
const chatId = message.channel.id;
|
|
424
|
+
const binding = deps.telegramBindingRepo?.findByChatId(chatId);
|
|
425
|
+
if (!binding) {
|
|
426
|
+
await message.reply({
|
|
427
|
+
text: 'No project is linked to this chat. Use /project to bind a workspace first.',
|
|
428
|
+
}).catch(logger_1.logger.error);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
// Resolve workspace path and connect to CDP
|
|
432
|
+
let cdp;
|
|
433
|
+
try {
|
|
434
|
+
const workspacePath = deps.workspaceService
|
|
435
|
+
? deps.workspaceService.getWorkspacePath(binding.workspacePath)
|
|
436
|
+
: binding.workspacePath;
|
|
437
|
+
cdp = await deps.bridge.pool.getOrConnect(workspacePath);
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
logger_1.logger.error('[TelegramCommand:new] CDP connection failed:', err?.message || err);
|
|
441
|
+
await message.reply({ text: 'Failed to connect to Antigravity.' }).catch(logger_1.logger.error);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
// Start a new chat session
|
|
445
|
+
try {
|
|
446
|
+
const result = await deps.chatSessionService.startNewChat(cdp);
|
|
447
|
+
if (result.ok) {
|
|
448
|
+
await message.reply({ text: 'New chat session started.' }).catch(logger_1.logger.error);
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
logger_1.logger.warn('[TelegramCommand:new] startNewChat failed:', result.error);
|
|
452
|
+
await message.reply({
|
|
453
|
+
text: `Failed to start new chat: ${(0, telegramFormatter_1.escapeHtml)(result.error || 'unknown error')}`,
|
|
454
|
+
}).catch(logger_1.logger.error);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
catch (err) {
|
|
458
|
+
logger_1.logger.error('[TelegramCommand:new] startNewChat threw:', err?.message || err);
|
|
459
|
+
await message.reply({ text: 'Failed to start new chat.' }).catch(logger_1.logger.error);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
412
462
|
// ---------------------------------------------------------------------------
|
|
413
463
|
// Helpers
|
|
414
464
|
// ---------------------------------------------------------------------------
|
|
@@ -17,6 +17,7 @@ const processLogBuffer_1 = require("../utils/processLogBuffer");
|
|
|
17
17
|
const discordFormatter_1 = require("../utils/discordFormatter");
|
|
18
18
|
const telegramProjectCommand_1 = require("./telegramProjectCommand");
|
|
19
19
|
const telegramCommands_1 = require("./telegramCommands");
|
|
20
|
+
const telegramFormatter_1 = require("../platform/telegram/telegramFormatter");
|
|
20
21
|
const defaultModelApplicator_1 = require("../services/defaultModelApplicator");
|
|
21
22
|
const logger_1 = require("../utils/logger");
|
|
22
23
|
const telegramImageHandler_1 = require("../utils/telegramImageHandler");
|
|
@@ -63,6 +64,7 @@ function createTelegramMessageHandler(deps) {
|
|
|
63
64
|
workspaceService: deps.workspaceService,
|
|
64
65
|
fetchQuota: deps.fetchQuota,
|
|
65
66
|
activeMonitors: deps.activeMonitors,
|
|
67
|
+
chatSessionService: deps.chatSessionService,
|
|
66
68
|
}, message, cmd);
|
|
67
69
|
return;
|
|
68
70
|
}
|
|
@@ -211,8 +213,10 @@ function createTelegramMessageHandler(deps) {
|
|
|
211
213
|
}
|
|
212
214
|
if (statusMsg && lastActivityLogText) {
|
|
213
215
|
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
216
|
+
// Escape HTML to prevent Telegram parse_mode errors
|
|
217
|
+
// (activity logs may contain <, >, & from code/paths)
|
|
214
218
|
statusMsg.edit({
|
|
215
|
-
text: `${lastActivityLogText}\n\n⏱️ ${elapsed}s`,
|
|
219
|
+
text: `${(0, telegramFormatter_1.escapeHtml)(lastActivityLogText)}\n\n⏱️ ${elapsed}s`,
|
|
216
220
|
}).catch(() => { });
|
|
217
221
|
}
|
|
218
222
|
},
|
|
@@ -235,7 +239,7 @@ function createTelegramMessageHandler(deps) {
|
|
|
235
239
|
// Update status message with final activity log
|
|
236
240
|
if (statusMsg && finalLogText && finalLogText.trim().length > 0) {
|
|
237
241
|
await statusMsg.edit({
|
|
238
|
-
text: `${finalLogText}\n\n✅ Done in ${elapsed}s`,
|
|
242
|
+
text: `${(0, telegramFormatter_1.escapeHtml)(finalLogText)}\n\n✅ Done in ${elapsed}s`,
|
|
239
243
|
}).catch(() => { });
|
|
240
244
|
}
|
|
241
245
|
else if (statusMsg) {
|
|
@@ -35,6 +35,10 @@ function componentRowsToInlineKeyboard(rows) {
|
|
|
35
35
|
let buttons = [];
|
|
36
36
|
for (const comp of row.components) {
|
|
37
37
|
if (comp.type === 'button') {
|
|
38
|
+
// Telegram inline keyboards do not support disabled buttons;
|
|
39
|
+
// skip them so resolved overlays don't re-show clickable buttons.
|
|
40
|
+
if (comp.disabled)
|
|
41
|
+
continue;
|
|
38
42
|
buttons = [...buttons, buttonDefToInline(comp)];
|
|
39
43
|
}
|
|
40
44
|
else if (comp.type === 'selectMenu') {
|
|
@@ -225,6 +225,26 @@ class CdpService extends events_1.EventEmitter {
|
|
|
225
225
|
this.ws.send(JSON.stringify({ id, method, params }));
|
|
226
226
|
});
|
|
227
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Try call(), and on WebSocket connection error,
|
|
230
|
+
* attempt a single on-demand reconnect then retry once.
|
|
231
|
+
* Non-connection errors (timeout, protocol) are NOT retried.
|
|
232
|
+
*/
|
|
233
|
+
async callWithRetry(method, params = {}, timeoutMs = 10000) {
|
|
234
|
+
try {
|
|
235
|
+
return await this.call(method, params);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
239
|
+
const isConnectionError = message === 'WebSocket is not connected' ||
|
|
240
|
+
message === 'WebSocket disconnected';
|
|
241
|
+
if (!isConnectionError) {
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
await this.reconnectOnDemand(timeoutMs);
|
|
245
|
+
return await this.call(method, params);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
228
248
|
async disconnect() {
|
|
229
249
|
// Stop reconnection attempts
|
|
230
250
|
this.maxReconnectAttempts = 0;
|
|
@@ -644,6 +664,72 @@ class CdpService extends events_1.EventEmitter {
|
|
|
644
664
|
logger_1.logger.error('[CdpService]', finalError.message);
|
|
645
665
|
this.emit('reconnectFailed', finalError);
|
|
646
666
|
}
|
|
667
|
+
/**
|
|
668
|
+
* Wait for an in-progress reconnection to complete.
|
|
669
|
+
* Resolves when 'reconnected' fires, rejects on 'reconnectFailed' or timeout.
|
|
670
|
+
*/
|
|
671
|
+
waitForReconnection(timeoutMs = 15000) {
|
|
672
|
+
return new Promise((resolve, reject) => {
|
|
673
|
+
const timer = setTimeout(() => {
|
|
674
|
+
cleanup();
|
|
675
|
+
reject(new Error('WebSocket is not connected'));
|
|
676
|
+
}, timeoutMs);
|
|
677
|
+
const onReconnected = () => {
|
|
678
|
+
cleanup();
|
|
679
|
+
resolve();
|
|
680
|
+
};
|
|
681
|
+
const onFailed = (_err) => {
|
|
682
|
+
cleanup();
|
|
683
|
+
reject(new Error('WebSocket is not connected'));
|
|
684
|
+
};
|
|
685
|
+
const cleanup = () => {
|
|
686
|
+
clearTimeout(timer);
|
|
687
|
+
this.removeListener('reconnected', onReconnected);
|
|
688
|
+
this.removeListener('reconnectFailed', onFailed);
|
|
689
|
+
};
|
|
690
|
+
this.on('reconnected', onReconnected);
|
|
691
|
+
this.on('reconnectFailed', onFailed);
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
/** Shared promise to coalesce concurrent reconnectOnDemand() calls */
|
|
695
|
+
reconnectOnDemandPromise = null;
|
|
696
|
+
/**
|
|
697
|
+
* On-demand reconnect: if already reconnecting, wait; otherwise attempt once.
|
|
698
|
+
* Throws 'WebSocket is not connected' when no workspace path or reconnect fails.
|
|
699
|
+
*/
|
|
700
|
+
async reconnectOnDemand(timeoutMs = 15000) {
|
|
701
|
+
if (this.isReconnecting) {
|
|
702
|
+
return this.waitForReconnection(timeoutMs);
|
|
703
|
+
}
|
|
704
|
+
if (!this.currentWorkspacePath) {
|
|
705
|
+
throw new Error('WebSocket is not connected');
|
|
706
|
+
}
|
|
707
|
+
// Coalesce concurrent calls
|
|
708
|
+
if (!this.reconnectOnDemandPromise) {
|
|
709
|
+
this.reconnectOnDemandPromise = (async () => {
|
|
710
|
+
try {
|
|
711
|
+
await this.discoverAndConnectForWorkspace(this.currentWorkspacePath);
|
|
712
|
+
}
|
|
713
|
+
catch {
|
|
714
|
+
throw new Error('WebSocket is not connected');
|
|
715
|
+
}
|
|
716
|
+
finally {
|
|
717
|
+
this.reconnectOnDemandPromise = null;
|
|
718
|
+
}
|
|
719
|
+
})();
|
|
720
|
+
}
|
|
721
|
+
let timer;
|
|
722
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
723
|
+
timer = setTimeout(() => reject(new Error('WebSocket is not connected')), timeoutMs);
|
|
724
|
+
});
|
|
725
|
+
try {
|
|
726
|
+
await Promise.race([this.reconnectOnDemandPromise, timeoutPromise]);
|
|
727
|
+
}
|
|
728
|
+
finally {
|
|
729
|
+
if (timer)
|
|
730
|
+
clearTimeout(timer);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
647
733
|
isConnected() {
|
|
648
734
|
return this.isConnectedFlag;
|
|
649
735
|
}
|
|
@@ -1163,7 +1249,7 @@ class CdpService extends events_1.EventEmitter {
|
|
|
1163
1249
|
*/
|
|
1164
1250
|
async setUiMode(modeName) {
|
|
1165
1251
|
if (!this.isConnectedFlag || !this.ws) {
|
|
1166
|
-
|
|
1252
|
+
await this.reconnectOnDemand();
|
|
1167
1253
|
}
|
|
1168
1254
|
const safeMode = JSON.stringify(modeName);
|
|
1169
1255
|
// Internal mode name -> Antigravity UI display name mapping
|
|
@@ -1330,7 +1416,7 @@ class CdpService extends events_1.EventEmitter {
|
|
|
1330
1416
|
*/
|
|
1331
1417
|
async setUiModel(modelName) {
|
|
1332
1418
|
if (!this.isConnectedFlag || !this.ws) {
|
|
1333
|
-
|
|
1419
|
+
await this.reconnectOnDemand();
|
|
1334
1420
|
}
|
|
1335
1421
|
// DOM manipulation script: based on actual Antigravity UI DOM structure
|
|
1336
1422
|
// Model list uses div.cursor-pointer elements with class 'px-2 py-1 flex items-center justify-between'
|
|
@@ -78,12 +78,20 @@ const SCRAPE_PAST_CONVERSATIONS_SCRIPT = `(() => {
|
|
|
78
78
|
const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
|
|
79
79
|
const normalize = (text) => (text || '').trim();
|
|
80
80
|
|
|
81
|
+
// Past Conversations opens as a floating QuickInput dialog, not inside the side panel.
|
|
82
|
+
// Try the visible QuickInput dialog first, then fall back to the side panel.
|
|
83
|
+
const quickInputPanels = Array.from(document.querySelectorAll('div[class*="bg-quickinput-background"]'));
|
|
84
|
+
const panel = quickInputPanels.find((el) => isVisible(el))
|
|
85
|
+
|| document.querySelector('.antigravity-agent-side-panel');
|
|
86
|
+
if (!panel) return null;
|
|
87
|
+
|
|
81
88
|
const items = [];
|
|
82
89
|
const seen = new Set();
|
|
83
90
|
|
|
84
|
-
// Find the scrollable conversation list container
|
|
85
|
-
const containers = Array.from(
|
|
86
|
-
const container = containers.find((c) => isVisible(c) && c.querySelectorAll('div[class*="cursor-pointer"]').length > 0)
|
|
91
|
+
// Find the scrollable conversation list container within the side panel
|
|
92
|
+
const containers = Array.from(panel.querySelectorAll('div[class*="overflow-auto"], div[class*="overflow-y-scroll"]'));
|
|
93
|
+
const container = containers.find((c) => isVisible(c) && c.querySelectorAll('div[class*="cursor-pointer"]').length > 0);
|
|
94
|
+
if (!container) return null;
|
|
87
95
|
|
|
88
96
|
// Detect the "Other Conversations" section boundary.
|
|
89
97
|
// Sessions below this header belong to other projects and must be excluded.
|
|
@@ -131,7 +139,11 @@ const SCRAPE_PAST_CONVERSATIONS_SCRIPT = `(() => {
|
|
|
131
139
|
*/
|
|
132
140
|
const FIND_SHOW_MORE_BUTTON_SCRIPT = `(() => {
|
|
133
141
|
const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
|
|
134
|
-
const
|
|
142
|
+
const quickInputPanels = Array.from(document.querySelectorAll('div[class*="bg-quickinput-background"]'));
|
|
143
|
+
const root = quickInputPanels.find((el) => isVisible(el))
|
|
144
|
+
|| document.querySelector('.antigravity-agent-side-panel')
|
|
145
|
+
|| document;
|
|
146
|
+
const els = Array.from(root.querySelectorAll('div, span'));
|
|
135
147
|
for (const el of els) {
|
|
136
148
|
if (!isVisible(el)) continue;
|
|
137
149
|
const text = (el.textContent || '').trim();
|
|
@@ -436,6 +448,7 @@ class ChatSessionService {
|
|
|
436
448
|
* @returns Array of session list items (empty array on failure)
|
|
437
449
|
*/
|
|
438
450
|
async listAllSessions(cdpService) {
|
|
451
|
+
let panelOpened = false;
|
|
439
452
|
try {
|
|
440
453
|
// Step 1: Find Past Conversations button
|
|
441
454
|
const btnState = await this.evaluateOnAnyContext(cdpService, FIND_PAST_CONVERSATIONS_BUTTON_SCRIPT, false);
|
|
@@ -444,8 +457,32 @@ class ChatSessionService {
|
|
|
444
457
|
}
|
|
445
458
|
// Step 2: Click via CDP mouse events (reliable in Electron)
|
|
446
459
|
await this.cdpMouseClick(cdpService, btnState.x, btnState.y);
|
|
447
|
-
|
|
448
|
-
|
|
460
|
+
panelOpened = true;
|
|
461
|
+
// Step 3: Wait for panel to render (poll for content, up to 3s)
|
|
462
|
+
const PANEL_READY_CHECK = `(() => {
|
|
463
|
+
const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
|
|
464
|
+
const quickInputPanels = Array.from(document.querySelectorAll('div[class*="bg-quickinput-background"]'));
|
|
465
|
+
const panel = quickInputPanels.find((el) => isVisible(el))
|
|
466
|
+
|| document.querySelector('.antigravity-agent-side-panel');
|
|
467
|
+
if (!panel) return false;
|
|
468
|
+
const containers = Array.from(
|
|
469
|
+
panel.querySelectorAll('div[class*="overflow-auto"], div[class*="overflow-y-scroll"]')
|
|
470
|
+
);
|
|
471
|
+
return containers.some((c) =>
|
|
472
|
+
isVisible(c) && c.querySelector('div[class*="cursor-pointer"]')
|
|
473
|
+
);
|
|
474
|
+
})()`;
|
|
475
|
+
let panelReady = false;
|
|
476
|
+
const deadline = Date.now() + 3000;
|
|
477
|
+
while (Date.now() < deadline) {
|
|
478
|
+
panelReady = Boolean(await this.evaluateOnAnyContext(cdpService, PANEL_READY_CHECK, false));
|
|
479
|
+
if (panelReady)
|
|
480
|
+
break;
|
|
481
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
482
|
+
}
|
|
483
|
+
if (!panelReady) {
|
|
484
|
+
return [];
|
|
485
|
+
}
|
|
449
486
|
// Step 4: Scrape sessions
|
|
450
487
|
let scrapeResult = await this.evaluateOnAnyContext(cdpService, SCRAPE_PAST_CONVERSATIONS_SCRIPT, false);
|
|
451
488
|
let sessions = scrapeResult?.sessions ?? [];
|
|
@@ -460,7 +497,22 @@ class ChatSessionService {
|
|
|
460
497
|
sessions = scrapeResult?.sessions ?? [];
|
|
461
498
|
}
|
|
462
499
|
}
|
|
463
|
-
|
|
500
|
+
return sessions.slice(0, ChatSessionService.LIST_SESSIONS_TARGET);
|
|
501
|
+
}
|
|
502
|
+
catch (_) {
|
|
503
|
+
return [];
|
|
504
|
+
}
|
|
505
|
+
finally {
|
|
506
|
+
if (panelOpened) {
|
|
507
|
+
await this.closePanelWithEscape(cdpService);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Close the Past Conversations panel by sending Escape key events.
|
|
513
|
+
*/
|
|
514
|
+
async closePanelWithEscape(cdpService) {
|
|
515
|
+
try {
|
|
464
516
|
await cdpService.call('Input.dispatchKeyEvent', {
|
|
465
517
|
type: 'keyDown', key: 'Escape', code: 'Escape',
|
|
466
518
|
windowsVirtualKeyCode: 27, nativeVirtualKeyCode: 27,
|
|
@@ -469,11 +521,8 @@ class ChatSessionService {
|
|
|
469
521
|
type: 'keyUp', key: 'Escape', code: 'Escape',
|
|
470
522
|
windowsVirtualKeyCode: 27, nativeVirtualKeyCode: 27,
|
|
471
523
|
});
|
|
472
|
-
return sessions.slice(0, ChatSessionService.LIST_SESSIONS_TARGET);
|
|
473
|
-
}
|
|
474
|
-
catch (_) {
|
|
475
|
-
return [];
|
|
476
524
|
}
|
|
525
|
+
catch (_) { /* best-effort cleanup */ }
|
|
477
526
|
}
|
|
478
527
|
/**
|
|
479
528
|
* Evaluate a script on the first context that returns a truthy value.
|
|
@@ -33,7 +33,7 @@ class ScreenshotService {
|
|
|
33
33
|
if (options.captureBeyondViewport !== undefined) {
|
|
34
34
|
params.captureBeyondViewport = options.captureBeyondViewport;
|
|
35
35
|
}
|
|
36
|
-
const result = await this.cdpService.
|
|
36
|
+
const result = await this.cdpService.callWithRetry('Page.captureScreenshot', params);
|
|
37
37
|
const base64Data = result?.data ?? '';
|
|
38
38
|
if (!base64Data) {
|
|
39
39
|
return {
|
|
@@ -73,7 +73,7 @@ class ScreenshotService {
|
|
|
73
73
|
if (options.clip) {
|
|
74
74
|
params.clip = options.clip;
|
|
75
75
|
}
|
|
76
|
-
const result = await this.cdpService.
|
|
76
|
+
const result = await this.cdpService.callWithRetry('Page.captureScreenshot', params);
|
|
77
77
|
return result?.data ?? null;
|
|
78
78
|
}
|
|
79
79
|
catch (error) {
|
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.COOLDOWN_MS = exports.UPDATE_CHECK_FILE = void 0;
|
|
37
37
|
exports.shouldCheckForUpdates = shouldCheckForUpdates;
|
|
38
38
|
exports.fetchLatestVersion = fetchLatestVersion;
|
|
39
|
+
exports.isGlobalInstall = isGlobalInstall;
|
|
39
40
|
exports.checkForUpdates = checkForUpdates;
|
|
40
41
|
const https = __importStar(require("https"));
|
|
41
42
|
const fs = __importStar(require("fs"));
|
|
@@ -127,11 +128,25 @@ function compareSemver(a, b) {
|
|
|
127
128
|
}
|
|
128
129
|
return 0;
|
|
129
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Detect whether the process is running from a global npm install
|
|
133
|
+
* (as opposed to a local dev checkout via `ts-node`, `tsx`, etc.).
|
|
134
|
+
*/
|
|
135
|
+
function isGlobalInstall() {
|
|
136
|
+
const execPath = process.argv[1] || '';
|
|
137
|
+
// Global installs run from a path containing node_modules
|
|
138
|
+
// Local dev runs from the source tree (no node_modules/.bin in argv[1])
|
|
139
|
+
const globalIndicators = ['/lib/node_modules/', '\\node_modules\\lazy-gravity\\'];
|
|
140
|
+
return globalIndicators.some((indicator) => execPath.includes(indicator));
|
|
141
|
+
}
|
|
130
142
|
/**
|
|
131
143
|
* Non-blocking update check. Call at startup (fire-and-forget).
|
|
132
144
|
* Respects a 24-hour cooldown via a local cache file.
|
|
145
|
+
* Skipped when running from source (dev/local checkout).
|
|
133
146
|
*/
|
|
134
147
|
async function checkForUpdates(currentVersion) {
|
|
148
|
+
if (!isGlobalInstall())
|
|
149
|
+
return;
|
|
135
150
|
if (!shouldCheckForUpdates())
|
|
136
151
|
return;
|
|
137
152
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lazy-gravity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Control Antigravity from anywhere — a local, secure bot (Discord + Telegram) that lets you remotely operate Antigravity on your home PC from your smartphone.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"start:built": "node dist/bin/cli.js",
|
|
28
28
|
"dev": "ts-node-dev --respawn src/bin/cli.ts",
|
|
29
29
|
"docs:diagram": "mmdc -i docs/diagrams/architecture.mmd -o docs/images/architecture.svg -b transparent",
|
|
30
|
-
"prepublishOnly": "npm run build
|
|
30
|
+
"prepublishOnly": "npm run build"
|
|
31
31
|
},
|
|
32
32
|
"keywords": [
|
|
33
33
|
"discord",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"jest-environment-jsdom": "^30.2.0",
|
|
68
68
|
"jsdom": "^28.1.0",
|
|
69
69
|
"minimatch": "^10.2.1",
|
|
70
|
+
"semantic-release": "^25.0.3",
|
|
70
71
|
"ts-jest": "^29.4.6",
|
|
71
72
|
"ts-morph": "^27.0.2",
|
|
72
73
|
"ts-node": "^10.9.2",
|
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.JoinDetachCommandHandler = void 0;
|
|
4
|
-
const i18n_1 = require("../utils/i18n");
|
|
5
|
-
const discord_js_1 = require("discord.js");
|
|
6
|
-
const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
|
|
7
|
-
const responseMonitor_1 = require("../services/responseMonitor");
|
|
8
|
-
const sessionPickerUi_1 = require("../ui/sessionPickerUi");
|
|
9
|
-
const logger_1 = require("../utils/logger");
|
|
10
|
-
/** Maximum embed description length (Discord limit is 4096) */
|
|
11
|
-
const MAX_EMBED_DESC = 4000;
|
|
12
|
-
/**
|
|
13
|
-
* Handler for /join and /mirror commands.
|
|
14
|
-
*
|
|
15
|
-
* /join — List Antigravity sessions and connect to one via a select menu.
|
|
16
|
-
* /mirror — Toggle PC-to-Discord message mirroring ON/OFF.
|
|
17
|
-
*/
|
|
18
|
-
class JoinDetachCommandHandler {
|
|
19
|
-
chatSessionService;
|
|
20
|
-
chatSessionRepo;
|
|
21
|
-
bindingRepo;
|
|
22
|
-
channelManager;
|
|
23
|
-
pool;
|
|
24
|
-
client;
|
|
25
|
-
/** Active ResponseMonitors per workspace (for AI response mirroring) */
|
|
26
|
-
activeResponseMonitors = new Map();
|
|
27
|
-
constructor(chatSessionService, chatSessionRepo, bindingRepo, channelManager, pool, client) {
|
|
28
|
-
this.chatSessionService = chatSessionService;
|
|
29
|
-
this.chatSessionRepo = chatSessionRepo;
|
|
30
|
-
this.bindingRepo = bindingRepo;
|
|
31
|
-
this.channelManager = channelManager;
|
|
32
|
-
this.pool = pool;
|
|
33
|
-
this.client = client;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* /join — Show session picker for the workspace bound to this channel.
|
|
37
|
-
*/
|
|
38
|
-
async handleJoin(interaction, bridge) {
|
|
39
|
-
const binding = this.bindingRepo.findByChannelId(interaction.channelId);
|
|
40
|
-
const session = this.chatSessionRepo.findByChannelId(interaction.channelId);
|
|
41
|
-
const workspaceName = binding?.workspacePath ?? session?.workspacePath;
|
|
42
|
-
if (!workspaceName) {
|
|
43
|
-
await interaction.editReply({
|
|
44
|
-
content: (0, i18n_1.t)('⚠️ No project is bound to this channel. Use `/project` first.'),
|
|
45
|
-
});
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
let cdp;
|
|
49
|
-
try {
|
|
50
|
-
cdp = await this.pool.getOrConnect(workspaceName);
|
|
51
|
-
}
|
|
52
|
-
catch (e) {
|
|
53
|
-
await interaction.editReply({
|
|
54
|
-
content: (0, i18n_1.t)(`⚠️ Failed to connect to project: ${e.message}`),
|
|
55
|
-
});
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
const sessions = await this.chatSessionService.listAllSessions(cdp);
|
|
59
|
-
const { embeds, components } = (0, sessionPickerUi_1.buildSessionPickerUI)(sessions);
|
|
60
|
-
await interaction.editReply({ embeds, components });
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Handle session selection from the /join picker.
|
|
64
|
-
*
|
|
65
|
-
* Flow:
|
|
66
|
-
* 1. Check if a channel already exists for this session (by displayName)
|
|
67
|
-
* 2. If yes → reply with a link to that channel
|
|
68
|
-
* 3. If no → create a new channel, bind it, activate session, start mirroring
|
|
69
|
-
*/
|
|
70
|
-
async handleJoinSelect(interaction, bridge) {
|
|
71
|
-
const selectedTitle = interaction.values[0];
|
|
72
|
-
const guild = interaction.guild;
|
|
73
|
-
if (!guild) {
|
|
74
|
-
await interaction.editReply({ content: (0, i18n_1.t)('⚠️ This command can only be used in a server.') });
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
const binding = this.bindingRepo.findByChannelId(interaction.channelId);
|
|
78
|
-
const session = this.chatSessionRepo.findByChannelId(interaction.channelId);
|
|
79
|
-
const workspaceName = binding?.workspacePath ?? session?.workspacePath;
|
|
80
|
-
if (!workspaceName) {
|
|
81
|
-
await interaction.editReply({ content: (0, i18n_1.t)('⚠️ No project is bound to this channel.') });
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
// Step 1: Check if a channel already exists for this session
|
|
85
|
-
const existingSession = this.chatSessionRepo.findByDisplayName(workspaceName, selectedTitle);
|
|
86
|
-
if (existingSession) {
|
|
87
|
-
const embed = new discord_js_1.EmbedBuilder()
|
|
88
|
-
.setTitle((0, i18n_1.t)('🔗 Session Already Connected'))
|
|
89
|
-
.setDescription((0, i18n_1.t)(`This session already has a channel:\n→ <#${existingSession.channelId}>`))
|
|
90
|
-
.setColor(0x3498DB)
|
|
91
|
-
.setTimestamp();
|
|
92
|
-
await interaction.editReply({ embeds: [embed], components: [] });
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
// Step 2: Connect to CDP
|
|
96
|
-
let cdp;
|
|
97
|
-
try {
|
|
98
|
-
cdp = await this.pool.getOrConnect(workspaceName);
|
|
99
|
-
}
|
|
100
|
-
catch (e) {
|
|
101
|
-
await interaction.editReply({ content: (0, i18n_1.t)(`⚠️ Failed to connect to project: ${e.message}`) });
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
// Step 3: Activate the session in Antigravity
|
|
105
|
-
const activateResult = await this.chatSessionService.activateSessionByTitle(cdp, selectedTitle);
|
|
106
|
-
if (!activateResult.ok) {
|
|
107
|
-
await interaction.editReply({ content: (0, i18n_1.t)(`⚠️ Failed to join session: ${activateResult.error}`) });
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
// Step 4: Create a new Discord channel for this session
|
|
111
|
-
const categoryResult = await this.channelManager.ensureCategory(guild, workspaceName);
|
|
112
|
-
const categoryId = categoryResult.categoryId;
|
|
113
|
-
const sessionNumber = this.chatSessionRepo.getNextSessionNumber(categoryId);
|
|
114
|
-
const channelName = this.channelManager.sanitizeChannelName(`${sessionNumber}-${selectedTitle}`);
|
|
115
|
-
const channelResult = await this.channelManager.createSessionChannel(guild, categoryId, channelName);
|
|
116
|
-
const newChannelId = channelResult.channelId;
|
|
117
|
-
// Step 5: Register binding and session
|
|
118
|
-
this.bindingRepo.upsert({
|
|
119
|
-
channelId: newChannelId,
|
|
120
|
-
workspacePath: workspaceName,
|
|
121
|
-
guildId: guild.id,
|
|
122
|
-
});
|
|
123
|
-
this.chatSessionRepo.create({
|
|
124
|
-
channelId: newChannelId,
|
|
125
|
-
categoryId,
|
|
126
|
-
workspacePath: workspaceName,
|
|
127
|
-
sessionNumber,
|
|
128
|
-
guildId: guild.id,
|
|
129
|
-
});
|
|
130
|
-
this.chatSessionRepo.updateDisplayName(newChannelId, selectedTitle);
|
|
131
|
-
// Step 6: Start mirroring (routes dynamically to all bound session channels)
|
|
132
|
-
this.startMirroring(bridge, cdp, workspaceName);
|
|
133
|
-
const embed = new discord_js_1.EmbedBuilder()
|
|
134
|
-
.setTitle((0, i18n_1.t)('🔗 Joined Session'))
|
|
135
|
-
.setDescription((0, i18n_1.t)(`Connected to: **${selectedTitle}**\n→ <#${newChannelId}>\n\n` +
|
|
136
|
-
`📡 Mirroring is **ON** — PC messages will appear in the new channel.\n` +
|
|
137
|
-
`Use \`/mirror\` to toggle.`))
|
|
138
|
-
.setColor(0x2ECC71)
|
|
139
|
-
.setTimestamp();
|
|
140
|
-
await interaction.editReply({ embeds: [embed], components: [] });
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* /mirror — Toggle mirroring ON/OFF for the current channel's workspace.
|
|
144
|
-
*/
|
|
145
|
-
async handleMirror(interaction, bridge) {
|
|
146
|
-
const binding = this.bindingRepo.findByChannelId(interaction.channelId);
|
|
147
|
-
const session = this.chatSessionRepo.findByChannelId(interaction.channelId);
|
|
148
|
-
const workspaceName = binding?.workspacePath ?? session?.workspacePath;
|
|
149
|
-
const dirName = workspaceName ? this.pool.extractDirName(workspaceName) : null;
|
|
150
|
-
if (!dirName || !workspaceName) {
|
|
151
|
-
await interaction.editReply({
|
|
152
|
-
content: (0, i18n_1.t)('⚠️ No project is bound to this channel. Use `/project` first.'),
|
|
153
|
-
});
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
const detector = this.pool.getUserMessageDetector(dirName);
|
|
157
|
-
if (detector?.isActive()) {
|
|
158
|
-
// Turn OFF — stop user message detector and any active response monitor
|
|
159
|
-
detector.stop();
|
|
160
|
-
const responseMonitor = this.activeResponseMonitors.get(dirName);
|
|
161
|
-
if (responseMonitor?.isActive()) {
|
|
162
|
-
await responseMonitor.stop();
|
|
163
|
-
this.activeResponseMonitors.delete(dirName);
|
|
164
|
-
}
|
|
165
|
-
const embed = new discord_js_1.EmbedBuilder()
|
|
166
|
-
.setTitle((0, i18n_1.t)('📡 Mirroring OFF'))
|
|
167
|
-
.setDescription((0, i18n_1.t)('PC-to-Discord message mirroring has been stopped.'))
|
|
168
|
-
.setColor(0x95A5A6)
|
|
169
|
-
.setTimestamp();
|
|
170
|
-
await interaction.editReply({ embeds: [embed] });
|
|
171
|
-
}
|
|
172
|
-
else {
|
|
173
|
-
// Turn ON
|
|
174
|
-
let cdp;
|
|
175
|
-
try {
|
|
176
|
-
cdp = await this.pool.getOrConnect(workspaceName);
|
|
177
|
-
}
|
|
178
|
-
catch (e) {
|
|
179
|
-
await interaction.editReply({
|
|
180
|
-
content: (0, i18n_1.t)(`⚠️ Failed to connect to project: ${e.message}`),
|
|
181
|
-
});
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
this.startMirroring(bridge, cdp, workspaceName);
|
|
185
|
-
const embed = new discord_js_1.EmbedBuilder()
|
|
186
|
-
.setTitle((0, i18n_1.t)('📡 Mirroring ON'))
|
|
187
|
-
.setDescription((0, i18n_1.t)('PC-to-Discord message mirroring is now active.\n' +
|
|
188
|
-
'Messages typed in Antigravity will appear in the corresponding session channel.'))
|
|
189
|
-
.setColor(0x2ECC71)
|
|
190
|
-
.setTimestamp();
|
|
191
|
-
await interaction.editReply({ embeds: [embed] });
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Start user message mirroring for a workspace.
|
|
196
|
-
*
|
|
197
|
-
* When a PC message is detected, the callback resolves the correct Discord
|
|
198
|
-
* channel via chatSessionRepo.findByDisplayName. Only explicitly joined
|
|
199
|
-
* sessions (with a displayName binding) receive mirrored messages.
|
|
200
|
-
*/
|
|
201
|
-
startMirroring(bridge, cdp, workspaceName) {
|
|
202
|
-
const dirName = this.pool.extractDirName(workspaceName);
|
|
203
|
-
(0, cdpBridgeManager_1.ensureUserMessageDetector)(bridge, cdp, dirName, (info) => {
|
|
204
|
-
this.routeMirroredMessage(cdp, dirName, workspaceName, info)
|
|
205
|
-
.catch((err) => {
|
|
206
|
-
logger_1.logger.error('[Mirror] Error routing mirrored message:', err);
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Route a mirrored PC message to the correct Discord channel and
|
|
212
|
-
* start a passive ResponseMonitor to capture the AI response.
|
|
213
|
-
*
|
|
214
|
-
* Routing: chatSessionRepo.findByDisplayName only — no fallbacks.
|
|
215
|
-
* Sessions without an explicit channel binding are silently skipped.
|
|
216
|
-
*/
|
|
217
|
-
async routeMirroredMessage(cdp, dirName, workspaceName, info) {
|
|
218
|
-
const chatTitle = await (0, cdpBridgeManager_1.getCurrentChatTitle)(cdp);
|
|
219
|
-
if (!chatTitle) {
|
|
220
|
-
logger_1.logger.debug('[Mirror] No chat title detected, skipping');
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
const session = this.chatSessionRepo.findByDisplayName(workspaceName, chatTitle);
|
|
224
|
-
if (!session) {
|
|
225
|
-
logger_1.logger.debug(`[Mirror] No bound channel for session "${chatTitle}", skipping`);
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
const channel = this.client.channels.cache.get(session.channelId);
|
|
229
|
-
if (!channel || !('send' in channel))
|
|
230
|
-
return;
|
|
231
|
-
const sendable = channel;
|
|
232
|
-
// Mirror the user message
|
|
233
|
-
const userEmbed = new discord_js_1.EmbedBuilder()
|
|
234
|
-
.setDescription(`🖥️ ${info.text}`)
|
|
235
|
-
.setColor(0x95A5A6)
|
|
236
|
-
.setFooter({ text: `Typed in Antigravity · ${chatTitle}` })
|
|
237
|
-
.setTimestamp();
|
|
238
|
-
await sendable.send({ embeds: [userEmbed] }).catch((err) => {
|
|
239
|
-
logger_1.logger.error('[Mirror] Failed to send user message:', err);
|
|
240
|
-
});
|
|
241
|
-
// Start passive ResponseMonitor to capture the AI response
|
|
242
|
-
this.startResponseMirror(cdp, dirName, sendable, chatTitle);
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Start a passive ResponseMonitor that sends the AI response to Discord
|
|
246
|
-
* when generation completes.
|
|
247
|
-
*/
|
|
248
|
-
startResponseMirror(cdp, dirName, channel, chatTitle) {
|
|
249
|
-
// Stop previous monitor if still running
|
|
250
|
-
const prev = this.activeResponseMonitors.get(dirName);
|
|
251
|
-
if (prev?.isActive()) {
|
|
252
|
-
prev.stop().catch(() => { });
|
|
253
|
-
}
|
|
254
|
-
const monitor = new responseMonitor_1.ResponseMonitor({
|
|
255
|
-
cdpService: cdp,
|
|
256
|
-
pollIntervalMs: 2000,
|
|
257
|
-
maxDurationMs: 300000,
|
|
258
|
-
onComplete: (finalText) => {
|
|
259
|
-
this.activeResponseMonitors.delete(dirName);
|
|
260
|
-
if (!finalText || finalText.trim().length === 0)
|
|
261
|
-
return;
|
|
262
|
-
const text = finalText.length > MAX_EMBED_DESC
|
|
263
|
-
? finalText.slice(0, MAX_EMBED_DESC) + '\n…(truncated)'
|
|
264
|
-
: finalText;
|
|
265
|
-
const embed = new discord_js_1.EmbedBuilder()
|
|
266
|
-
.setDescription(text)
|
|
267
|
-
.setColor(0x5865F2)
|
|
268
|
-
.setFooter({ text: `Antigravity response · ${chatTitle}` })
|
|
269
|
-
.setTimestamp();
|
|
270
|
-
channel.send({ embeds: [embed] }).catch((err) => {
|
|
271
|
-
logger_1.logger.error('[Mirror] Failed to send AI response:', err);
|
|
272
|
-
});
|
|
273
|
-
},
|
|
274
|
-
onTimeout: () => {
|
|
275
|
-
this.activeResponseMonitors.delete(dirName);
|
|
276
|
-
},
|
|
277
|
-
});
|
|
278
|
-
this.activeResponseMonitors.set(dirName, monitor);
|
|
279
|
-
monitor.startPassive().catch((err) => {
|
|
280
|
-
logger_1.logger.error('[Mirror] Failed to start response monitor:', err);
|
|
281
|
-
this.activeResponseMonitors.delete(dirName);
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
exports.JoinDetachCommandHandler = JoinDetachCommandHandler;
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// =============================================================================
|
|
3
|
-
// Retry store — keeps retry info for the Retry button on errors
|
|
4
|
-
// Extracted to avoid circular dependency between bot/index.ts and
|
|
5
|
-
// interactionCreateHandler.ts.
|
|
6
|
-
// =============================================================================
|
|
7
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.RETRY_BTN_PREFIX = void 0;
|
|
9
|
-
exports.storeRetry = storeRetry;
|
|
10
|
-
exports.getRetryInfo = getRetryInfo;
|
|
11
|
-
exports.deleteRetryInfo = deleteRetryInfo;
|
|
12
|
-
exports.RETRY_BTN_PREFIX = 'retry_prompt_';
|
|
13
|
-
const MAX_RETRY_STORE_SIZE = 100;
|
|
14
|
-
/** TTL for retry entries — matches Discord interaction token lifetime (15 min) */
|
|
15
|
-
const RETRY_TTL_MS = 15 * 60 * 1000;
|
|
16
|
-
const retryStore = new Map();
|
|
17
|
-
/** Prune entries older than RETRY_TTL_MS */
|
|
18
|
-
function pruneExpired() {
|
|
19
|
-
const now = Date.now();
|
|
20
|
-
for (const [k, v] of retryStore) {
|
|
21
|
-
if (now - v.createdAt > RETRY_TTL_MS)
|
|
22
|
-
retryStore.delete(k);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
function storeRetry(key, info) {
|
|
26
|
-
pruneExpired();
|
|
27
|
-
if (retryStore.size >= MAX_RETRY_STORE_SIZE) {
|
|
28
|
-
const firstKey = retryStore.keys().next().value;
|
|
29
|
-
if (firstKey !== undefined)
|
|
30
|
-
retryStore.delete(firstKey);
|
|
31
|
-
}
|
|
32
|
-
retryStore.set(key, { ...info, createdAt: Date.now() });
|
|
33
|
-
}
|
|
34
|
-
function getRetryInfo(key) {
|
|
35
|
-
const entry = retryStore.get(key);
|
|
36
|
-
if (!entry)
|
|
37
|
-
return undefined;
|
|
38
|
-
if (Date.now() - entry.createdAt > RETRY_TTL_MS) {
|
|
39
|
-
retryStore.delete(key);
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
return entry;
|
|
43
|
-
}
|
|
44
|
-
function deleteRetryInfo(key) {
|
|
45
|
-
retryStore.delete(key);
|
|
46
|
-
}
|
package/dist/ui/buttonUtils.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
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 the given message component rows.
|
|
7
|
-
* Returns new ActionRows with every button set to disabled.
|
|
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((btn) => btn !== null);
|
|
27
|
-
if (disabledButtons.length === 0)
|
|
28
|
-
return null;
|
|
29
|
-
nextRow.addComponents(...disabledButtons);
|
|
30
|
-
return nextRow;
|
|
31
|
-
})
|
|
32
|
-
.filter((row) => row !== null);
|
|
33
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
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.LogFileTransportImpl = void 0;
|
|
37
|
-
const fs = __importStar(require("fs"));
|
|
38
|
-
const path = __importStar(require("path"));
|
|
39
|
-
const os = __importStar(require("os"));
|
|
40
|
-
const DEFAULT_LOG_DIR = path.join(os.homedir(), '.lazy-gravity', 'logs');
|
|
41
|
-
const LOG_FILE_PREFIX = 'lazy-gravity-';
|
|
42
|
-
const LOG_FILE_EXT = '.log';
|
|
43
|
-
/** Maximum number of log files to keep (default 14 days). */
|
|
44
|
-
const DEFAULT_MAX_FILES = 14;
|
|
45
|
-
/** Maximum size in bytes for a single log file (default 10 MB). */
|
|
46
|
-
const DEFAULT_MAX_SIZE_BYTES = 10 * 1024 * 1024;
|
|
47
|
-
function formatDate(date) {
|
|
48
|
-
const y = date.getFullYear();
|
|
49
|
-
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
50
|
-
const d = String(date.getDate()).padStart(2, '0');
|
|
51
|
-
return `${y}-${m}-${d}`;
|
|
52
|
-
}
|
|
53
|
-
function buildFileName(dateStr) {
|
|
54
|
-
return `${LOG_FILE_PREFIX}${dateStr}${LOG_FILE_EXT}`;
|
|
55
|
-
}
|
|
56
|
-
class LogFileTransportImpl {
|
|
57
|
-
logDir;
|
|
58
|
-
currentDate;
|
|
59
|
-
currentFilePath;
|
|
60
|
-
constructor(logDir = DEFAULT_LOG_DIR) {
|
|
61
|
-
this.logDir = logDir;
|
|
62
|
-
this.currentDate = formatDate(new Date());
|
|
63
|
-
this.currentFilePath = path.join(this.logDir, buildFileName(this.currentDate));
|
|
64
|
-
this.ensureDir();
|
|
65
|
-
this.scheduleCleanup();
|
|
66
|
-
}
|
|
67
|
-
write(level, timestamp, message) {
|
|
68
|
-
this.rollIfNeeded();
|
|
69
|
-
const line = `${timestamp} [${level}] ${message}\n`;
|
|
70
|
-
try {
|
|
71
|
-
fs.appendFileSync(this.currentFilePath, line, 'utf-8');
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
// Silently ignore write errors to avoid crashing the bot
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Remove old log files that exceed maxFiles count or maxSizeBytes per file.
|
|
79
|
-
* Runs asynchronously to avoid blocking startup.
|
|
80
|
-
*/
|
|
81
|
-
cleanup(maxFiles = DEFAULT_MAX_FILES, maxSizeBytes = DEFAULT_MAX_SIZE_BYTES) {
|
|
82
|
-
setImmediate(() => {
|
|
83
|
-
try {
|
|
84
|
-
this.cleanupSync(maxFiles, maxSizeBytes);
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
// Silently ignore cleanup errors
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
/** Synchronous cleanup for testability. */
|
|
92
|
-
cleanupSync(maxFiles = DEFAULT_MAX_FILES, maxSizeBytes = DEFAULT_MAX_SIZE_BYTES) {
|
|
93
|
-
if (!fs.existsSync(this.logDir))
|
|
94
|
-
return;
|
|
95
|
-
const entries = fs
|
|
96
|
-
.readdirSync(this.logDir)
|
|
97
|
-
.filter((f) => f.startsWith(LOG_FILE_PREFIX) && f.endsWith(LOG_FILE_EXT))
|
|
98
|
-
.sort(); // chronological order (YYYY-MM-DD sorts naturally)
|
|
99
|
-
// Remove files exceeding size limit
|
|
100
|
-
for (const entry of entries) {
|
|
101
|
-
const filePath = path.join(this.logDir, entry);
|
|
102
|
-
try {
|
|
103
|
-
const stat = fs.statSync(filePath);
|
|
104
|
-
if (stat.size > maxSizeBytes) {
|
|
105
|
-
fs.unlinkSync(filePath);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
// Ignore stat/unlink errors
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
// Re-read after size-based cleanup
|
|
113
|
-
const remaining = fs
|
|
114
|
-
.readdirSync(this.logDir)
|
|
115
|
-
.filter((f) => f.startsWith(LOG_FILE_PREFIX) && f.endsWith(LOG_FILE_EXT))
|
|
116
|
-
.sort();
|
|
117
|
-
// Remove oldest files if count exceeds limit
|
|
118
|
-
const excess = remaining.length - maxFiles;
|
|
119
|
-
if (excess > 0) {
|
|
120
|
-
for (let i = 0; i < excess; i++) {
|
|
121
|
-
const filePath = path.join(this.logDir, remaining[i]);
|
|
122
|
-
try {
|
|
123
|
-
fs.unlinkSync(filePath);
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
// Ignore unlink errors
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
ensureDir() {
|
|
132
|
-
if (!fs.existsSync(this.logDir)) {
|
|
133
|
-
fs.mkdirSync(this.logDir, { recursive: true });
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
rollIfNeeded() {
|
|
137
|
-
const today = formatDate(new Date());
|
|
138
|
-
if (today !== this.currentDate) {
|
|
139
|
-
this.currentDate = today;
|
|
140
|
-
this.currentFilePath = path.join(this.logDir, buildFileName(today));
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
scheduleCleanup() {
|
|
144
|
-
this.cleanup();
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
exports.LogFileTransportImpl = LogFileTransportImpl;
|