lazy-gravity 0.9.0 β 0.9.2
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
CHANGED
|
@@ -93,6 +93,7 @@ Just type in any bound channel:
|
|
|
93
93
|
- `π /join` β Join an existing Antigravity session (shows up to 20 recent sessions)
|
|
94
94
|
- `π /mirror` β Toggle PCβDiscord message mirroring for the current session
|
|
95
95
|
- `π /stop` β Force-stop a running Antigravity task
|
|
96
|
+
- `π /shutdown` β Shut down the IDE while keeping the active CDP project connections and session bindings
|
|
96
97
|
- `πΈ /screenshot` β Capture and send Antigravity's current screen
|
|
97
98
|
- `π§ /status` β Show bot connection status, current mode, and active project
|
|
98
99
|
- `β
/autoaccept [on|off|status]` β Toggle auto-approval of file edit dialogs
|
package/dist/bot/index.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.handleJoin = handleJoin;
|
|
4
4
|
exports.handleTelegramJoinSelect = handleTelegramJoinSelect;
|
|
5
5
|
exports.handleMirror = handleMirror;
|
|
6
|
+
exports.startResponseMirror = startResponseMirror;
|
|
6
7
|
const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
|
|
7
8
|
const responseMonitor_1 = require("../services/responseMonitor");
|
|
8
9
|
const sessionPickerUi_1 = require("../ui/sessionPickerUi");
|
|
@@ -137,6 +138,14 @@ async function routeMirroredMessage(deps, cdp, workspacePath, info, channel) {
|
|
|
137
138
|
startResponseMirror(deps, cdp, workspacePath, channel, chatTitle || 'Unknown');
|
|
138
139
|
}
|
|
139
140
|
function startResponseMirror(deps, cdp, workspacePath, channel, chatTitle) {
|
|
141
|
+
// If the primary message handler already has an active monitor for this workspace,
|
|
142
|
+
// skip the passive mirror β the primary handler delivers the response itself, and
|
|
143
|
+
// running both would send the same AI response twice.
|
|
144
|
+
const projectName = deps.bridge.pool.extractProjectName(workspacePath);
|
|
145
|
+
if (deps.activeMonitors?.has(projectName)) {
|
|
146
|
+
logger_1.logger.debug(`[TelegramMirror] Skipping passive monitor β primary monitor active for ${projectName}`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
140
149
|
const prev = activeResponseMonitors.get(workspacePath);
|
|
141
150
|
if (prev?.isActive()) {
|
|
142
151
|
prev.stop().catch(() => { });
|
|
@@ -173,6 +173,12 @@ function createTelegramMessageHandler(deps) {
|
|
|
173
173
|
// Determine the prompt text β use default for image-only messages
|
|
174
174
|
const effectivePrompt = promptText || 'Please review the attached images and respond accordingly.';
|
|
175
175
|
const baseline = await (0, responseMonitor_1.captureResponseMonitorBaseline)(cdp);
|
|
176
|
+
// Register the echo hash BEFORE injecting (the Discord path registers it later):
|
|
177
|
+
// UserMessageDetector polls the DOM every ~2s, so registering only after
|
|
178
|
+
// injectMessage() resolves leaves a window where the injected prompt is detected
|
|
179
|
+
// as PC-side input and mirrored back to Telegram. If injection fails, the stale
|
|
180
|
+
// hash is harmless β it expires via its 60s TTL.
|
|
181
|
+
deps.bridge.pool.getUserMessageDetector?.(projectName, selectedAccount)?.addEchoHash(effectivePrompt);
|
|
176
182
|
// Inject prompt (with or without images) into Antigravity
|
|
177
183
|
logger_1.logger.prompt(effectivePrompt);
|
|
178
184
|
let injectResult;
|
|
@@ -68,6 +68,9 @@ function checkPort(port) {
|
|
|
68
68
|
});
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Waits for a specific port to respond within the given timeout.
|
|
73
|
+
*/
|
|
71
74
|
async function waitForPort(port, timeoutMs = 30000) {
|
|
72
75
|
const deadline = Date.now() + timeoutMs;
|
|
73
76
|
do {
|
|
@@ -77,6 +80,9 @@ async function waitForPort(port, timeoutMs = 30000) {
|
|
|
77
80
|
} while (Date.now() < deadline);
|
|
78
81
|
return false;
|
|
79
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Ensures lifecycle operations (start/stop) are executed sequentially to prevent race conditions.
|
|
85
|
+
*/
|
|
80
86
|
function serializeLifecycle(operation) {
|
|
81
87
|
const pending = lifecycleOperation
|
|
82
88
|
? lifecycleOperation.then(operation, operation)
|
|
@@ -88,6 +94,9 @@ function serializeLifecycle(operation) {
|
|
|
88
94
|
}).catch(() => { });
|
|
89
95
|
return pending;
|
|
90
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Starts the Antigravity IDE process with CDP enabled on the specified port.
|
|
99
|
+
*/
|
|
91
100
|
function startAntigravity(port = cdpPorts_1.CDP_PORTS[0]) {
|
|
92
101
|
return serializeLifecycle(async () => {
|
|
93
102
|
if (await checkPort(port))
|
|
@@ -112,13 +121,17 @@ function startAntigravity(port = cdpPorts_1.CDP_PORTS[0]) {
|
|
|
112
121
|
return 'started';
|
|
113
122
|
});
|
|
114
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Stops the running Antigravity IDE CDP process on the specified port (SIGTERM on POSIX, Stop-Process -Force on Windows).
|
|
126
|
+
*/
|
|
115
127
|
function stopAntigravity(port = cdpPorts_1.CDP_PORTS[0]) {
|
|
116
128
|
return serializeLifecycle(async () => {
|
|
117
129
|
if (!await checkPort(port))
|
|
118
130
|
return 'already-stopped';
|
|
119
131
|
const version = await getCdpVersion(port);
|
|
120
132
|
const browser = typeof version?.Browser === 'string' ? version.Browser.toLowerCase() : '';
|
|
121
|
-
|
|
133
|
+
const userAgent = typeof version?.['User-Agent'] === 'string' ? version['User-Agent'].toLowerCase() : '';
|
|
134
|
+
if (!browser.includes('antigravity') && !userAgent.includes('antigravity')) {
|
|
122
135
|
throw new Error(`Refusing to stop non-Antigravity CDP service on port ${port}.`);
|
|
123
136
|
}
|
|
124
137
|
await new Promise((resolve, reject) => {
|
|
@@ -148,6 +161,9 @@ function stopAntigravity(port = cdpPorts_1.CDP_PORTS[0]) {
|
|
|
148
161
|
return 'stopped';
|
|
149
162
|
});
|
|
150
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Fetches the CDP /json/version payload to identify the connected browser target.
|
|
166
|
+
*/
|
|
151
167
|
function getCdpVersion(port) {
|
|
152
168
|
return new Promise((resolve, reject) => {
|
|
153
169
|
const req = http.get(`http://127.0.0.1:${port}/json/version`, (res) => {
|
|
@@ -169,6 +185,9 @@ function getCdpVersion(port) {
|
|
|
169
185
|
});
|
|
170
186
|
});
|
|
171
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Fetches the CDP /json/list payload to discover available debugging targets.
|
|
190
|
+
*/
|
|
172
191
|
function getCdpTargets(port) {
|
|
173
192
|
return new Promise((resolve, reject) => {
|
|
174
193
|
const req = http.get(`http://127.0.0.1:${port}/json/list`, (res) => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ChatSessionService = void 0;
|
|
4
|
+
exports.buildActivateViaPastConversationsScript = buildActivateViaPastConversationsScript;
|
|
4
5
|
const logger_1 = require("../utils/logger");
|
|
5
6
|
/** Script to get the state of the new chat button */
|
|
6
7
|
const GET_NEW_CHAT_BUTTON_SCRIPT = `(() => {
|
|
@@ -290,9 +291,9 @@ function buildActivateViaPastConversationsScript(title) {
|
|
|
290
291
|
|
|
291
292
|
const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
|
|
292
293
|
const asArray = (nodeList) => Array.from(nodeList || []);
|
|
293
|
-
const
|
|
294
|
-
if (!el || !(el instanceof Element)) return
|
|
295
|
-
|
|
294
|
+
const getLabelParts = (el) => {
|
|
295
|
+
if (!el || !(el instanceof Element)) return [];
|
|
296
|
+
return [
|
|
296
297
|
el.textContent || '',
|
|
297
298
|
el.getAttribute('aria-label') || '',
|
|
298
299
|
el.getAttribute('title') || '',
|
|
@@ -300,7 +301,26 @@ function buildActivateViaPastConversationsScript(title) {
|
|
|
300
301
|
el.getAttribute('data-tooltip-content') || '',
|
|
301
302
|
el.getAttribute('data-testid') || '',
|
|
302
303
|
];
|
|
303
|
-
|
|
304
|
+
};
|
|
305
|
+
const getLabelText = (el) => getLabelParts(el).filter(Boolean).join(' ');
|
|
306
|
+
// Verify the option that was actually selected really is the requested
|
|
307
|
+
// title. getLabelText concatenates several attributes, so a naive
|
|
308
|
+
// whole-string comparison can fail even for a correct pick (e.g. when
|
|
309
|
+
// textContent and aria-label duplicate the same value). Instead, check
|
|
310
|
+
// each label component individually for an exact (or loose-exact) match.
|
|
311
|
+
// If any component matches, report the requested title; otherwise report
|
|
312
|
+
// the actual visible text so the acceptance gate rejects a mere
|
|
313
|
+
// substring/loose selection.
|
|
314
|
+
const resolveMatchedTitle = (el) => {
|
|
315
|
+
const parts = getLabelParts(el);
|
|
316
|
+
for (const part of parts) {
|
|
317
|
+
if (!part) continue;
|
|
318
|
+
if (normalize(part) === wanted || (wantedLoose && normalizeLoose(part) === wantedLoose)) {
|
|
319
|
+
return wantedRaw;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const visible = (parts[0] || '').trim();
|
|
323
|
+
return visible || null;
|
|
304
324
|
};
|
|
305
325
|
const getClickable = (el) => {
|
|
306
326
|
if (!el || !(el instanceof Element)) return null;
|
|
@@ -475,22 +495,23 @@ function buildActivateViaPastConversationsScript(title) {
|
|
|
475
495
|
await wait(260);
|
|
476
496
|
}
|
|
477
497
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
}
|
|
490
|
-
if (!selected) {
|
|
498
|
+
// Resolve the exact option element, then click + focus + press Enter
|
|
499
|
+
// on that same element so the click target and the verification
|
|
500
|
+
// target can never drift apart. Mirror clickByPatterns' scoped
|
|
501
|
+
// selector -> broad fallback behaviour.
|
|
502
|
+
const optionSelector = '[role="option"], li, button, [data-testid*="conversation"]';
|
|
503
|
+
const scopedOptions = asArray(document.querySelectorAll(optionSelector));
|
|
504
|
+
const broadOptions = asArray(document.querySelectorAll('button, [role="button"], a, li, div, span'));
|
|
505
|
+
const optionSource = scopedOptions.length > 0 ? scopedOptions : broadOptions;
|
|
506
|
+
const selectedOption = pickBest(optionSource, [wanted, wantedLoose]);
|
|
507
|
+
const selectedClickable = getClickable(selectedOption);
|
|
508
|
+
if (!selectedClickable) {
|
|
491
509
|
return { ok: false, error: 'Conversation not found in Past Conversations' };
|
|
492
510
|
}
|
|
493
|
-
|
|
511
|
+
selectedClickable.click();
|
|
512
|
+
selectedClickable.focus();
|
|
513
|
+
pressEnter(selectedClickable);
|
|
514
|
+
return { ok: true, matchedTitle: resolveMatchedTitle(selectedOption) };
|
|
494
515
|
})();
|
|
495
516
|
})()`;
|
|
496
517
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lazy-gravity",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
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": {
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@inquirer/select": "^5.1.0",
|
|
51
51
|
"better-sqlite3": "^12.6.2",
|
|
52
|
-
"commander": "^
|
|
52
|
+
"commander": "^15.0.0",
|
|
53
53
|
"discord.js": "^14.25.1",
|
|
54
54
|
"dotenv": "^17.3.1",
|
|
55
55
|
"grammy": "^1.41.1",
|