lazy-gravity 0.0.4 β†’ 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 (44) hide show
  1. package/README.md +22 -7
  2. package/dist/bin/cli.js +18 -18
  3. package/dist/bin/commands/doctor.js +25 -19
  4. package/dist/bin/commands/start.js +25 -2
  5. package/dist/bot/index.js +445 -126
  6. package/dist/commands/joinCommandHandler.js +302 -0
  7. package/dist/commands/joinDetachCommandHandler.js +285 -0
  8. package/dist/commands/registerSlashCommands.js +40 -0
  9. package/dist/commands/workspaceCommandHandler.js +17 -28
  10. package/dist/database/chatSessionRepository.js +10 -0
  11. package/dist/database/userPreferenceRepository.js +72 -0
  12. package/dist/events/interactionCreateHandler.js +338 -30
  13. package/dist/events/messageCreateHandler.js +161 -47
  14. package/dist/services/antigravityLauncher.js +4 -3
  15. package/dist/services/approvalDetector.js +7 -0
  16. package/dist/services/assistantDomExtractor.js +339 -0
  17. package/dist/services/cdpBridgeManager.js +323 -39
  18. package/dist/services/cdpConnectionPool.js +117 -33
  19. package/dist/services/cdpService.js +149 -53
  20. package/dist/services/chatSessionService.js +229 -8
  21. package/dist/services/errorPopupDetector.js +271 -0
  22. package/dist/services/planningDetector.js +318 -0
  23. package/dist/services/responseMonitor.js +308 -70
  24. package/dist/services/retryStore.js +46 -0
  25. package/dist/services/updateCheckService.js +147 -0
  26. package/dist/services/userMessageDetector.js +221 -0
  27. package/dist/ui/buttonUtils.js +33 -0
  28. package/dist/ui/modeUi.js +11 -1
  29. package/dist/ui/modelsUi.js +24 -13
  30. package/dist/ui/outputUi.js +30 -0
  31. package/dist/ui/projectListUi.js +83 -0
  32. package/dist/ui/sessionPickerUi.js +48 -0
  33. package/dist/utils/antigravityPaths.js +94 -0
  34. package/dist/utils/configLoader.js +18 -0
  35. package/dist/utils/discordButtonUtils.js +33 -0
  36. package/dist/utils/discordFormatter.js +149 -16
  37. package/dist/utils/htmlToDiscordMarkdown.js +184 -0
  38. package/dist/utils/logBuffer.js +47 -0
  39. package/dist/utils/logFileTransport.js +147 -0
  40. package/dist/utils/logger.js +86 -21
  41. package/dist/utils/pathUtils.js +57 -0
  42. package/dist/utils/plainTextFormatter.js +70 -0
  43. package/dist/utils/processLogBuffer.js +4 -0
  44. package/package.json +4 -4
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createMessageCreateHandler = createMessageCreateHandler;
4
4
  const discord_js_1 = require("discord.js");
5
5
  const messageParser_1 = require("../commands/messageParser");
6
+ const plainTextFormatter_1 = require("../utils/plainTextFormatter");
6
7
  const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
7
8
  const modeService_1 = require("../services/modeService");
8
9
  const imageHandler_1 = require("../utils/imageHandler");
@@ -10,11 +11,30 @@ const logger_1 = require("../utils/logger");
10
11
  function createMessageCreateHandler(deps) {
11
12
  const getCurrentCdp = deps.getCurrentCdp ?? cdpBridgeManager_1.getCurrentCdp;
12
13
  const ensureApprovalDetector = deps.ensureApprovalDetector ?? cdpBridgeManager_1.ensureApprovalDetector;
14
+ const ensureErrorPopupDetector = deps.ensureErrorPopupDetector ?? cdpBridgeManager_1.ensureErrorPopupDetector;
15
+ const ensurePlanningDetector = deps.ensurePlanningDetector ?? cdpBridgeManager_1.ensurePlanningDetector;
13
16
  const registerApprovalWorkspaceChannel = deps.registerApprovalWorkspaceChannel ?? cdpBridgeManager_1.registerApprovalWorkspaceChannel;
14
17
  const registerApprovalSessionChannel = deps.registerApprovalSessionChannel ?? cdpBridgeManager_1.registerApprovalSessionChannel;
15
18
  const downloadInboundImageAttachments = deps.downloadInboundImageAttachments ?? imageHandler_1.downloadInboundImageAttachments;
16
19
  const cleanupInboundImageAttachments = deps.cleanupInboundImageAttachments ?? imageHandler_1.cleanupInboundImageAttachments;
17
20
  const isImageAttachment = deps.isImageAttachment ?? imageHandler_1.isImageAttachment;
21
+ // Per-workspace prompt queue: serializes send→response cycles
22
+ const workspaceQueues = new Map();
23
+ const workspaceQueueDepths = new Map();
24
+ function enqueueForWorkspace(workspacePath, task) {
25
+ // .catch: ensure a prior rejection never stalls the chain
26
+ const current = (workspaceQueues.get(workspacePath) ?? Promise.resolve()).catch(() => { });
27
+ const next = current.then(async () => {
28
+ try {
29
+ await task();
30
+ }
31
+ catch (err) {
32
+ logger_1.logger.error('[WorkspaceQueue] task error:', err?.message || err);
33
+ }
34
+ });
35
+ workspaceQueues.set(workspacePath, next);
36
+ return next;
37
+ }
18
38
  return async (message) => {
19
39
  if (message.author.bot)
20
40
  return;
@@ -36,12 +56,12 @@ function createMessageCreateHandler(deps) {
36
56
  if (parsed.commandName === 'status') {
37
57
  const activeNames = deps.bridge.pool.getActiveWorkspaceNames();
38
58
  const currentMode = deps.modeService.getCurrentMode();
39
- const embed = new discord_js_1.EmbedBuilder()
40
- .setTitle('πŸ”§ Bot Status')
41
- .setColor(activeNames.length > 0 ? 0x00CC88 : 0x888888)
42
- .addFields({ name: 'CDP Connection', value: activeNames.length > 0 ? `🟒 ${activeNames.length} project(s) connected` : 'βšͺ Disconnected', inline: true }, { name: 'Mode', value: modeService_1.MODE_DISPLAY_NAMES[currentMode] || currentMode, inline: true }, { name: 'Auto Approve', value: deps.bridge.autoAccept.isEnabled() ? '🟒 ON' : 'βšͺ OFF', inline: true })
43
- .setFooter({ text: 'πŸ’‘ Use the slash command /status for more detailed information' })
44
- .setTimestamp();
59
+ const statusFields = [
60
+ { name: 'CDP Connection', value: activeNames.length > 0 ? `🟒 ${activeNames.length} project(s) connected` : 'βšͺ Disconnected', inline: true },
61
+ { name: 'Mode', value: modeService_1.MODE_DISPLAY_NAMES[currentMode] || currentMode, inline: true },
62
+ { name: 'Auto Approve', value: deps.bridge.autoAccept.isEnabled() ? '🟒 ON' : 'βšͺ OFF', inline: true },
63
+ ];
64
+ let statusDescription = '';
45
65
  if (activeNames.length > 0) {
46
66
  const lines = activeNames.map((name) => {
47
67
  const cdp = deps.bridge.pool.getConnected(name);
@@ -49,15 +69,33 @@ function createMessageCreateHandler(deps) {
49
69
  const detectorActive = deps.bridge.pool.getApprovalDetector(name)?.isActive() ? ' [Detecting]' : '';
50
70
  return `β€’ **${name}** β€” Contexts: ${contexts}${detectorActive}`;
51
71
  });
52
- embed.setDescription(`**Connected Projects:**\n${lines.join('\n')}`);
72
+ statusDescription = `**Connected Projects:**\n${lines.join('\n')}`;
53
73
  }
54
74
  else {
55
- embed.setDescription('Send a message to auto-connect to a project.');
75
+ statusDescription = 'Send a message to auto-connect to a project.';
56
76
  }
77
+ const statusOutputFormat = deps.userPrefRepo?.getOutputFormat(message.author.id) ?? 'embed';
78
+ if (statusOutputFormat === 'plain') {
79
+ const chunks = (0, plainTextFormatter_1.formatAsPlainText)({
80
+ title: 'πŸ”§ Bot Status',
81
+ description: statusDescription,
82
+ fields: statusFields,
83
+ footerText: 'Use the slash command /status for more detailed information',
84
+ });
85
+ await message.reply({ content: chunks[0] });
86
+ return;
87
+ }
88
+ const embed = new discord_js_1.EmbedBuilder()
89
+ .setTitle('πŸ”§ Bot Status')
90
+ .setColor(activeNames.length > 0 ? 0x00CC88 : 0x888888)
91
+ .addFields(...statusFields)
92
+ .setDescription(statusDescription)
93
+ .setFooter({ text: 'πŸ’‘ Use the slash command /status for more detailed information' })
94
+ .setTimestamp();
57
95
  await message.reply({ embeds: [embed] });
58
96
  return;
59
97
  }
60
- const slashOnlyCommands = ['help', 'stop', 'model', 'mode', 'project', 'chat', 'new', 'cleanup'];
98
+ const slashOnlyCommands = ['help', 'stop', 'model', 'mode', 'project', 'chat', 'new', 'cleanup', 'join', 'mirror', 'output'];
61
99
  if (slashOnlyCommands.includes(parsed.commandName)) {
62
100
  await message.reply({
63
101
  content: `πŸ’‘ Please use \`/${parsed.commandName}\` as a slash command.\nType \`/${parsed.commandName}\` in the Discord input field to see suggestions.`,
@@ -76,6 +114,7 @@ function createMessageCreateHandler(deps) {
76
114
  chatSessionRepo: deps.chatSessionRepo,
77
115
  channelManager: deps.channelManager,
78
116
  titleGenerator: deps.titleGenerator,
117
+ userPrefRepo: deps.userPrefRepo,
79
118
  });
80
119
  }
81
120
  else {
@@ -96,51 +135,126 @@ function createMessageCreateHandler(deps) {
96
135
  const workspacePath = deps.wsHandler.getWorkspaceForChannel(message.channelId);
97
136
  try {
98
137
  if (workspacePath) {
99
- try {
100
- const cdp = await deps.bridge.pool.getOrConnect(workspacePath);
101
- const dirName = deps.bridge.pool.extractDirName(workspacePath);
102
- deps.bridge.lastActiveWorkspace = dirName;
103
- deps.bridge.lastActiveChannel = message.channel;
104
- registerApprovalWorkspaceChannel(deps.bridge, dirName, message.channel);
105
- ensureApprovalDetector(deps.bridge, cdp, dirName, deps.client);
106
- const session = deps.chatSessionRepo.findByChannelId(message.channelId);
107
- if (session?.displayName) {
108
- registerApprovalSessionChannel(deps.bridge, dirName, session.displayName, message.channel);
138
+ const projectLabel = deps.bridge.pool.extractProjectName(workspacePath);
139
+ // Track queue depth for hourglass reactions
140
+ const currentDepth = workspaceQueueDepths.get(workspacePath) ?? 0;
141
+ workspaceQueueDepths.set(workspacePath, currentDepth + 1);
142
+ const newDepth = currentDepth + 1;
143
+ if (currentDepth > 0) {
144
+ logger_1.logger.info(`[Queue:${projectLabel}] Enqueued (depth: ${newDepth}, channel: ${message.channelId})`);
145
+ await message.react('⏳').catch(() => { });
146
+ }
147
+ else {
148
+ logger_1.logger.info(`[Queue:${projectLabel}] Processing immediately (depth: ${newDepth}, channel: ${message.channelId})`);
149
+ }
150
+ const queueStartTime = Date.now();
151
+ await enqueueForWorkspace(workspacePath, async () => {
152
+ const waitMs = Date.now() - queueStartTime;
153
+ if (waitMs > 100) {
154
+ logger_1.logger.info(`[Queue:${projectLabel}] Task started after ${Math.round(waitMs / 1000)}s wait (channel: ${message.channelId})`);
109
155
  }
110
- if (session?.isRenamed && session.displayName) {
111
- const activationResult = await deps.chatSessionService.activateSessionByTitle(cdp, session.displayName);
112
- if (!activationResult.ok) {
113
- const reason = activationResult.error ? ` (${activationResult.error})` : '';
114
- await message.reply(`⚠️ Could not route this message to the bound session (${session.displayName}). ` +
115
- `Please open /chat and verify the session${reason}.`).catch(() => { });
116
- return;
117
- }
156
+ // Remove hourglass when task starts processing
157
+ const botId = message.client.user?.id;
158
+ if (botId) {
159
+ await message.reactions.resolve('⏳')?.users.remove(botId).catch(() => { });
118
160
  }
119
- else if (session && !session.isRenamed) {
120
- try {
121
- const chatResult = await deps.chatSessionService.startNewChat(cdp);
122
- if (!chatResult.ok) {
123
- logger_1.logger.warn('[MessageCreate] Failed to start new chat in Antigravity:', chatResult.error);
161
+ try {
162
+ const cdp = await deps.bridge.pool.getOrConnect(workspacePath);
163
+ const projectName = deps.bridge.pool.extractProjectName(workspacePath);
164
+ deps.bridge.lastActiveWorkspace = projectName;
165
+ deps.bridge.lastActiveChannel = message.channel;
166
+ registerApprovalWorkspaceChannel(deps.bridge, projectName, message.channel);
167
+ ensureApprovalDetector(deps.bridge, cdp, projectName, deps.client);
168
+ ensureErrorPopupDetector(deps.bridge, cdp, projectName, deps.client);
169
+ ensurePlanningDetector(deps.bridge, cdp, projectName, deps.client);
170
+ const session = deps.chatSessionRepo.findByChannelId(message.channelId);
171
+ if (session?.displayName) {
172
+ registerApprovalSessionChannel(deps.bridge, projectName, session.displayName, message.channel);
173
+ }
174
+ if (session?.isRenamed && session.displayName) {
175
+ const activationResult = await deps.chatSessionService.activateSessionByTitle(cdp, session.displayName);
176
+ if (!activationResult.ok) {
177
+ const reason = activationResult.error ? ` (${activationResult.error})` : '';
178
+ await message.reply(`⚠️ Could not route this message to the bound session (${session.displayName}). ` +
179
+ `Please open /chat and verify the session${reason}.`).catch(() => { });
180
+ return;
181
+ }
182
+ }
183
+ else if (session && !session.isRenamed) {
184
+ try {
185
+ const chatResult = await deps.chatSessionService.startNewChat(cdp);
186
+ if (!chatResult.ok) {
187
+ logger_1.logger.warn('[MessageCreate] Failed to start new chat in Antigravity:', chatResult.error);
188
+ message.channel.send(`⚠️ Could not open a new chat in Antigravity. Sending to existing chat.`).catch(() => { });
189
+ }
190
+ }
191
+ catch (err) {
192
+ logger_1.logger.error('[MessageCreate] startNewChat error:', err);
124
193
  message.channel.send(`⚠️ Could not open a new chat in Antigravity. Sending to existing chat.`).catch(() => { });
125
194
  }
126
195
  }
127
- catch (err) {
128
- logger_1.logger.error('[MessageCreate] startNewChat error:', err);
129
- message.channel.send(`⚠️ Could not open a new chat in Antigravity. Sending to existing chat.`).catch(() => { });
196
+ await deps.autoRenameChannel(message, deps.chatSessionRepo, deps.titleGenerator, deps.channelManager, cdp);
197
+ // Re-register session channel after autoRenameChannel sets displayName
198
+ const updatedSession = deps.chatSessionRepo.findByChannelId(message.channelId);
199
+ if (updatedSession?.displayName) {
200
+ registerApprovalSessionChannel(deps.bridge, projectName, updatedSession.displayName, message.channel);
130
201
  }
202
+ // Register echo hash so UserMessageDetector skips this message
203
+ const userMsgDetector = deps.bridge.pool.getUserMessageDetector?.(projectName);
204
+ if (userMsgDetector) {
205
+ userMsgDetector.addEchoHash(promptText);
206
+ }
207
+ // Wait for full response cycle (onComplete/onTimeout) before releasing the queue.
208
+ // Safety timeout (360s) prevents permanent queue deadlock if onFullCompletion
209
+ // is never called due to a bug.
210
+ const QUEUE_SAFETY_TIMEOUT_MS = 360_000;
211
+ const promptStartTime = Date.now();
212
+ await new Promise((resolve) => {
213
+ const safetyTimer = setTimeout(() => {
214
+ logger_1.logger.warn(`[Queue:${projectName}] Safety timeout β€” releasing queue after 360s ` +
215
+ `(channel: ${message.channelId})`);
216
+ resolve();
217
+ }, QUEUE_SAFETY_TIMEOUT_MS);
218
+ let settled = false;
219
+ const settle = () => {
220
+ if (settled)
221
+ return;
222
+ settled = true;
223
+ clearTimeout(safetyTimer);
224
+ const elapsed = Math.round((Date.now() - promptStartTime) / 1000);
225
+ logger_1.logger.info(`[Queue:${projectName}] Prompt completed in ${elapsed}s ` +
226
+ `(channel: ${message.channelId})`);
227
+ resolve();
228
+ };
229
+ deps.sendPromptToAntigravity(deps.bridge, message, promptText, cdp, deps.modeService, deps.modelService, inboundImages, {
230
+ chatSessionService: deps.chatSessionService,
231
+ chatSessionRepo: deps.chatSessionRepo,
232
+ channelManager: deps.channelManager,
233
+ titleGenerator: deps.titleGenerator,
234
+ userPrefRepo: deps.userPrefRepo,
235
+ onFullCompletion: settle,
236
+ }).catch((err) => {
237
+ // sendPromptToAntigravity rejected before onFullCompletion fired
238
+ // (e.g. setup code threw before top-level try/catch).
239
+ // Release the queue immediately instead of waiting for safety timeout.
240
+ logger_1.logger.error(`[Queue:${projectName}] sendPromptToAntigravity rejected early ` +
241
+ `(channel: ${message.channelId}):`, err?.message || err);
242
+ settle();
243
+ });
244
+ });
131
245
  }
132
- await deps.autoRenameChannel(message, deps.chatSessionRepo, deps.titleGenerator, deps.channelManager, cdp);
133
- await deps.sendPromptToAntigravity(deps.bridge, message, promptText, cdp, deps.modeService, deps.modelService, inboundImages, {
134
- chatSessionService: deps.chatSessionService,
135
- chatSessionRepo: deps.chatSessionRepo,
136
- channelManager: deps.channelManager,
137
- titleGenerator: deps.titleGenerator,
138
- });
139
- }
140
- catch (e) {
141
- await message.reply(`Failed to connect to workspace: ${e.message}`);
142
- return;
143
- }
246
+ catch (e) {
247
+ logger_1.logger.error(`[Queue:${projectLabel}] Task failed (channel: ${message.channelId}):`, e.message);
248
+ await message.reply(`Failed to connect to workspace: ${e.message}`);
249
+ }
250
+ finally {
251
+ const remainingDepth = (workspaceQueueDepths.get(workspacePath) ?? 1) - 1;
252
+ workspaceQueueDepths.set(workspacePath, remainingDepth);
253
+ if (remainingDepth > 0) {
254
+ logger_1.logger.info(`[Queue:${projectLabel}] Task done, ${remainingDepth} remaining`);
255
+ }
256
+ }
257
+ });
144
258
  }
145
259
  else {
146
260
  await message.reply('No project is configured for this channel. Please create or select one with `/project`.');
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.ensureAntigravityRunning = ensureAntigravityRunning;
37
37
  const logger_1 = require("../utils/logger");
38
38
  const cdpPorts_1 = require("../utils/cdpPorts");
39
+ const pathUtils_1 = require("../utils/pathUtils");
39
40
  const http = __importStar(require("http"));
40
41
  /**
41
42
  * Check if CDP responds on the specified port.
@@ -69,10 +70,10 @@ function checkPort(port) {
69
70
  * Called during Bot initialization.
70
71
  */
71
72
  async function ensureAntigravityRunning() {
72
- logger_1.logger.info('[AntigravityLauncher] Checking CDP ports...');
73
+ logger_1.logger.debug('[AntigravityLauncher] Checking CDP ports...');
73
74
  for (const port of cdpPorts_1.CDP_PORTS) {
74
75
  if (await checkPort(port)) {
75
- logger_1.logger.info(`[AntigravityLauncher] OK β€” Port ${port} responding`);
76
+ logger_1.logger.debug(`[AntigravityLauncher] OK β€” Port ${port} responding`);
76
77
  return;
77
78
  }
78
79
  }
@@ -83,7 +84,7 @@ async function ensureAntigravityRunning() {
83
84
  logger_1.logger.warn(' Please run AntigravityDebug.command before starting the Bot');
84
85
  logger_1.logger.warn('');
85
86
  logger_1.logger.warn(' Or manually:');
86
- logger_1.logger.warn(' open -a Antigravity --args --remote-debugging-port=9222');
87
+ logger_1.logger.warn(` ${(0, pathUtils_1.getAntigravityCdpHint)(9222)}`);
87
88
  logger_1.logger.warn('='.repeat(70));
88
89
  logger_1.logger.warn('');
89
90
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ApprovalDetector = void 0;
4
+ exports.buildClickScript = buildClickScript;
4
5
  const logger_1 = require("../utils/logger");
5
6
  /**
6
7
  * Approval button detection script for the Antigravity UI
@@ -205,6 +206,7 @@ class ApprovalDetector {
205
206
  cdpService;
206
207
  pollIntervalMs;
207
208
  onApprovalRequired;
209
+ onResolved;
208
210
  pollTimer = null;
209
211
  isRunning = false;
210
212
  /** Key of the last detected button info (for duplicate notification prevention) */
@@ -215,6 +217,7 @@ class ApprovalDetector {
215
217
  this.cdpService = options.cdpService;
216
218
  this.pollIntervalMs = options.pollIntervalMs ?? 1500;
217
219
  this.onApprovalRequired = options.onApprovalRequired;
220
+ this.onResolved = options.onResolved;
218
221
  }
219
222
  /**
220
223
  * Start monitoring.
@@ -285,8 +288,12 @@ class ApprovalDetector {
285
288
  }
286
289
  else {
287
290
  // Reset when buttons disappear (prepare for next approval detection)
291
+ const wasDetected = this.lastDetectedKey !== null;
288
292
  this.lastDetectedKey = null;
289
293
  this.lastDetectedInfo = null;
294
+ if (wasDetected && this.onResolved) {
295
+ this.onResolved();
296
+ }
290
297
  }
291
298
  }
292
299
  catch (error) {