lazy-gravity 0.9.1 → 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/dist/bot/index.js CHANGED
@@ -1202,6 +1202,7 @@ const startBot = async (cliLogLevel) => {
1202
1202
  accountPrefRepo,
1203
1203
  channelPrefRepo,
1204
1204
  antigravityAccounts: config.antigravityAccounts,
1205
+ activeMonitors,
1205
1206
  }, interaction);
1206
1207
  return;
1207
1208
  }
@@ -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;
@@ -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 getLabelText = (el) => {
294
- if (!el || !(el instanceof Element)) return '';
295
- const parts = [
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
- return parts.filter(Boolean).join(' ');
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
- let selected = clickByPatterns([wanted, wantedLoose], '[role="option"], li, button, [data-testid*="conversation"]');
479
- if (selected) {
480
- const selectedOption = pickBest(
481
- asArray(document.querySelectorAll('[role="option"], li, button, [data-testid*="conversation"]')),
482
- [wanted, wantedLoose],
483
- );
484
- const selectedClickable = getClickable(selectedOption);
485
- if (selectedClickable) {
486
- selectedClickable.focus();
487
- pressEnter(selectedClickable);
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
- return { ok: true, matchedTitle: wantedRaw };
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.1",
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": "^14.0.3",
52
+ "commander": "^15.0.0",
53
53
  "discord.js": "^14.25.1",
54
54
  "dotenv": "^17.3.1",
55
55
  "grammy": "^1.41.1",