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.
- package/README.md +22 -7
- package/dist/bin/cli.js +18 -18
- package/dist/bin/commands/doctor.js +25 -19
- package/dist/bin/commands/start.js +25 -2
- package/dist/bot/index.js +445 -126
- package/dist/commands/joinCommandHandler.js +302 -0
- package/dist/commands/joinDetachCommandHandler.js +285 -0
- package/dist/commands/registerSlashCommands.js +40 -0
- package/dist/commands/workspaceCommandHandler.js +17 -28
- package/dist/database/chatSessionRepository.js +10 -0
- package/dist/database/userPreferenceRepository.js +72 -0
- package/dist/events/interactionCreateHandler.js +338 -30
- package/dist/events/messageCreateHandler.js +161 -47
- package/dist/services/antigravityLauncher.js +4 -3
- package/dist/services/approvalDetector.js +7 -0
- package/dist/services/assistantDomExtractor.js +339 -0
- package/dist/services/cdpBridgeManager.js +323 -39
- package/dist/services/cdpConnectionPool.js +117 -33
- package/dist/services/cdpService.js +149 -53
- package/dist/services/chatSessionService.js +229 -8
- package/dist/services/errorPopupDetector.js +271 -0
- package/dist/services/planningDetector.js +318 -0
- package/dist/services/responseMonitor.js +308 -70
- package/dist/services/retryStore.js +46 -0
- package/dist/services/updateCheckService.js +147 -0
- package/dist/services/userMessageDetector.js +221 -0
- package/dist/ui/buttonUtils.js +33 -0
- package/dist/ui/modeUi.js +11 -1
- package/dist/ui/modelsUi.js +24 -13
- package/dist/ui/outputUi.js +30 -0
- package/dist/ui/projectListUi.js +83 -0
- package/dist/ui/sessionPickerUi.js +48 -0
- package/dist/utils/antigravityPaths.js +94 -0
- package/dist/utils/configLoader.js +18 -0
- package/dist/utils/discordButtonUtils.js +33 -0
- package/dist/utils/discordFormatter.js +149 -16
- package/dist/utils/htmlToDiscordMarkdown.js +184 -0
- package/dist/utils/logBuffer.js +47 -0
- package/dist/utils/logFileTransport.js +147 -0
- package/dist/utils/logger.js +86 -21
- package/dist/utils/pathUtils.js +57 -0
- package/dist/utils/plainTextFormatter.js +70 -0
- package/dist/utils/processLogBuffer.js +4 -0
- package/package.json +4 -4
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PlanningDetector = void 0;
|
|
4
|
+
const logger_1 = require("../utils/logger");
|
|
5
|
+
const approvalDetector_1 = require("./approvalDetector");
|
|
6
|
+
/**
|
|
7
|
+
* Detection script for the Antigravity UI planning mode.
|
|
8
|
+
*
|
|
9
|
+
* Looks for Open/Proceed button pairs inside .notify-user-container
|
|
10
|
+
* and extracts plan metadata from the surrounding DOM elements.
|
|
11
|
+
*/
|
|
12
|
+
const DETECT_PLANNING_SCRIPT = `(() => {
|
|
13
|
+
const OPEN_PATTERNS = ['open'];
|
|
14
|
+
const PROCEED_PATTERNS = ['proceed'];
|
|
15
|
+
|
|
16
|
+
const normalize = (text) => (text || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
17
|
+
|
|
18
|
+
// Find the notify container that holds planning UI
|
|
19
|
+
const container = document.querySelector('.notify-user-container');
|
|
20
|
+
if (!container) return null;
|
|
21
|
+
|
|
22
|
+
const allButtons = Array.from(container.querySelectorAll('button'))
|
|
23
|
+
.filter(btn => btn.offsetParent !== null);
|
|
24
|
+
|
|
25
|
+
const openBtn = allButtons.find(btn => {
|
|
26
|
+
const t = normalize(btn.textContent || '');
|
|
27
|
+
return OPEN_PATTERNS.some(p => t === p || t.includes(p));
|
|
28
|
+
}) || null;
|
|
29
|
+
|
|
30
|
+
const proceedBtn = allButtons.find(btn => {
|
|
31
|
+
const t = normalize(btn.textContent || '');
|
|
32
|
+
return PROCEED_PATTERNS.some(p => t === p || t.includes(p));
|
|
33
|
+
}) || null;
|
|
34
|
+
|
|
35
|
+
// Both buttons must exist for this to be a planning UI
|
|
36
|
+
if (!openBtn || !proceedBtn) return null;
|
|
37
|
+
|
|
38
|
+
const openText = (openBtn.textContent || '').trim();
|
|
39
|
+
const proceedText = (proceedBtn.textContent || '').trim();
|
|
40
|
+
|
|
41
|
+
// Extract plan title from .inline-flex.break-all
|
|
42
|
+
const titleEl = container.querySelector('span.inline-flex.break-all, .inline-flex.break-all');
|
|
43
|
+
const planTitle = titleEl ? (titleEl.textContent || '').trim() : '';
|
|
44
|
+
|
|
45
|
+
// Extract plan summary from span.text-sm (excluding buttons text)
|
|
46
|
+
const summaryEls = Array.from(container.querySelectorAll('span.text-sm'));
|
|
47
|
+
const planSummary = summaryEls
|
|
48
|
+
.map(el => (el.textContent || '').trim())
|
|
49
|
+
.filter(text => text.length > 0 && text !== openText && text !== proceedText)
|
|
50
|
+
.join(' ');
|
|
51
|
+
|
|
52
|
+
// Extract description from leading-relaxed container, skipping code/style blocks
|
|
53
|
+
const descEl = container.querySelector('.leading-relaxed.select-text');
|
|
54
|
+
let description = '';
|
|
55
|
+
if (descEl) {
|
|
56
|
+
const SKIP_TAGS = new Set(['PRE', 'CODE', 'STYLE', 'SCRIPT']);
|
|
57
|
+
const parts = [];
|
|
58
|
+
const walk = (node) => {
|
|
59
|
+
if (node.nodeType === 3) {
|
|
60
|
+
const t = node.textContent || '';
|
|
61
|
+
if (t.trim()) parts.push(t.trim());
|
|
62
|
+
} else if (node.nodeType === 1 && !SKIP_TAGS.has(node.tagName)) {
|
|
63
|
+
for (const child of node.childNodes) walk(child);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
walk(descEl);
|
|
67
|
+
description = parts.join(' ').slice(0, 500);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { openText, proceedText, planTitle, planSummary, description };
|
|
71
|
+
})()`;
|
|
72
|
+
/**
|
|
73
|
+
* Extract plan content displayed after clicking Open.
|
|
74
|
+
*
|
|
75
|
+
* Looks for the rendered markdown inside the plan content area
|
|
76
|
+
* and returns the text, truncated to 4000 characters for Discord embed limits.
|
|
77
|
+
*/
|
|
78
|
+
const EXTRACT_PLAN_CONTENT_SCRIPT = `(() => {
|
|
79
|
+
// Simple HTML-to-Markdown converter for plan content
|
|
80
|
+
const htmlToMd = (el) => {
|
|
81
|
+
const parts = [];
|
|
82
|
+
const process = (node) => {
|
|
83
|
+
if (node.nodeType === 3) {
|
|
84
|
+
parts.push(node.textContent || '');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (node.nodeType !== 1) return;
|
|
88
|
+
const tag = node.tagName;
|
|
89
|
+
if (tag === 'H1') { parts.push('\\n# '); node.childNodes.forEach(process); parts.push('\\n'); return; }
|
|
90
|
+
if (tag === 'H2') { parts.push('\\n## '); node.childNodes.forEach(process); parts.push('\\n'); return; }
|
|
91
|
+
if (tag === 'H3') { parts.push('\\n### '); node.childNodes.forEach(process); parts.push('\\n'); return; }
|
|
92
|
+
if (tag === 'H4') { parts.push('\\n#### '); node.childNodes.forEach(process); parts.push('\\n'); return; }
|
|
93
|
+
if (tag === 'STRONG' || tag === 'B') { parts.push('**'); node.childNodes.forEach(process); parts.push('**'); return; }
|
|
94
|
+
if (tag === 'EM' || tag === 'I') { parts.push('*'); node.childNodes.forEach(process); parts.push('*'); return; }
|
|
95
|
+
if (tag === 'PRE') {
|
|
96
|
+
const code = node.querySelector('code');
|
|
97
|
+
const text = code ? (code.textContent || '') : (node.textContent || '');
|
|
98
|
+
parts.push('\\n\`\`\`\\n' + text + '\\n\`\`\`\\n');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (tag === 'CODE') { parts.push('\`' + (node.textContent || '') + '\`'); return; }
|
|
102
|
+
if (tag === 'A') {
|
|
103
|
+
const href = node.getAttribute('href') || '';
|
|
104
|
+
parts.push('['); node.childNodes.forEach(process); parts.push('](' + href + ')');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (tag === 'LI') { parts.push('\\n- '); node.childNodes.forEach(process); return; }
|
|
108
|
+
if (tag === 'BR') { parts.push('\\n'); return; }
|
|
109
|
+
if (tag === 'P') { parts.push('\\n\\n'); node.childNodes.forEach(process); parts.push('\\n'); return; }
|
|
110
|
+
if (tag === 'UL' || tag === 'OL') { node.childNodes.forEach(process); parts.push('\\n'); return; }
|
|
111
|
+
if (tag === 'STYLE' || tag === 'SCRIPT') return;
|
|
112
|
+
node.childNodes.forEach(process);
|
|
113
|
+
};
|
|
114
|
+
process(el);
|
|
115
|
+
return parts.join('').replace(/\\n{3,}/g, '\\n\\n').trim();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Primary selector: plan content container
|
|
119
|
+
const contentContainer = document.querySelector(
|
|
120
|
+
'div.relative.pl-4.pr-4.py-1, div.relative.pl-4.pr-4'
|
|
121
|
+
);
|
|
122
|
+
if (contentContainer) {
|
|
123
|
+
const textEl = contentContainer.querySelector('.leading-relaxed.select-text');
|
|
124
|
+
if (textEl) {
|
|
125
|
+
return htmlToMd(textEl);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Fallback: any leading-relaxed.select-text with significant content
|
|
130
|
+
const allLeading = Array.from(document.querySelectorAll('.leading-relaxed.select-text'));
|
|
131
|
+
for (const el of allLeading) {
|
|
132
|
+
const md = htmlToMd(el);
|
|
133
|
+
if (md.length > 100) {
|
|
134
|
+
return md;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return null;
|
|
139
|
+
})()`;
|
|
140
|
+
/**
|
|
141
|
+
* Detects planning mode buttons (Open/Proceed) in the Antigravity UI via polling.
|
|
142
|
+
*
|
|
143
|
+
* Follows the same polling pattern as ApprovalDetector:
|
|
144
|
+
* - start()/stop() lifecycle
|
|
145
|
+
* - Duplicate notification prevention via lastDetectedKey
|
|
146
|
+
* - CDP error tolerance (continues polling on error)
|
|
147
|
+
*/
|
|
148
|
+
class PlanningDetector {
|
|
149
|
+
cdpService;
|
|
150
|
+
pollIntervalMs;
|
|
151
|
+
onPlanningRequired;
|
|
152
|
+
onResolved;
|
|
153
|
+
pollTimer = null;
|
|
154
|
+
isRunning = false;
|
|
155
|
+
/** Key of the last detected planning info (for duplicate notification prevention) */
|
|
156
|
+
lastDetectedKey = null;
|
|
157
|
+
/** Full PlanningInfo from the last detection */
|
|
158
|
+
lastDetectedInfo = null;
|
|
159
|
+
/** Timestamp of last notification (for cooldown-based dedup) */
|
|
160
|
+
lastNotifiedAt = 0;
|
|
161
|
+
/** Cooldown period in ms to suppress duplicate notifications */
|
|
162
|
+
static COOLDOWN_MS = 5000;
|
|
163
|
+
constructor(options) {
|
|
164
|
+
this.cdpService = options.cdpService;
|
|
165
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 2000;
|
|
166
|
+
this.onPlanningRequired = options.onPlanningRequired;
|
|
167
|
+
this.onResolved = options.onResolved;
|
|
168
|
+
}
|
|
169
|
+
/** Start monitoring. */
|
|
170
|
+
start() {
|
|
171
|
+
if (this.isRunning)
|
|
172
|
+
return;
|
|
173
|
+
this.isRunning = true;
|
|
174
|
+
this.lastDetectedKey = null;
|
|
175
|
+
this.lastDetectedInfo = null;
|
|
176
|
+
this.lastNotifiedAt = 0;
|
|
177
|
+
this.schedulePoll();
|
|
178
|
+
}
|
|
179
|
+
/** Stop monitoring. */
|
|
180
|
+
async stop() {
|
|
181
|
+
this.isRunning = false;
|
|
182
|
+
if (this.pollTimer) {
|
|
183
|
+
clearTimeout(this.pollTimer);
|
|
184
|
+
this.pollTimer = null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** Return the last detected planning info. Returns null if nothing has been detected. */
|
|
188
|
+
getLastDetectedInfo() {
|
|
189
|
+
return this.lastDetectedInfo;
|
|
190
|
+
}
|
|
191
|
+
/** Returns whether monitoring is currently active. */
|
|
192
|
+
isActive() {
|
|
193
|
+
return this.isRunning;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Click the Open button via CDP.
|
|
197
|
+
* @param buttonText Text of the button to click (default: detected openText or "Open")
|
|
198
|
+
* @returns true if click succeeded
|
|
199
|
+
*/
|
|
200
|
+
async clickOpenButton(buttonText) {
|
|
201
|
+
const text = buttonText ?? this.lastDetectedInfo?.openText ?? 'Open';
|
|
202
|
+
return this.clickButton(text);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Click the Proceed button via CDP.
|
|
206
|
+
* @param buttonText Text of the button to click (default: detected proceedText or "Proceed")
|
|
207
|
+
* @returns true if click succeeded
|
|
208
|
+
*/
|
|
209
|
+
async clickProceedButton(buttonText) {
|
|
210
|
+
const text = buttonText ?? this.lastDetectedInfo?.proceedText ?? 'Proceed';
|
|
211
|
+
return this.clickButton(text);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Extract plan content from the DOM after Open has been clicked.
|
|
215
|
+
* @returns Plan content text or null if not found
|
|
216
|
+
*/
|
|
217
|
+
async extractPlanContent() {
|
|
218
|
+
try {
|
|
219
|
+
const result = await this.runEvaluateScript(EXTRACT_PLAN_CONTENT_SCRIPT);
|
|
220
|
+
return typeof result === 'string' ? result : null;
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
logger_1.logger.error('[PlanningDetector] Error extracting plan content:', error);
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/** Schedule the next poll. */
|
|
228
|
+
schedulePoll() {
|
|
229
|
+
if (!this.isRunning)
|
|
230
|
+
return;
|
|
231
|
+
this.pollTimer = setTimeout(async () => {
|
|
232
|
+
await this.poll();
|
|
233
|
+
if (this.isRunning) {
|
|
234
|
+
this.schedulePoll();
|
|
235
|
+
}
|
|
236
|
+
}, this.pollIntervalMs);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Single poll iteration:
|
|
240
|
+
* 1. Get planning button info from DOM (with contextId)
|
|
241
|
+
* 2. Notify via callback only on new detection (prevent duplicates)
|
|
242
|
+
* 3. Reset lastDetectedKey / lastDetectedInfo when buttons disappear
|
|
243
|
+
*/
|
|
244
|
+
async poll() {
|
|
245
|
+
try {
|
|
246
|
+
const contextId = this.cdpService.getPrimaryContextId();
|
|
247
|
+
const callParams = {
|
|
248
|
+
expression: DETECT_PLANNING_SCRIPT,
|
|
249
|
+
returnByValue: true,
|
|
250
|
+
awaitPromise: false,
|
|
251
|
+
};
|
|
252
|
+
if (contextId !== null) {
|
|
253
|
+
callParams.contextId = contextId;
|
|
254
|
+
}
|
|
255
|
+
const result = await this.cdpService.call('Runtime.evaluate', callParams);
|
|
256
|
+
const info = result?.result?.value ?? null;
|
|
257
|
+
if (info) {
|
|
258
|
+
// Duplicate prevention: use button text pair as key (stable across DOM redraws)
|
|
259
|
+
const key = `${info.openText}::${info.proceedText}`;
|
|
260
|
+
const now = Date.now();
|
|
261
|
+
const withinCooldown = (now - this.lastNotifiedAt) < PlanningDetector.COOLDOWN_MS;
|
|
262
|
+
if (key !== this.lastDetectedKey && !withinCooldown) {
|
|
263
|
+
this.lastDetectedKey = key;
|
|
264
|
+
this.lastDetectedInfo = info;
|
|
265
|
+
this.lastNotifiedAt = now;
|
|
266
|
+
this.onPlanningRequired(info);
|
|
267
|
+
}
|
|
268
|
+
else if (key === this.lastDetectedKey) {
|
|
269
|
+
// Same key — update stored info silently
|
|
270
|
+
this.lastDetectedInfo = info;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
// Reset when buttons disappear (prepare for next planning detection)
|
|
275
|
+
const wasDetected = this.lastDetectedKey !== null;
|
|
276
|
+
this.lastDetectedKey = null;
|
|
277
|
+
this.lastDetectedInfo = null;
|
|
278
|
+
if (wasDetected && this.onResolved) {
|
|
279
|
+
this.onResolved();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
// Ignore CDP errors and continue monitoring
|
|
285
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
286
|
+
if (message.includes('WebSocket is not connected')) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
logger_1.logger.error('[PlanningDetector] Error during polling:', error);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/** Internal click handler using buildClickScript from approvalDetector. */
|
|
293
|
+
async clickButton(buttonText) {
|
|
294
|
+
try {
|
|
295
|
+
const result = await this.runEvaluateScript((0, approvalDetector_1.buildClickScript)(buttonText));
|
|
296
|
+
return result?.ok === true;
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
logger_1.logger.error('[PlanningDetector] Error while clicking button:', error);
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/** Execute Runtime.evaluate with contextId and return result.value. */
|
|
304
|
+
async runEvaluateScript(expression) {
|
|
305
|
+
const contextId = this.cdpService.getPrimaryContextId();
|
|
306
|
+
const callParams = {
|
|
307
|
+
expression,
|
|
308
|
+
returnByValue: true,
|
|
309
|
+
awaitPromise: false,
|
|
310
|
+
};
|
|
311
|
+
if (contextId !== null) {
|
|
312
|
+
callParams.contextId = contextId;
|
|
313
|
+
}
|
|
314
|
+
const result = await this.cdpService.call('Runtime.evaluate', callParams);
|
|
315
|
+
return result?.result?.value;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
exports.PlanningDetector = PlanningDetector;
|