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.
- package/dist/bot/index.js +354 -28
- package/dist/bot/telegramCommands.js +175 -48
- package/dist/bot/telegramJoinCommand.js +170 -0
- package/dist/bot/telegramMessageHandler.js +24 -7
- package/dist/bot/telegramProjectCommand.js +71 -18
- package/dist/bot/telegramStartupTarget.js +54 -0
- package/dist/commands/chatCommandHandler.js +8 -12
- package/dist/commands/joinCommandHandler.js +16 -10
- package/dist/commands/registerSlashCommands.js +13 -1
- package/dist/commands/workspaceCommandHandler.js +22 -7
- package/dist/database/accountPreferenceRepository.js +29 -0
- package/dist/database/channelPreferenceRepository.js +29 -0
- package/dist/database/chatSessionRepository.js +66 -3
- package/dist/database/telegramBindingRepository.js +13 -0
- package/dist/events/interactionCreateHandler.js +194 -13
- package/dist/events/messageCreateHandler.js +103 -7
- package/dist/handlers/accountSelectAction.js +45 -0
- package/dist/handlers/modelButtonAction.js +13 -0
- package/dist/services/cdpBridgeManager.js +23 -18
- package/dist/services/cdpConnectionPool.js +133 -206
- package/dist/services/cdpService.js +14 -5
- package/dist/services/chatSessionService.js +199 -16
- package/dist/services/userMessageDetector.js +4 -4
- package/dist/ui/accountUi.js +60 -0
- package/dist/utils/accountUtils.js +36 -0
- package/dist/utils/cdpPorts.js +97 -2
- package/dist/utils/configLoader.js +14 -0
- package/dist/utils/lockfile.js +33 -41
- package/package.json +2 -1
|
@@ -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
|
-
//
|
|
601
|
-
await
|
|
602
|
-
|
|
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
|
-
//
|
|
620
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|
package/dist/utils/cdpPorts.js
CHANGED
|
@@ -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 =
|
|
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) {
|
package/dist/utils/lockfile.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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 = () => {
|