lazy-gravity 0.6.2 → 0.7.1

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.
@@ -30,6 +30,56 @@ const GET_CHAT_TITLE_SCRIPT = `(() => {
30
30
  const hasActiveChat = title.length > 0 && title !== 'Agent';
31
31
  return { title: title || '(Untitled)', hasActiveChat };
32
32
  })()`;
33
+ const GET_SESSION_VIEW_STATE_SCRIPT = `(() => {
34
+ const panel = document.querySelector('.antigravity-agent-side-panel');
35
+ if (!panel) {
36
+ return {
37
+ panelFound: false,
38
+ title: '',
39
+ hasActiveChat: false,
40
+ hasLoadingIndicator: false,
41
+ hasRenderableContent: false,
42
+ renderablePreview: [],
43
+ };
44
+ }
45
+ const header = panel.querySelector('div[class*="border-b"]');
46
+ const titleEl = header?.querySelector('div[class*="text-ellipsis"]');
47
+ const title = titleEl ? (titleEl.textContent || '').trim() : '';
48
+ const hasActiveChat = title.length > 0 && title !== 'Agent';
49
+
50
+ const bodyCandidates = Array.from(panel.querySelectorAll(
51
+ '[data-message-author-role], [data-message-role], .rendered-markdown, .prose'
52
+ ));
53
+ const renderablePreview = bodyCandidates.filter((el) => {
54
+ if (!(el instanceof HTMLElement)) return false;
55
+ const text = (el.textContent || '').trim();
56
+ if (el.offsetParent === null || text.length === 0) return false;
57
+ if (/^\\/\\*\\s*Copied from /i.test(text)) return false;
58
+ return true;
59
+ }).slice(0, 5).map((el) => ({
60
+ tag: el.tagName,
61
+ className: el.className || '',
62
+ text: ((el.textContent || '').trim()).slice(0, 160),
63
+ }));
64
+ const hasRenderableContent = renderablePreview.length > 0;
65
+
66
+ const hasLoadingIndicator = Boolean(
67
+ panel.querySelector(
68
+ '[role="progressbar"], ' +
69
+ 'svg[class*="animate-spin"], div[class*="animate-spin"], ' +
70
+ 'svg[class*="spinner"], div[class*="spinner"], div[class*="loading"]'
71
+ )
72
+ );
73
+
74
+ return {
75
+ panelFound: true,
76
+ title: title || '(Untitled)',
77
+ hasActiveChat,
78
+ hasLoadingIndicator,
79
+ hasRenderableContent,
80
+ renderablePreview,
81
+ };
82
+ })()`;
33
83
  /**
34
84
  * Script to find the Past Conversations button and return its coordinates.
35
85
  * We use coordinates so that the actual click is done via CDP Input.dispatchMouseEvent,
@@ -432,6 +482,10 @@ class ChatSessionService {
432
482
  static ACTIVATE_SESSION_MAX_WAIT_MS = 30000;
433
483
  static ACTIVATE_SESSION_RETRY_INTERVAL_MS = 800;
434
484
  static LIST_SESSIONS_TARGET = 20;
485
+ static HYDRATE_RETRY_DELAY_MS = 700;
486
+ static REOPEN_RETRY_ATTEMPTS = 4;
487
+ static REOPEN_NEW_CHAT_DELAY_MS = 400;
488
+ static REOPEN_HISTORY_DELAY_MS = 1000;
435
489
  /**
436
490
  * List recent sessions by opening the Past Conversations panel.
437
491
  *
@@ -556,6 +610,25 @@ class ChatSessionService {
556
610
  type: 'mouseReleased', x, y, button: 'left', clickCount: 1,
557
611
  });
558
612
  }
613
+ async dispatchNewConversationShortcut(cdpService) {
614
+ const modifiers = process.platform === 'darwin' ? 8 : 10;
615
+ await cdpService.call('Input.dispatchKeyEvent', {
616
+ type: 'keyDown',
617
+ key: 'L',
618
+ code: 'KeyL',
619
+ modifiers,
620
+ windowsVirtualKeyCode: 76,
621
+ nativeVirtualKeyCode: 76,
622
+ });
623
+ await cdpService.call('Input.dispatchKeyEvent', {
624
+ type: 'keyUp',
625
+ key: 'L',
626
+ code: 'KeyL',
627
+ modifiers,
628
+ windowsVirtualKeyCode: 76,
629
+ nativeVirtualKeyCode: 76,
630
+ });
631
+ }
559
632
  /**
560
633
  * Start a new chat session in the Antigravity UI.
561
634
  *
@@ -597,27 +670,23 @@ class ChatSessionService {
597
670
  if (!btnState.enabled) {
598
671
  return { ok: true };
599
672
  }
600
- // cursor: pointer -> click via CDP Input API coordinates
601
- await cdpService.call('Input.dispatchMouseEvent', {
602
- type: 'mouseMoved', x: btnState.x, y: btnState.y,
603
- });
604
- await cdpService.call('Input.dispatchMouseEvent', {
605
- type: 'mousePressed', x: btnState.x, y: btnState.y,
606
- button: 'left', clickCount: 1,
607
- });
608
- await cdpService.call('Input.dispatchMouseEvent', {
609
- type: 'mouseReleased', x: btnState.x, y: btnState.y,
610
- button: 'left', clickCount: 1,
611
- });
612
- // Wait for UI to update after click
673
+ // Prefer the keyboard shortcut because some Antigravity builds bind hover tips to the button target.
674
+ await this.dispatchNewConversationShortcut(cdpService);
675
+ // Wait for UI to update after shortcut
613
676
  await new Promise(r => setTimeout(r, 1500));
614
677
  // Check if button changed to not-allowed (evidence that a new chat was opened)
615
678
  const afterState = await this.getNewChatButtonState(cdpService, contexts);
616
679
  if (afterState.found && !afterState.enabled) {
617
680
  return { ok: true };
618
681
  }
619
- // Button still enabled -> click may not have worked
620
- return { ok: false, error: 'Clicked new chat button but state did not change' };
682
+ // Fallback for older builds where the shortcut is not wired.
683
+ await this.cdpMouseClick(cdpService, btnState.x, btnState.y);
684
+ await new Promise(r => setTimeout(r, 1500));
685
+ const afterFallback = await this.getNewChatButtonState(cdpService, contexts);
686
+ if (afterFallback.found && !afterFallback.enabled) {
687
+ return { ok: true };
688
+ }
689
+ return { ok: false, error: 'New conversation shortcut and button click did not change state' };
621
690
  }
622
691
  catch (error) {
623
692
  const message = error instanceof Error ? error.message : String(error);
@@ -655,6 +724,104 @@ class ChatSessionService {
655
724
  return { title: '(Failed to retrieve)', hasActiveChat: false };
656
725
  }
657
726
  }
727
+ async getCurrentSessionViewState(cdpService) {
728
+ try {
729
+ const contexts = cdpService.getContexts();
730
+ for (const ctx of contexts) {
731
+ try {
732
+ const result = await cdpService.call('Runtime.evaluate', {
733
+ expression: GET_SESSION_VIEW_STATE_SCRIPT,
734
+ returnByValue: true,
735
+ contextId: ctx.id,
736
+ });
737
+ const value = result?.result?.value;
738
+ const hasPanel = value?.panelFound === true;
739
+ const looksUseful = Boolean(hasPanel ||
740
+ value?.hasLoadingIndicator ||
741
+ value?.hasRenderableContent ||
742
+ (typeof value?.title === 'string' && value.title.trim().length > 0));
743
+ if (value && looksUseful) {
744
+ return {
745
+ title: value.title,
746
+ hasActiveChat: value.hasActiveChat ?? false,
747
+ panelFound: value.panelFound ?? false,
748
+ hasLoadingIndicator: value.hasLoadingIndicator ?? false,
749
+ hasRenderableContent: value.hasRenderableContent ?? false,
750
+ renderablePreview: Array.isArray(value.renderablePreview) ? value.renderablePreview : [],
751
+ };
752
+ }
753
+ }
754
+ catch (_) { /* try next context */ }
755
+ }
756
+ }
757
+ catch (_) { /* fall through */ }
758
+ return {
759
+ title: '(Failed to retrieve)',
760
+ hasActiveChat: false,
761
+ panelFound: false,
762
+ hasLoadingIndicator: false,
763
+ hasRenderableContent: false,
764
+ renderablePreview: [],
765
+ };
766
+ }
767
+ async refreshSessionViewIfStuck(cdpService, title) {
768
+ const state = await this.getCurrentSessionViewState(cdpService);
769
+ if (state.title.trim() !== title.trim()) {
770
+ return { ok: false, error: `Current title mismatch before refresh (expected="${title}", actual="${state.title}")` };
771
+ }
772
+ if (!state.hasLoadingIndicator && state.hasRenderableContent) {
773
+ return { ok: true };
774
+ }
775
+ const bounce = await this.recoverSessionViewWithNewConversationBounce(cdpService, title);
776
+ if (bounce.ok) {
777
+ return bounce;
778
+ }
779
+ return {
780
+ ok: false,
781
+ error: `Session "${title}" still appears stuck after new-conversation recovery ` +
782
+ `(${bounce.error || 'unknown'})`,
783
+ };
784
+ }
785
+ async recoverSessionViewWithNewConversationBounce(cdpService, title, options) {
786
+ const state = await this.getCurrentSessionViewState(cdpService);
787
+ if (state.title.trim() === title.trim() && !state.hasLoadingIndicator && state.hasRenderableContent) {
788
+ return { ok: true };
789
+ }
790
+ const maxAttempts = options?.maxAttempts ?? ChatSessionService.REOPEN_RETRY_ATTEMPTS;
791
+ const newChatDelayMs = options?.newChatDelayMs ?? ChatSessionService.REOPEN_NEW_CHAT_DELAY_MS;
792
+ const reopenDelayMs = options?.reopenDelayMs ?? ChatSessionService.REOPEN_HISTORY_DELAY_MS;
793
+ let lastError = `Session "${title}" still appears stuck before recovery`;
794
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
795
+ const newChat = await this.startNewChat(cdpService);
796
+ if (!newChat.ok) {
797
+ lastError =
798
+ `Attempt ${attempt}/${maxAttempts}: failed to open a fresh new conversation before reopening "${title}": ` +
799
+ `${newChat.error || 'unknown'}`;
800
+ continue;
801
+ }
802
+ await new Promise((resolve) => setTimeout(resolve, newChatDelayMs));
803
+ const reopened = await this.activateSessionByTitle(cdpService, title, {
804
+ maxWaitMs: 8000,
805
+ retryIntervalMs: 300,
806
+ allowVisibilityWarmupMs: 1000,
807
+ });
808
+ if (!reopened.ok) {
809
+ lastError =
810
+ `Attempt ${attempt}/${maxAttempts}: failed to reopen "${title}" after new conversation: ` +
811
+ `${reopened.error || 'unknown'}`;
812
+ continue;
813
+ }
814
+ await new Promise((resolve) => setTimeout(resolve, reopenDelayMs));
815
+ const after = await this.getCurrentSessionViewState(cdpService);
816
+ if (after.title.trim() === title.trim() && (!after.hasLoadingIndicator || after.hasRenderableContent)) {
817
+ return { ok: true };
818
+ }
819
+ lastError =
820
+ `Attempt ${attempt}/${maxAttempts}: session "${title}" still appears stuck after reopening ` +
821
+ `(loading=${after.hasLoadingIndicator}, content=${after.hasRenderableContent}, actual="${after.title}")`;
822
+ }
823
+ return { ok: false, error: lastError };
824
+ }
658
825
  /**
659
826
  * Activate an existing chat by title.
660
827
  * Returns ok:false if the target chat cannot be located or verified.
@@ -669,12 +836,14 @@ class ChatSessionService {
669
836
  }
670
837
  const maxWaitMs = options?.maxWaitMs ?? ChatSessionService.ACTIVATE_SESSION_MAX_WAIT_MS;
671
838
  const retryIntervalMs = options?.retryIntervalMs ?? ChatSessionService.ACTIVATE_SESSION_RETRY_INTERVAL_MS;
839
+ const allowVisibilityWarmupMs = options?.allowVisibilityWarmupMs ?? 0;
672
840
  let usedPastConversations = false;
673
841
  let directResult = { ok: false, error: 'not attempted' };
674
842
  let pastResult = null;
675
843
  let clicked = false;
676
- const startedAt = Date.now();
844
+ let startedAt = Date.now();
677
845
  let attempts = 0;
846
+ let warmupConsumed = false;
678
847
  while (Date.now() - startedAt <= maxWaitMs) {
679
848
  attempts += 1;
680
849
  directResult = await this.tryActivateByDirectSidePanel(cdpService, title);
@@ -704,8 +873,21 @@ class ChatSessionService {
704
873
  await new Promise((resolve) => setTimeout(resolve, 500));
705
874
  const after = await this.getCurrentSessionInfo(cdpService);
706
875
  if (after.title.trim() === title.trim()) {
876
+ if (usedPastConversations) {
877
+ await this.closePanelWithEscape(cdpService);
878
+ }
707
879
  return { ok: true };
708
880
  }
881
+ if (!warmupConsumed && allowVisibilityWarmupMs > 0 && after.title.trim() === 'Agent') {
882
+ warmupConsumed = true;
883
+ startedAt = Date.now();
884
+ await new Promise((resolve) => setTimeout(resolve, allowVisibilityWarmupMs));
885
+ return this.activateSessionByTitle(cdpService, title, {
886
+ maxWaitMs,
887
+ retryIntervalMs,
888
+ allowVisibilityWarmupMs: 0,
889
+ });
890
+ }
709
891
  // If direct side-panel activation hit the wrong row, try the explicit Past Conversations flow.
710
892
  if (!usedPastConversations) {
711
893
  const viaPast = await this.tryActivateByPastConversations(cdpService, title);
@@ -713,6 +895,7 @@ class ChatSessionService {
713
895
  await new Promise((resolve) => setTimeout(resolve, 500));
714
896
  const afterPast = await this.getCurrentSessionInfo(cdpService);
715
897
  if (afterPast.title.trim() === title.trim()) {
898
+ await this.closePanelWithEscape(cdpService);
716
899
  return { ok: true };
717
900
  }
718
901
  return {
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.UserMessageDetector = void 0;
4
+ const events_1 = require("events");
4
5
  const node_crypto_1 = require("node:crypto");
5
6
  const logger_1 = require("../utils/logger");
6
7
  /**
@@ -74,10 +75,9 @@ function computeEchoHash(text) {
74
75
  * Detects user messages posted directly in the Antigravity UI (e.g., from a PC).
75
76
  * Follows the ApprovalDetector polling pattern.
76
77
  */
77
- class UserMessageDetector {
78
+ class UserMessageDetector extends events_1.EventEmitter {
78
79
  cdpService;
79
80
  pollIntervalMs;
80
- onUserMessage;
81
81
  pollTimer = null;
82
82
  isRunning = false;
83
83
  /** Hash of the last detected message (for duplicate prevention) */
@@ -90,9 +90,9 @@ class UserMessageDetector {
90
90
  /** True during the first poll — seeds existing DOM state without firing callback */
91
91
  isPriming = false;
92
92
  constructor(options) {
93
+ super();
93
94
  this.cdpService = options.cdpService;
94
95
  this.pollIntervalMs = options.pollIntervalMs ?? 2000;
95
- this.onUserMessage = options.onUserMessage;
96
96
  }
97
97
  /**
98
98
  * Register a message hash as an echo (sent by LazyGravity).
@@ -207,7 +207,7 @@ class UserMessageDetector {
207
207
  this.lastDetectedHash = hash;
208
208
  this.addToSeenHashes(hash);
209
209
  logger_1.logger.debug(`[UserMessageDetector] New message detected: "${preview}..."`);
210
- this.onUserMessage(info);
210
+ this.emit('message', info);
211
211
  }
212
212
  }
213
213
  catch (error) {
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ACCOUNT_SELECT_ID = void 0;
4
+ exports.buildAccountPayload = buildAccountPayload;
5
+ exports.sendAccountUI = sendAccountUI;
6
+ const discord_js_1 = require("discord.js");
7
+ const richContentBuilder_1 = require("../platform/richContentBuilder");
8
+ exports.ACCOUNT_SELECT_ID = 'account_select';
9
+ function buildAccountPayload(currentAccount, accountNames) {
10
+ const names = accountNames.length > 0 ? accountNames : ['default'];
11
+ const rc = (0, richContentBuilder_1.withTimestamp)((0, richContentBuilder_1.withFooter)((0, richContentBuilder_1.withDescription)((0, richContentBuilder_1.withColor)((0, richContentBuilder_1.withTitle)((0, richContentBuilder_1.createRichContent)(), 'Account Management'), 0x57F287), `**Current Account:** ${currentAccount}\n\n` +
12
+ `**Available Accounts (${names.length})**\n` +
13
+ names.map((name) => {
14
+ const icon = name === currentAccount ? '[x]' : '[ ]';
15
+ return `${icon} **${name}**`;
16
+ }).join('\n')), 'Select an account from the dropdown below'));
17
+ return {
18
+ richContent: rc,
19
+ components: [
20
+ {
21
+ components: [
22
+ {
23
+ type: 'selectMenu',
24
+ customId: exports.ACCOUNT_SELECT_ID,
25
+ placeholder: 'Select an account...',
26
+ options: names.map((name) => ({
27
+ label: name,
28
+ value: name,
29
+ isDefault: name === currentAccount,
30
+ })),
31
+ },
32
+ ],
33
+ },
34
+ ],
35
+ };
36
+ }
37
+ async function sendAccountUI(target, currentAccount, accountNames) {
38
+ const names = accountNames.length > 0 ? accountNames : ['default'];
39
+ const embed = new discord_js_1.EmbedBuilder()
40
+ .setTitle('Account Management')
41
+ .setColor(0x57F287)
42
+ .setDescription(`**Current Account:** ${currentAccount}\n\n` +
43
+ `**Available Accounts (${names.length})**\n` +
44
+ names.map((name) => {
45
+ const icon = name === currentAccount ? '[x]' : '[ ]';
46
+ return `${icon} **${name}**`;
47
+ }).join('\n'))
48
+ .setFooter({ text: 'Select an account from the dropdown below' })
49
+ .setTimestamp();
50
+ const selectMenu = new discord_js_1.StringSelectMenuBuilder()
51
+ .setCustomId(exports.ACCOUNT_SELECT_ID)
52
+ .setPlaceholder('Select an account...')
53
+ .addOptions(names.map((name) => ({
54
+ label: name,
55
+ value: name,
56
+ default: name === currentAccount,
57
+ })));
58
+ const row = new discord_js_1.ActionRowBuilder().addComponents(selectMenu);
59
+ await target.editReply({ content: '', embeds: [embed], components: [row] });
60
+ }
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveValidAccountName = resolveValidAccountName;
4
+ exports.listAccountNames = listAccountNames;
5
+ exports.inferParentScopeChannelId = inferParentScopeChannelId;
6
+ exports.resolveScopedAccountName = resolveScopedAccountName;
7
+ function resolveValidAccountName(requested, accounts) {
8
+ const safeAccounts = accounts && accounts.length > 0 ? accounts : [{ name: 'default', cdpPort: 9222 }];
9
+ if (!requested)
10
+ return safeAccounts[0].name;
11
+ return safeAccounts.some((account) => account.name === requested) ? requested : safeAccounts[0].name;
12
+ }
13
+ function listAccountNames(accounts) {
14
+ const safeAccounts = accounts && accounts.length > 0 ? accounts : [{ name: 'default', cdpPort: 9222 }];
15
+ return safeAccounts.map((account) => account.name);
16
+ }
17
+ function inferParentScopeChannelId(channelId, explicitParentChannelId) {
18
+ if (explicitParentChannelId && explicitParentChannelId.trim().length > 0) {
19
+ return explicitParentChannelId.trim();
20
+ }
21
+ const underscoreIndex = channelId.indexOf('_');
22
+ if (underscoreIndex > 0) {
23
+ return channelId.slice(0, underscoreIndex);
24
+ }
25
+ return null;
26
+ }
27
+ function resolveScopedAccountName(options) {
28
+ const parentChannelId = inferParentScopeChannelId(options.channelId, options.parentChannelId);
29
+ return resolveValidAccountName(options.sessionAccountName
30
+ ?? options.selectedAccountByChannel?.get(options.channelId)
31
+ ?? options.channelPrefRepo?.getAccountName(options.channelId)
32
+ ?? (parentChannelId ? options.selectedAccountByChannel?.get(parentChannelId) : null)
33
+ ?? (parentChannelId ? options.channelPrefRepo?.getAccountName(parentChannelId) : null)
34
+ ?? options.accountPrefRepo?.getAccountName(options.userId)
35
+ ?? 'default', options.accounts);
36
+ }
@@ -1,5 +1,100 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CDP_PORTS = void 0;
3
+ exports.CDP_PORTS = exports.DEFAULT_CDP_PORTS = void 0;
4
+ exports.normalizeAntigravityAccounts = normalizeAntigravityAccounts;
5
+ exports.parseAntigravityAccounts = parseAntigravityAccounts;
6
+ exports.serializeAntigravityAccounts = serializeAntigravityAccounts;
7
+ exports.getConfiguredCdpPorts = getConfiguredCdpPorts;
8
+ exports.getAccountPortMap = getAccountPortMap;
9
+ /** Default CDP port list scanned for Antigravity connections. */
10
+ exports.DEFAULT_CDP_PORTS = [9222, 9223, 9333, 9444, 9555, 9666];
11
+ function parsePort(raw) {
12
+ const port = Number(raw);
13
+ if (!Number.isInteger(port))
14
+ return null;
15
+ if (port < 1 || port > 65535)
16
+ return null;
17
+ return port;
18
+ }
19
+ function normalizeAntigravityAccounts(accounts) {
20
+ if (!accounts || accounts.length === 0) {
21
+ return [{ name: 'default', cdpPort: exports.DEFAULT_CDP_PORTS[0] }];
22
+ }
23
+ const seenNames = new Set();
24
+ const normalized = [];
25
+ for (const account of accounts) {
26
+ const name = String(account.name || '').trim();
27
+ const cdpPort = parsePort(String(account.cdpPort));
28
+ if (!name || cdpPort === null || seenNames.has(name))
29
+ continue;
30
+ seenNames.add(name);
31
+ const userDataDir = typeof account.userDataDir === 'string'
32
+ ? account.userDataDir.trim()
33
+ : '';
34
+ normalized.push({
35
+ name,
36
+ cdpPort,
37
+ ...(userDataDir ? { userDataDir } : {}),
38
+ });
39
+ }
40
+ return normalized.length > 0
41
+ ? normalized
42
+ : [{ name: 'default', cdpPort: exports.DEFAULT_CDP_PORTS[0] }];
43
+ }
44
+ function parseAntigravityAccounts(rawValue) {
45
+ if (!rawValue || rawValue.trim().length === 0) {
46
+ return [{ name: 'default', cdpPort: exports.DEFAULT_CDP_PORTS[0] }];
47
+ }
48
+ const parsed = rawValue
49
+ .split(',')
50
+ .map((entry) => entry.trim())
51
+ .filter((entry) => entry.length > 0)
52
+ .map((entry) => {
53
+ const colonIndex = entry.indexOf(':');
54
+ if (colonIndex <= 0)
55
+ return null;
56
+ const name = entry.slice(0, colonIndex).trim();
57
+ const rest = entry.slice(colonIndex + 1).trim();
58
+ const atIndex = rest.indexOf('@');
59
+ const portRaw = atIndex >= 0 ? rest.slice(0, atIndex).trim() : rest;
60
+ const userDataDirRaw = atIndex >= 0 ? rest.slice(atIndex + 1).trim() : '';
61
+ const cdpPort = parsePort(portRaw);
62
+ if (!name || cdpPort === null)
63
+ return null;
64
+ return {
65
+ name,
66
+ cdpPort,
67
+ ...(userDataDirRaw ? { userDataDir: userDataDirRaw } : {}),
68
+ };
69
+ })
70
+ .filter((account) => account !== null);
71
+ return normalizeAntigravityAccounts(parsed);
72
+ }
73
+ function serializeAntigravityAccounts(accounts) {
74
+ return normalizeAntigravityAccounts(accounts)
75
+ .map((account) => {
76
+ const userDataDir = typeof account.userDataDir === 'string'
77
+ ? account.userDataDir.trim()
78
+ : '';
79
+ return userDataDir
80
+ ? `${account.name}:${account.cdpPort}@${userDataDir}`
81
+ : `${account.name}:${account.cdpPort}`;
82
+ })
83
+ .join(',');
84
+ }
85
+ function getConfiguredCdpPorts(rawValue) {
86
+ if (!rawValue || rawValue.trim().length === 0) {
87
+ return [...exports.DEFAULT_CDP_PORTS];
88
+ }
89
+ const accounts = parseAntigravityAccounts(rawValue);
90
+ const uniquePorts = new Set();
91
+ for (const account of accounts) {
92
+ uniquePorts.add(account.cdpPort);
93
+ }
94
+ return uniquePorts.size > 0 ? [...uniquePorts] : [...exports.DEFAULT_CDP_PORTS];
95
+ }
96
+ function getAccountPortMap(rawValue) {
97
+ return Object.fromEntries(parseAntigravityAccounts(rawValue).map((account) => [account.name, account.cdpPort]));
98
+ }
4
99
  /** CDP port list scanned for Antigravity connections */
5
- exports.CDP_PORTS = [9222, 9223, 9333, 9444, 9555, 9666];
100
+ exports.CDP_PORTS = exports.DEFAULT_CDP_PORTS;
@@ -38,6 +38,7 @@ const fs = __importStar(require("fs"));
38
38
  const os = __importStar(require("os"));
39
39
  const path = __importStar(require("path"));
40
40
  const dotenv = __importStar(require("dotenv"));
41
+ const cdpPorts_1 = require("./cdpPorts");
41
42
  // Load .env at module init time (same as the original config.ts behavior).
42
43
  // dotenv will NOT override already-set env vars by default.
43
44
  dotenv.config();
@@ -103,6 +104,7 @@ function mergeConfig(persisted) {
103
104
  const logLevel = resolveLogLevel(process.env.LOG_LEVEL, persisted.logLevel);
104
105
  const extractionMode = resolveExtractionMode(process.env.EXTRACTION_MODE, persisted.extractionMode);
105
106
  const responseTimeoutMs = resolvePositiveInt(process.env.RESPONSE_TIMEOUT_MS, persisted.responseTimeoutMs, 900000);
107
+ const antigravityAccounts = resolveAntigravityAccounts(process.env.ANTIGRAVITY_ACCOUNTS, persisted.antigravityAccounts);
106
108
  // Telegram credentials — only required when Telegram is an active platform
107
109
  const telegramToken = process.env.TELEGRAM_BOT_TOKEN ?? persisted.telegramToken ?? undefined;
108
110
  const telegramAllowedUserIds = resolveTelegramAllowedUserIds(persisted);
@@ -119,6 +121,7 @@ function mergeConfig(persisted) {
119
121
  logLevel,
120
122
  extractionMode,
121
123
  responseTimeoutMs,
124
+ antigravityAccounts,
122
125
  telegramToken,
123
126
  telegramAllowedUserIds,
124
127
  platforms,
@@ -164,6 +167,17 @@ function resolveTelegramAllowedUserIds(persisted) {
164
167
  }
165
168
  return undefined;
166
169
  }
170
+ function resolveAntigravityAccounts(envValue, persistedValue) {
171
+ if (envValue && envValue.trim().length > 0) {
172
+ return (0, cdpPorts_1.parseAntigravityAccounts)(envValue);
173
+ }
174
+ if (typeof persistedValue === 'string' && persistedValue.trim().length > 0) {
175
+ return (0, cdpPorts_1.parseAntigravityAccounts)(persistedValue);
176
+ }
177
+ return Array.isArray(persistedValue)
178
+ ? (0, cdpPorts_1.normalizeAntigravityAccounts)(persistedValue)
179
+ : (0, cdpPorts_1.normalizeAntigravityAccounts)(undefined);
180
+ }
167
181
  const VALID_PLATFORMS = ['discord', 'telegram'];
168
182
  function resolvePlatforms(envValue, persistedValue) {
169
183
  if (envValue) {
@@ -6,8 +6,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.acquireLock = acquireLock;
7
7
  const logger_1 = require("./logger");
8
8
  const fs_1 = __importDefault(require("fs"));
9
+ const os_1 = __importDefault(require("os"));
9
10
  const path_1 = __importDefault(require("path"));
10
- const LOCK_FILE = path_1.default.resolve(process.cwd(), '.bot.lock');
11
+ const LOCK_DIR = process.env.XDG_RUNTIME_DIR || path_1.default.join(os_1.default.tmpdir(), `lazygravity-${process.getuid ? process.getuid() : 'user'}`);
12
+ const LOCK_FILE = path_1.default.join(LOCK_DIR, '.bot.lock');
11
13
  /**
12
14
  * Check if a process with the given PID is running
13
15
  */
@@ -20,64 +22,54 @@ function isProcessRunning(pid) {
20
22
  return false;
21
23
  }
22
24
  }
23
- /**
24
- * Stop an existing process and wait for it to exit
25
- */
26
- function killExistingProcess(pid) {
27
- logger_1.logger.warn(`🔄 Stopping existing Bot process (PID: ${pid})...`);
28
- try {
29
- process.kill(pid, 'SIGTERM');
30
- }
31
- catch {
32
- // Ignore if already terminated
33
- return;
34
- }
35
- // Wait up to 5 seconds for process to exit
36
- const deadline = Date.now() + 5000;
37
- while (Date.now() < deadline) {
38
- if (!isProcessRunning(pid)) {
39
- logger_1.logger.info(`✅ Existing process (PID: ${pid}) stopped`);
40
- return;
41
- }
42
- // Wait 50ms (busy wait)
43
- const waitUntil = Date.now() + 50;
44
- while (Date.now() < waitUntil) { /* spin */ }
45
- }
46
- // Timeout: force kill with SIGKILL
47
- logger_1.logger.warn(`⚠️ Process did not exit with SIGTERM, force killing (SIGKILL)`);
48
- try {
49
- process.kill(pid, 'SIGKILL');
50
- }
51
- catch {
52
- // ignore
53
- }
54
- }
55
25
  /**
56
26
  * Acquire a lockfile to prevent duplicate bot instances.
57
- * If another process is already running, stop it before starting.
58
27
  *
59
28
  * @returns A function to release the lock
60
29
  */
61
30
  function acquireLock() {
31
+ fs_1.default.mkdirSync(LOCK_DIR, { recursive: true, mode: 0o700 });
32
+ const dirStat = fs_1.default.lstatSync(LOCK_DIR);
33
+ if (!dirStat.isDirectory()) {
34
+ throw new Error(`Lock path is not a directory: ${LOCK_DIR}`);
35
+ }
36
+ if (typeof process.getuid === 'function' && dirStat.uid !== process.getuid()) {
37
+ throw new Error(`Lock directory is not owned by current user: ${LOCK_DIR}`);
38
+ }
39
+ if ((dirStat.mode & 0o077) !== 0) {
40
+ throw new Error(`Lock directory has overly permissive permissions: ${LOCK_DIR}`);
41
+ }
62
42
  // Check existing lock file
63
43
  if (fs_1.default.existsSync(LOCK_FILE)) {
64
44
  const content = fs_1.default.readFileSync(LOCK_FILE, 'utf-8').trim();
65
45
  const existingPid = parseInt(content, 10);
66
46
  if (!isNaN(existingPid) && existingPid !== process.pid && isProcessRunning(existingPid)) {
67
- // Stop existing process and restart
68
- killExistingProcess(existingPid);
47
+ throw new Error(`Another Bot process is already running (PID: ${existingPid})`);
69
48
  }
70
49
  else if (!isNaN(existingPid) && !isProcessRunning(existingPid)) {
71
50
  logger_1.logger.warn(`⚠️ Stale lock file detected (PID: ${existingPid} has exited). Cleaning up.`);
51
+ try {
52
+ fs_1.default.unlinkSync(LOCK_FILE);
53
+ }
54
+ catch { /* ignore */ }
72
55
  }
73
- // Remove stale lock file
56
+ }
57
+ // Create new lock file atomically
58
+ try {
59
+ const fd = fs_1.default.openSync(LOCK_FILE, 'wx', 0o600);
74
60
  try {
75
- fs_1.default.unlinkSync(LOCK_FILE);
61
+ fs_1.default.writeFileSync(fd, String(process.pid), { encoding: 'utf-8' });
62
+ }
63
+ finally {
64
+ fs_1.default.closeSync(fd);
65
+ }
66
+ }
67
+ catch (err) {
68
+ if (err?.code === 'EEXIST') {
69
+ throw new Error('Another Bot process is already running');
76
70
  }
77
- catch { /* ignore */ }
71
+ throw err;
78
72
  }
79
- // Create new lock file
80
- fs_1.default.writeFileSync(LOCK_FILE, String(process.pid), 'utf-8');
81
73
  logger_1.logger.info(`🔒 Lock acquired (PID: ${process.pid})`);
82
74
  // Cleanup function
83
75
  const releaseLock = () => {