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,204 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerApprovalWorkspaceChannel = registerApprovalWorkspaceChannel;
4
+ exports.registerApprovalSessionChannel = registerApprovalSessionChannel;
5
+ exports.resolveApprovalChannelForCurrentChat = resolveApprovalChannelForCurrentChat;
6
+ exports.buildApprovalCustomId = buildApprovalCustomId;
7
+ exports.parseApprovalCustomId = parseApprovalCustomId;
8
+ exports.initCdpBridge = initCdpBridge;
9
+ exports.getCurrentCdp = getCurrentCdp;
10
+ exports.ensureApprovalDetector = ensureApprovalDetector;
11
+ const discord_js_1 = require("discord.js");
12
+ const i18n_1 = require("../utils/i18n");
13
+ const logger_1 = require("../utils/logger");
14
+ const approvalDetector_1 = require("./approvalDetector");
15
+ const autoAcceptService_1 = require("./autoAcceptService");
16
+ const cdpConnectionPool_1 = require("./cdpConnectionPool");
17
+ const quotaService_1 = require("./quotaService");
18
+ const APPROVE_ACTION_PREFIX = 'approve_action';
19
+ const ALWAYS_ALLOW_ACTION_PREFIX = 'always_allow_action';
20
+ const DENY_ACTION_PREFIX = 'deny_action';
21
+ function normalizeSessionTitle(title) {
22
+ return title.trim().toLowerCase();
23
+ }
24
+ function buildSessionRouteKey(workspaceDirName, sessionTitle) {
25
+ return `${workspaceDirName}::${normalizeSessionTitle(sessionTitle)}`;
26
+ }
27
+ const GET_CURRENT_CHAT_TITLE_SCRIPT = `(() => {
28
+ const panel = document.querySelector('.antigravity-agent-side-panel');
29
+ if (!panel) return '';
30
+ const header = panel.querySelector('div[class*="border-b"]');
31
+ if (!header) return '';
32
+ const titleEl = header.querySelector('div[class*="text-ellipsis"]');
33
+ const title = titleEl ? (titleEl.textContent || '').trim() : '';
34
+ if (!title || title === 'Agent') return '';
35
+ return title;
36
+ })()`;
37
+ async function getCurrentChatTitle(cdp) {
38
+ const contexts = cdp.getContexts();
39
+ for (const ctx of contexts) {
40
+ try {
41
+ const result = await cdp.call('Runtime.evaluate', {
42
+ expression: GET_CURRENT_CHAT_TITLE_SCRIPT,
43
+ returnByValue: true,
44
+ contextId: ctx.id,
45
+ });
46
+ const value = result?.result?.value;
47
+ if (typeof value === 'string' && value.trim().length > 0) {
48
+ return value.trim();
49
+ }
50
+ }
51
+ catch {
52
+ // Continue to next context
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+ function registerApprovalWorkspaceChannel(bridge, workspaceDirName, channel) {
58
+ bridge.approvalChannelByWorkspace.set(workspaceDirName, channel);
59
+ }
60
+ function registerApprovalSessionChannel(bridge, workspaceDirName, sessionTitle, channel) {
61
+ if (!sessionTitle || sessionTitle.trim().length === 0)
62
+ return;
63
+ bridge.approvalChannelBySession.set(buildSessionRouteKey(workspaceDirName, sessionTitle), channel);
64
+ bridge.approvalChannelByWorkspace.set(workspaceDirName, channel);
65
+ }
66
+ function resolveApprovalChannelForCurrentChat(bridge, workspaceDirName, currentChatTitle) {
67
+ if (!currentChatTitle || currentChatTitle.trim().length === 0) {
68
+ return null;
69
+ }
70
+ const key = buildSessionRouteKey(workspaceDirName, currentChatTitle);
71
+ return bridge.approvalChannelBySession.get(key) ?? null;
72
+ }
73
+ function buildApprovalCustomId(action, workspaceDirName, channelId) {
74
+ const prefix = action === 'approve'
75
+ ? APPROVE_ACTION_PREFIX
76
+ : action === 'always_allow'
77
+ ? ALWAYS_ALLOW_ACTION_PREFIX
78
+ : DENY_ACTION_PREFIX;
79
+ if (channelId && channelId.trim().length > 0) {
80
+ return `${prefix}:${workspaceDirName}:${channelId}`;
81
+ }
82
+ return `${prefix}:${workspaceDirName}`;
83
+ }
84
+ function parseApprovalCustomId(customId) {
85
+ if (customId === APPROVE_ACTION_PREFIX) {
86
+ return { action: 'approve', workspaceDirName: null, channelId: null };
87
+ }
88
+ if (customId === ALWAYS_ALLOW_ACTION_PREFIX) {
89
+ return { action: 'always_allow', workspaceDirName: null, channelId: null };
90
+ }
91
+ if (customId === DENY_ACTION_PREFIX) {
92
+ return { action: 'deny', workspaceDirName: null, channelId: null };
93
+ }
94
+ if (customId.startsWith(`${APPROVE_ACTION_PREFIX}:`)) {
95
+ const rest = customId.substring(`${APPROVE_ACTION_PREFIX}:`.length);
96
+ const [workspaceDirName, channelId] = rest.split(':');
97
+ return { action: 'approve', workspaceDirName: workspaceDirName || null, channelId: channelId || null };
98
+ }
99
+ if (customId.startsWith(`${ALWAYS_ALLOW_ACTION_PREFIX}:`)) {
100
+ const rest = customId.substring(`${ALWAYS_ALLOW_ACTION_PREFIX}:`.length);
101
+ const [workspaceDirName, channelId] = rest.split(':');
102
+ return { action: 'always_allow', workspaceDirName: workspaceDirName || null, channelId: channelId || null };
103
+ }
104
+ if (customId.startsWith(`${DENY_ACTION_PREFIX}:`)) {
105
+ const rest = customId.substring(`${DENY_ACTION_PREFIX}:`.length);
106
+ const [workspaceDirName, channelId] = rest.split(':');
107
+ return { action: 'deny', workspaceDirName: workspaceDirName || null, channelId: channelId || null };
108
+ }
109
+ return null;
110
+ }
111
+ /** Initialize the CDP bridge (lazy connection: pool creation only) */
112
+ function initCdpBridge(autoApproveDefault) {
113
+ const pool = new cdpConnectionPool_1.CdpConnectionPool({
114
+ cdpCallTimeout: 15000,
115
+ // Keep CDP reconnection lazy: do not reopen windows in background.
116
+ // Reconnection is triggered when the next chat/template message is sent.
117
+ maxReconnectAttempts: 0,
118
+ reconnectDelayMs: 3000,
119
+ });
120
+ const quota = new quotaService_1.QuotaService();
121
+ const autoAccept = new autoAcceptService_1.AutoAcceptService(autoApproveDefault);
122
+ return {
123
+ pool,
124
+ quota,
125
+ autoAccept,
126
+ lastActiveWorkspace: null,
127
+ lastActiveChannel: null,
128
+ approvalChannelByWorkspace: new Map(),
129
+ approvalChannelBySession: new Map(),
130
+ };
131
+ }
132
+ /**
133
+ * Helper to get the currently active CdpService from lastActiveWorkspace.
134
+ * Used in contexts where the workspace path is not explicitly provided,
135
+ * such as button interactions and model/mode switching.
136
+ */
137
+ function getCurrentCdp(bridge) {
138
+ if (!bridge.lastActiveWorkspace)
139
+ return null;
140
+ return bridge.pool.getConnected(bridge.lastActiveWorkspace);
141
+ }
142
+ /**
143
+ * Helper to start an approval detector for each workspace.
144
+ * Does nothing if a detector for the same workspace is already running.
145
+ */
146
+ function ensureApprovalDetector(bridge, cdp, workspaceDirName, client) {
147
+ const existing = bridge.pool.getApprovalDetector(workspaceDirName);
148
+ if (existing && existing.isActive())
149
+ return;
150
+ const detector = new approvalDetector_1.ApprovalDetector({
151
+ cdpService: cdp,
152
+ pollIntervalMs: 2000,
153
+ onApprovalRequired: async (info) => {
154
+ logger_1.logger.info(`[ApprovalDetector:${workspaceDirName}] Approval button detected (allow="${info.approveText}", deny="${info.denyText}")`);
155
+ const currentChatTitle = await getCurrentChatTitle(cdp);
156
+ const targetChannel = resolveApprovalChannelForCurrentChat(bridge, workspaceDirName, currentChatTitle);
157
+ const targetChannelId = targetChannel && 'id' in targetChannel ? String(targetChannel.id) : '';
158
+ if (!targetChannel || !targetChannelId || !('send' in targetChannel)) {
159
+ logger_1.logger.warn(`[ApprovalDetector:${workspaceDirName}] Skipped approval notification because chat is not linked to a Discord session` +
160
+ `${currentChatTitle ? ` (title="${currentChatTitle}")` : ''}`);
161
+ return;
162
+ }
163
+ if (bridge.autoAccept.isEnabled()) {
164
+ const accepted = await detector.alwaysAllowButton() || await detector.approveButton();
165
+ const autoEmbed = new discord_js_1.EmbedBuilder()
166
+ .setTitle(accepted ? (0, i18n_1.t)('Auto-approved') : (0, i18n_1.t)('Auto-approve failed'))
167
+ .setDescription(info.description || (0, i18n_1.t)('Antigravity is requesting approval for an action'))
168
+ .setColor(accepted ? 0x2ECC71 : 0xF39C12)
169
+ .addFields({ name: (0, i18n_1.t)('Auto-approve mode'), value: (0, i18n_1.t)('ON'), inline: true }, { name: (0, i18n_1.t)('Workspace'), value: workspaceDirName, inline: true }, { name: (0, i18n_1.t)('Result'), value: accepted ? (0, i18n_1.t)('Executed Always Allow/Allow') : (0, i18n_1.t)('Manual approval required'), inline: true })
170
+ .setTimestamp();
171
+ await targetChannel.send({ embeds: [autoEmbed] }).catch(logger_1.logger.error);
172
+ if (accepted) {
173
+ return;
174
+ }
175
+ }
176
+ const embed = new discord_js_1.EmbedBuilder()
177
+ .setTitle((0, i18n_1.t)('Approval Required'))
178
+ .setDescription(info.description || (0, i18n_1.t)('Antigravity is requesting approval for an action'))
179
+ .setColor(0xFFA500)
180
+ .addFields({ name: (0, i18n_1.t)('Allow button'), value: info.approveText, inline: true }, { name: (0, i18n_1.t)('Allow Chat button'), value: info.alwaysAllowText || (0, i18n_1.t)('In Dropdown'), inline: true }, { name: (0, i18n_1.t)('Deny button'), value: info.denyText || (0, i18n_1.t)('(None)'), inline: true }, { name: (0, i18n_1.t)('Workspace'), value: workspaceDirName, inline: true })
181
+ .setTimestamp();
182
+ const approveBtn = new discord_js_1.ButtonBuilder()
183
+ .setCustomId(buildApprovalCustomId('approve', workspaceDirName, targetChannelId))
184
+ .setLabel((0, i18n_1.t)('Allow'))
185
+ .setStyle(discord_js_1.ButtonStyle.Success);
186
+ const alwaysAllowBtn = new discord_js_1.ButtonBuilder()
187
+ .setCustomId(buildApprovalCustomId('always_allow', workspaceDirName, targetChannelId))
188
+ .setLabel((0, i18n_1.t)('Allow Chat'))
189
+ .setStyle(discord_js_1.ButtonStyle.Primary);
190
+ const denyBtn = new discord_js_1.ButtonBuilder()
191
+ .setCustomId(buildApprovalCustomId('deny', workspaceDirName, targetChannelId))
192
+ .setLabel((0, i18n_1.t)('Deny'))
193
+ .setStyle(discord_js_1.ButtonStyle.Danger);
194
+ const row = new discord_js_1.ActionRowBuilder().addComponents(approveBtn, alwaysAllowBtn, denyBtn);
195
+ targetChannel.send({
196
+ embeds: [embed],
197
+ components: [row],
198
+ }).catch(logger_1.logger.error);
199
+ },
200
+ });
201
+ detector.start();
202
+ bridge.pool.registerApprovalDetector(workspaceDirName, detector);
203
+ logger_1.logger.info(`[ApprovalDetector:${workspaceDirName}] Started approval button detection`);
204
+ }
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CdpConnectionPool = void 0;
4
+ const logger_1 = require("../utils/logger");
5
+ const cdpService_1 = require("./cdpService");
6
+ /**
7
+ * Pool that manages independent CdpService instances per workspace.
8
+ *
9
+ * Each workspace owns its own WebSocket / contexts / pendingCalls, so
10
+ * switching to workspace B while workspace A's ResponseMonitor is polling
11
+ * does not destroy A's WebSocket.
12
+ */
13
+ class CdpConnectionPool {
14
+ connections = new Map();
15
+ approvalDetectors = new Map();
16
+ connectingPromises = new Map();
17
+ cdpOptions;
18
+ constructor(cdpOptions = {}) {
19
+ this.cdpOptions = cdpOptions;
20
+ }
21
+ /**
22
+ * Get a CdpService for the given workspace path.
23
+ * Creates a new connection and caches it if not already connected.
24
+ * Prevents concurrent connections via Promise locking.
25
+ *
26
+ * @param workspacePath Full path of the workspace
27
+ * @returns Connected CdpService
28
+ */
29
+ async getOrConnect(workspacePath) {
30
+ const dirName = this.extractDirName(workspacePath);
31
+ // Return existing connection if available
32
+ const existing = this.connections.get(dirName);
33
+ if (existing && existing.isConnected()) {
34
+ // Re-validate that the still-open window is actually bound to this workspace.
35
+ await existing.discoverAndConnectForWorkspace(workspacePath);
36
+ return existing;
37
+ }
38
+ // Wait for the pending connection promise if one exists (prevents concurrent connections)
39
+ const pending = this.connectingPromises.get(dirName);
40
+ if (pending) {
41
+ return pending;
42
+ }
43
+ // Start a new connection
44
+ const connectPromise = this.createAndConnect(workspacePath, dirName);
45
+ this.connectingPromises.set(dirName, connectPromise);
46
+ try {
47
+ const cdp = await connectPromise;
48
+ return cdp;
49
+ }
50
+ finally {
51
+ this.connectingPromises.delete(dirName);
52
+ }
53
+ }
54
+ /**
55
+ * Get a connected CdpService (read-only).
56
+ * Returns null if not connected.
57
+ */
58
+ getConnected(workspaceDirName) {
59
+ const cdp = this.connections.get(workspaceDirName);
60
+ if (cdp && cdp.isConnected()) {
61
+ return cdp;
62
+ }
63
+ return null;
64
+ }
65
+ /**
66
+ * Disconnect the specified workspace.
67
+ */
68
+ disconnectWorkspace(workspaceDirName) {
69
+ const cdp = this.connections.get(workspaceDirName);
70
+ if (cdp) {
71
+ cdp.disconnect().catch((err) => {
72
+ logger_1.logger.error(`[CdpConnectionPool] Error while disconnecting ${workspaceDirName}:`, err);
73
+ });
74
+ this.connections.delete(workspaceDirName);
75
+ }
76
+ const detector = this.approvalDetectors.get(workspaceDirName);
77
+ if (detector) {
78
+ detector.stop();
79
+ this.approvalDetectors.delete(workspaceDirName);
80
+ }
81
+ }
82
+ /**
83
+ * Disconnect all workspace connections.
84
+ */
85
+ disconnectAll() {
86
+ for (const dirName of [...this.connections.keys()]) {
87
+ this.disconnectWorkspace(dirName);
88
+ }
89
+ }
90
+ /**
91
+ * Register an approval detector for a workspace.
92
+ */
93
+ registerApprovalDetector(workspaceDirName, detector) {
94
+ // Stop existing detector
95
+ const existing = this.approvalDetectors.get(workspaceDirName);
96
+ if (existing && existing.isActive()) {
97
+ existing.stop();
98
+ }
99
+ this.approvalDetectors.set(workspaceDirName, detector);
100
+ }
101
+ /**
102
+ * Get the approval detector for a workspace.
103
+ */
104
+ getApprovalDetector(workspaceDirName) {
105
+ return this.approvalDetectors.get(workspaceDirName);
106
+ }
107
+ /**
108
+ * Return a list of workspace names with active connections.
109
+ */
110
+ getActiveWorkspaceNames() {
111
+ const active = [];
112
+ for (const [name, cdp] of this.connections) {
113
+ if (cdp.isConnected()) {
114
+ active.push(name);
115
+ }
116
+ }
117
+ return active;
118
+ }
119
+ /**
120
+ * Extract the directory name from a workspace path.
121
+ */
122
+ extractDirName(workspacePath) {
123
+ return workspacePath.split('/').filter(Boolean).pop() || workspacePath;
124
+ }
125
+ /**
126
+ * Create a new CdpService and connect to the workspace.
127
+ */
128
+ async createAndConnect(workspacePath, dirName) {
129
+ // Disconnect old connection if exists
130
+ const old = this.connections.get(dirName);
131
+ if (old) {
132
+ await old.disconnect().catch(() => { });
133
+ this.connections.delete(dirName);
134
+ }
135
+ const cdp = new cdpService_1.CdpService(this.cdpOptions);
136
+ // Auto-cleanup on disconnect
137
+ cdp.on('disconnected', () => {
138
+ logger_1.logger.error(`[CdpConnectionPool] Workspace "${dirName}" disconnected`);
139
+ // Only remove from Map when reconnection fails
140
+ // (CdpService attempts reconnection internally, so we don't remove here)
141
+ });
142
+ cdp.on('reconnectFailed', () => {
143
+ logger_1.logger.error(`[CdpConnectionPool] Reconnection failed for workspace "${dirName}". Removing from pool`);
144
+ this.connections.delete(dirName);
145
+ const detector = this.approvalDetectors.get(dirName);
146
+ if (detector) {
147
+ detector.stop();
148
+ this.approvalDetectors.delete(dirName);
149
+ }
150
+ });
151
+ // Connect to the workspace
152
+ await cdp.discoverAndConnectForWorkspace(workspacePath);
153
+ this.connections.set(dirName, cdp);
154
+ return cdp;
155
+ }
156
+ }
157
+ exports.CdpConnectionPool = CdpConnectionPool;