lazy-gravity 0.10.0 → 0.12.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 +7 -0
- package/dist/bin/commands/doctor.js +45 -7
- package/dist/bot/index.js +526 -24
- package/dist/commands/chatCommandHandler.js +11 -41
- package/dist/commands/registerSlashCommands.js +62 -0
- package/dist/database/scheduleRepository.js +50 -6
- package/dist/events/interactionCreateHandler.js +163 -28
- package/dist/events/messageCreateHandler.js +24 -28
- package/dist/handlers/fileChangeButtonAction.js +12 -24
- package/dist/handlers/genericActionButtonAction.js +16 -18
- package/dist/handlers/planningButtonAction.js +105 -17
- package/dist/handlers/planningModalSubmitAction.js +38 -0
- package/dist/handlers/questionSelectAction.js +8 -7
- package/dist/handlers/questionSkipAction.js +7 -6
- package/dist/platform/discord/wrappers.js +76 -0
- package/dist/services/approvalDetector.js +43 -14
- package/dist/services/artifactService.js +61 -21
- package/dist/services/assistantDomExtractor.js +23 -3
- package/dist/services/cdpBridgeManager.js +35 -2
- package/dist/services/cdpConnectionPool.js +7 -2
- package/dist/services/cdpService.js +113 -8
- package/dist/services/chatSessionService.js +15 -8
- package/dist/services/heartbeatService.js +261 -0
- package/dist/services/notificationSender.js +12 -2
- package/dist/services/planningDetector.js +1 -1
- package/dist/services/promptDispatcher.js +21 -1
- package/dist/services/questionDetector.js +128 -76
- package/dist/services/responseMonitor.js +17 -1
- package/dist/services/scheduleService.js +101 -4
- package/dist/utils/configLoader.js +8 -0
- package/dist/utils/fileOpenCache.js +2 -6
- package/dist/utils/questionActionUtils.js +22 -0
- package/package.json +2 -1
package/dist/bot/index.js
CHANGED
|
@@ -62,6 +62,10 @@ const workspaceBindingRepository_1 = require("../database/workspaceBindingReposi
|
|
|
62
62
|
const channelPreferenceRepository_1 = require("../database/channelPreferenceRepository");
|
|
63
63
|
const chatSessionRepository_1 = require("../database/chatSessionRepository");
|
|
64
64
|
const artifactThreadRepository_1 = require("../database/artifactThreadRepository");
|
|
65
|
+
const scheduleRepository_1 = require("../database/scheduleRepository");
|
|
66
|
+
const scheduleService_1 = require("../services/scheduleService");
|
|
67
|
+
const cron_parser_1 = __importDefault(require("cron-parser"));
|
|
68
|
+
const workspaceQueue_1 = require("./workspaceQueue");
|
|
65
69
|
const workspaceService_1 = require("../services/workspaceService");
|
|
66
70
|
const workspaceCommandHandler_1 = require("../commands/workspaceCommandHandler");
|
|
67
71
|
const chatCommandHandler_1 = require("../commands/chatCommandHandler");
|
|
@@ -108,6 +112,7 @@ const buttonHandler_1 = require("../handlers/buttonHandler");
|
|
|
108
112
|
const selectHandler_1 = require("../handlers/selectHandler");
|
|
109
113
|
const approvalButtonAction_1 = require("../handlers/approvalButtonAction");
|
|
110
114
|
const planningButtonAction_1 = require("../handlers/planningButtonAction");
|
|
115
|
+
const heartbeatService_1 = require("../services/heartbeatService");
|
|
111
116
|
const errorPopupButtonAction_1 = require("../handlers/errorPopupButtonAction");
|
|
112
117
|
const runCommandButtonAction_1 = require("../handlers/runCommandButtonAction");
|
|
113
118
|
const modelButtonAction_1 = require("../handlers/modelButtonAction");
|
|
@@ -191,11 +196,15 @@ function createSerialTaskQueueForTest(queueName, traceId) {
|
|
|
191
196
|
async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService, modelService, inboundImages = [], options) {
|
|
192
197
|
// Completion signal — called exactly once when the entire prompt lifecycle ends
|
|
193
198
|
let completionSignaled = false;
|
|
199
|
+
let onApprovalRef = null;
|
|
194
200
|
const signalCompletion = (exitPath) => {
|
|
195
201
|
if (completionSignaled)
|
|
196
202
|
return;
|
|
197
203
|
completionSignaled = true;
|
|
198
204
|
logger_1.logger.debug(`[sendPrompt:${message.channelId}] signalCompletion via ${exitPath}`);
|
|
205
|
+
if (onApprovalRef) {
|
|
206
|
+
cdp.off('approval_required', onApprovalRef);
|
|
207
|
+
}
|
|
199
208
|
options?.onFullCompletion?.();
|
|
200
209
|
};
|
|
201
210
|
// Resolve output format once at the start (no mid-response switches)
|
|
@@ -587,6 +596,8 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
|
|
|
587
596
|
return;
|
|
588
597
|
}
|
|
589
598
|
const startTime = Date.now();
|
|
599
|
+
let earlyConvIdResolved = false;
|
|
600
|
+
let earlyConvIdInFlight = false;
|
|
590
601
|
await upsertLiveActivityEmbeds(`${PHASE_ICONS.thinking} Process Log`, '', PHASE_COLORS.thinking, (0, i18n_1.t)('⏱️ Elapsed: 0s | Process log'), { source: 'initial' });
|
|
591
602
|
const monitor = new responseMonitor_1.ResponseMonitor({
|
|
592
603
|
cdpService: cdp,
|
|
@@ -613,6 +624,30 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
|
|
|
613
624
|
expectedVersion: activityVersion,
|
|
614
625
|
skipWhenFinalized: true,
|
|
615
626
|
}).catch(() => { });
|
|
627
|
+
// Try to resolve conversation_id early if not already bound
|
|
628
|
+
const { artifactService, chatSessionRepo, chatSessionService } = options || {};
|
|
629
|
+
if (!earlyConvIdResolved && !earlyConvIdInFlight && artifactService && chatSessionRepo && chatSessionService) {
|
|
630
|
+
earlyConvIdInFlight = true;
|
|
631
|
+
chatSessionService.getCurrentSessionInfo(cdp)
|
|
632
|
+
.then((sessionInfo) => {
|
|
633
|
+
if (sessionInfo && sessionInfo.title && sessionInfo.title !== (0, i18n_1.t)('(Untitled)')) {
|
|
634
|
+
const session = chatSessionRepo.findByChannelId(message.channelId);
|
|
635
|
+
const workspaceDirName = (session
|
|
636
|
+
? bridge.pool.extractProjectName(session.workspacePath)
|
|
637
|
+
: cdp.getCurrentWorkspaceName()) ?? undefined;
|
|
638
|
+
const convId = artifactService.findConversationByTitle(sessionInfo.title, workspaceDirName);
|
|
639
|
+
if (convId) {
|
|
640
|
+
chatSessionRepo.setConversationId(message.channelId, convId);
|
|
641
|
+
earlyConvIdResolved = true;
|
|
642
|
+
logger_1.logger.debug(`[EarlyBinding] Saved conversation_id: "${convId}" for channel: ${message.channelId}`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
})
|
|
646
|
+
.catch(() => { })
|
|
647
|
+
.finally(() => {
|
|
648
|
+
earlyConvIdInFlight = false;
|
|
649
|
+
});
|
|
650
|
+
}
|
|
616
651
|
},
|
|
617
652
|
onProgress: (text) => {
|
|
618
653
|
if (isFinalized)
|
|
@@ -706,6 +741,12 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
|
|
|
706
741
|
const components = [];
|
|
707
742
|
if (!citedFiles)
|
|
708
743
|
citedFiles = [];
|
|
744
|
+
// Only pause when newly created conversation folders and files need to flush to disk
|
|
745
|
+
const existingConvId = options?.chatSessionRepo?.findByChannelId(message.channelId)?.conversationId;
|
|
746
|
+
if (!existingConvId) {
|
|
747
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
748
|
+
}
|
|
749
|
+
let activeConversationId = undefined;
|
|
709
750
|
if (options && message.guild) {
|
|
710
751
|
try {
|
|
711
752
|
const sessionInfo = await options.chatSessionService.getCurrentSessionInfo(cdp);
|
|
@@ -733,6 +774,7 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
|
|
|
733
774
|
const convId = options.artifactService.findConversationByTitle(sessionInfo.title, workspaceDirName);
|
|
734
775
|
if (convId) {
|
|
735
776
|
options.chatSessionRepo.setConversationId(message.channelId, convId);
|
|
777
|
+
activeConversationId = convId;
|
|
736
778
|
}
|
|
737
779
|
}
|
|
738
780
|
}
|
|
@@ -741,6 +783,51 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
|
|
|
741
783
|
logger_1.logger.error('[Rename] Failed to get title from Antigravity and rename:', e);
|
|
742
784
|
}
|
|
743
785
|
}
|
|
786
|
+
const classifiedForFiles = monitor.getLastClassified();
|
|
787
|
+
if (classifiedForFiles?.fileChanges && classifiedForFiles.fileChanges.length > 0) {
|
|
788
|
+
for (const fc of classifiedForFiles.fileChanges) {
|
|
789
|
+
if (!citedFiles.includes(fc.path)) {
|
|
790
|
+
citedFiles.push(fc.path);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (!activeConversationId && options?.chatSessionRepo) {
|
|
795
|
+
activeConversationId = options.chatSessionRepo.findByChannelId(message.channelId)?.conversationId ?? undefined;
|
|
796
|
+
}
|
|
797
|
+
if (activeConversationId && options?.artifactService) {
|
|
798
|
+
const artifacts = options.artifactService.listArtifacts(activeConversationId);
|
|
799
|
+
const planArt = artifacts.find(art => art.artifactType === 'ARTIFACT_TYPE_IMPLEMENTATION_PLAN' ||
|
|
800
|
+
art.filename.toLowerCase() === 'implementation_plan.md');
|
|
801
|
+
let planTime = 0;
|
|
802
|
+
if (planArt) {
|
|
803
|
+
try {
|
|
804
|
+
planTime = fs_1.default.statSync(planArt.absolutePath).mtimeMs;
|
|
805
|
+
}
|
|
806
|
+
catch { /* ignore */ }
|
|
807
|
+
}
|
|
808
|
+
for (const art of artifacts) {
|
|
809
|
+
const nameWithExt = art.filename;
|
|
810
|
+
const nameWithoutExt = art.filename.replace(/\.[^/.]+$/, "");
|
|
811
|
+
const regex = new RegExp(`\\b${nameWithoutExt.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')}\\b`, 'i');
|
|
812
|
+
const mentionsArtifact = finalOutputText.includes(nameWithExt) || finalOutputText.includes(`\`${nameWithoutExt}\``) || regex.test(finalOutputText);
|
|
813
|
+
const isTask = art.artifactType === 'ARTIFACT_TYPE_TASK' || nameWithExt.toLowerCase() === 'task.md';
|
|
814
|
+
const isWalkthrough = art.artifactType === 'ARTIFACT_TYPE_WALKTHROUGH' || nameWithExt.toLowerCase() === 'walkthrough.md';
|
|
815
|
+
let isSpecialArtifact = false;
|
|
816
|
+
if (isTask || isWalkthrough) {
|
|
817
|
+
let artTime = 0;
|
|
818
|
+
try {
|
|
819
|
+
artTime = fs_1.default.statSync(art.absolutePath).mtimeMs;
|
|
820
|
+
}
|
|
821
|
+
catch { /* ignore */ }
|
|
822
|
+
if (artTime > planTime) {
|
|
823
|
+
isSpecialArtifact = true;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if ((mentionsArtifact || isSpecialArtifact) && !citedFiles.includes(nameWithExt)) {
|
|
827
|
+
citedFiles.push(nameWithExt);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
744
831
|
if (citedFiles && citedFiles.length > 0) {
|
|
745
832
|
// Strip trailing punctuation and deduplicate
|
|
746
833
|
let uniqueFiles = Array.from(new Set(citedFiles.map(f => {
|
|
@@ -756,38 +843,82 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
|
|
|
756
843
|
// Max 5 buttons per row in Discord
|
|
757
844
|
for (let i = 0; i < Math.min(uniqueFiles.length, 5); i++) {
|
|
758
845
|
let fileUrl = uniqueFiles[i];
|
|
846
|
+
let isArtifactMatch = false;
|
|
847
|
+
let artifactDisplayName = fileUrl;
|
|
848
|
+
if (activeConversationId && options?.artifactService) {
|
|
849
|
+
const artifacts = options.artifactService.listArtifacts(activeConversationId);
|
|
850
|
+
const rawName = fileUrl.replace(/^file:\/\/\//, '');
|
|
851
|
+
const matched = artifacts.find(a => a.filename.toLowerCase() === fileUrl.toLowerCase() ||
|
|
852
|
+
a.filename.toLowerCase() === rawName.toLowerCase());
|
|
853
|
+
if (matched) {
|
|
854
|
+
isArtifactMatch = true;
|
|
855
|
+
fileUrl = `file:///${matched.absolutePath.replace(/\\/g, '/')}`;
|
|
856
|
+
artifactDisplayName = matched.filename;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
759
859
|
const wsPath = cdp.getCurrentWorkspacePath();
|
|
760
860
|
if (!wsPath) {
|
|
761
861
|
continue;
|
|
762
862
|
}
|
|
763
|
-
if (!
|
|
764
|
-
fileUrl
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
863
|
+
if (!isArtifactMatch) {
|
|
864
|
+
if (!fileUrl.startsWith('file:///') && !path_1.default.isAbsolute(fileUrl)) {
|
|
865
|
+
fileUrl = `file:///${path_1.default.resolve(wsPath, fileUrl).replace(/\\/g, '/')}`;
|
|
866
|
+
}
|
|
867
|
+
let fsPath = fileUrl.replace(/^file:\/\/\//, '');
|
|
868
|
+
if (path_1.default.sep === '/' && !fsPath.startsWith('/')) {
|
|
869
|
+
fsPath = '/' + fsPath;
|
|
870
|
+
}
|
|
871
|
+
else if (process.platform === 'win32' && fsPath.startsWith('/')) {
|
|
872
|
+
fsPath = fsPath.substring(1);
|
|
873
|
+
}
|
|
874
|
+
const relative = path_1.default.relative(wsPath, fsPath);
|
|
875
|
+
const isArtifact = fsPath.includes('.gemini') && fsPath.includes('antigravity') && fsPath.includes('brain');
|
|
876
|
+
if (!isArtifact && (relative.startsWith('..') || path_1.default.isAbsolute(relative))) {
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (!fs_1.default.existsSync(fsPath)) {
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
779
882
|
}
|
|
780
883
|
let displayName = fileUrl;
|
|
781
|
-
if (
|
|
884
|
+
if (isArtifactMatch) {
|
|
885
|
+
displayName = artifactDisplayName;
|
|
886
|
+
}
|
|
887
|
+
else if (displayName.startsWith('file:///')) {
|
|
782
888
|
const parts = displayName.split('/');
|
|
783
889
|
displayName = parts[parts.length - 1];
|
|
784
890
|
}
|
|
785
891
|
const hashId = Math.random().toString(36).substring(2, 10);
|
|
786
892
|
fileOpenCache_1.fileOpenCache.set(hashId, fileUrl);
|
|
893
|
+
let customId = `file_open:cache:${hashId}`;
|
|
894
|
+
if (isArtifactMatch && activeConversationId && displayName) {
|
|
895
|
+
const artId = `file_open:art:${activeConversationId}:${displayName}`;
|
|
896
|
+
if (artId.length < 100) {
|
|
897
|
+
customId = artId;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
else if (wsPath) {
|
|
901
|
+
let fsPath = fileUrl.replace(/^file:\/\/\//, '');
|
|
902
|
+
if (path_1.default.sep === '/' && !fsPath.startsWith('/')) {
|
|
903
|
+
fsPath = '/' + fsPath;
|
|
904
|
+
}
|
|
905
|
+
else if (process.platform === 'win32' && fsPath.startsWith('/')) {
|
|
906
|
+
fsPath = fsPath.substring(1);
|
|
907
|
+
}
|
|
908
|
+
const resolvedFsPath = path_1.default.resolve(fsPath);
|
|
909
|
+
const relative = path_1.default.relative(wsPath, resolvedFsPath);
|
|
910
|
+
if (!relative.startsWith('..') && !path_1.default.isAbsolute(relative)) {
|
|
911
|
+
const relSlash = relative.replace(/\\/g, '/');
|
|
912
|
+
const relId = `file_open:rel:${relSlash}`;
|
|
913
|
+
if (relId.length < 100) {
|
|
914
|
+
customId = relId;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
787
918
|
row.addComponents(new discord_js_1.ButtonBuilder()
|
|
788
|
-
.setCustomId(
|
|
789
|
-
.setLabel(`Open ${displayName.substring(0, 20)}`)
|
|
790
|
-
.setStyle(discord_js_1.ButtonStyle.Primary));
|
|
919
|
+
.setCustomId(customId)
|
|
920
|
+
.setLabel(isArtifactMatch ? `Review ${displayName.substring(0, 15)}` : `Open ${displayName.substring(0, 20)}`)
|
|
921
|
+
.setStyle(isArtifactMatch ? discord_js_1.ButtonStyle.Success : discord_js_1.ButtonStyle.Primary));
|
|
791
922
|
}
|
|
792
923
|
if (row.components.length > 0) {
|
|
793
924
|
components.push(row);
|
|
@@ -895,6 +1026,32 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
|
|
|
895
1026
|
}
|
|
896
1027
|
},
|
|
897
1028
|
});
|
|
1029
|
+
onApprovalRef = async () => {
|
|
1030
|
+
if (isFinalized)
|
|
1031
|
+
return;
|
|
1032
|
+
const textToUse = lastProgressText || monitor.getLastText() || '';
|
|
1033
|
+
const separated = (0, discordFormatter_1.splitOutputAndLogs)(textToUse);
|
|
1034
|
+
const outputText = separated.output || textToUse;
|
|
1035
|
+
if (outputText && outputText.trim().length > 0) {
|
|
1036
|
+
liveResponseUpdateVersion += 1;
|
|
1037
|
+
const responseVersion = liveResponseUpdateVersion;
|
|
1038
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
1039
|
+
await upsertLiveResponseEmbeds((0, i18n_1.t)('Response'), outputText, PHASE_COLORS.thinking, (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Awaiting Approval`), {
|
|
1040
|
+
source: 'approval-pause',
|
|
1041
|
+
expectedVersion: responseVersion,
|
|
1042
|
+
skipWhenFinalized: true,
|
|
1043
|
+
}).catch(() => { });
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
cdp.on('approval_required', onApprovalRef);
|
|
1047
|
+
if (options?.onMonitorCreated) {
|
|
1048
|
+
options.onMonitorCreated({
|
|
1049
|
+
stop: async () => {
|
|
1050
|
+
isFinalized = true;
|
|
1051
|
+
await monitor.stop();
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
898
1055
|
await monitor.start();
|
|
899
1056
|
// 1-second elapsed timer — updates footer independently of process log events
|
|
900
1057
|
const elapsedTimer = setInterval(() => {
|
|
@@ -948,9 +1105,13 @@ const startBot = async (cliLogLevel) => {
|
|
|
948
1105
|
const workspaceBindingRepo = new workspaceBindingRepository_1.WorkspaceBindingRepository(db);
|
|
949
1106
|
const chatSessionRepo = new chatSessionRepository_1.ChatSessionRepository(db);
|
|
950
1107
|
const artifactThreadRepo = new artifactThreadRepository_1.ArtifactThreadRepository(db);
|
|
1108
|
+
const scheduleRepo = new scheduleRepository_1.ScheduleRepository(db);
|
|
1109
|
+
const scheduleService = new scheduleService_1.ScheduleService(scheduleRepo);
|
|
1110
|
+
const workspaceQueue = new workspaceQueue_1.WorkspaceQueue();
|
|
951
1111
|
const artifactService = new artifactService_1.ArtifactService();
|
|
952
1112
|
const workspaceService = new workspaceService_1.WorkspaceService(config.workspaceBaseDir);
|
|
953
1113
|
const channelManager = new channelManager_1.ChannelManager();
|
|
1114
|
+
const heartbeatService = new heartbeatService_1.HeartbeatService();
|
|
954
1115
|
// Auto-launch Antigravity with CDP port if not already running
|
|
955
1116
|
await (0, antigravityLauncher_1.ensureAntigravityRunning)();
|
|
956
1117
|
// Initialize CDP bridge (lazy connection: pool creation only)
|
|
@@ -959,6 +1120,8 @@ const startBot = async (cliLogLevel) => {
|
|
|
959
1120
|
.filter((account) => typeof account.userDataDir === 'string' && account.userDataDir.trim().length > 0)
|
|
960
1121
|
.map((account) => [account.name, account.userDataDir.trim()]));
|
|
961
1122
|
const bridge = (0, cdpBridgeManager_1.initCdpBridge)(config.autoApproveFileEdits, accountPorts, accountUserDataDirs);
|
|
1123
|
+
bridge.chatSessionRepo = chatSessionRepo;
|
|
1124
|
+
bridge.artifactService = artifactService;
|
|
962
1125
|
// Initialize CDP-dependent services (constructor CDP dependency removed)
|
|
963
1126
|
const chatSessionService = new chatSessionService_1.ChatSessionService();
|
|
964
1127
|
const titleGenerator = new titleGeneratorService_1.TitleGeneratorService();
|
|
@@ -1009,7 +1172,20 @@ const startBot = async (cliLogLevel) => {
|
|
|
1009
1172
|
accountPrefRepo,
|
|
1010
1173
|
accounts: config.antigravityAccounts,
|
|
1011
1174
|
});
|
|
1012
|
-
await (0, antigravityLauncher_1.startAntigravity)(accountPorts[accountName] ?? 9222);
|
|
1175
|
+
const status = await (0, antigravityLauncher_1.startAntigravity)(accountPorts[accountName] ?? 9222);
|
|
1176
|
+
if (status === 'started') {
|
|
1177
|
+
const cdp = new cdpService_1.CdpService({ portsToScan: [accountPorts[accountName] ?? 9222] });
|
|
1178
|
+
try {
|
|
1179
|
+
await cdp.connect();
|
|
1180
|
+
await cdp.openChatPanel();
|
|
1181
|
+
}
|
|
1182
|
+
catch (e) {
|
|
1183
|
+
// ignore
|
|
1184
|
+
}
|
|
1185
|
+
finally {
|
|
1186
|
+
await cdp.disconnect().catch(() => { });
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1013
1189
|
});
|
|
1014
1190
|
const chatHandler = new chatCommandHandler_1.ChatCommandHandler(chatSessionService, chatSessionRepo, workspaceBindingRepo, channelManager, workspaceService, bridge.pool, (channelId, userId) => (0, accountUtils_1.resolveScopedAccountName)({
|
|
1015
1191
|
channelId,
|
|
@@ -1050,6 +1226,8 @@ const startBot = async (cliLogLevel) => {
|
|
|
1050
1226
|
}));
|
|
1051
1227
|
client.once(discord_js_1.Events.ClientReady, async (readyClient) => {
|
|
1052
1228
|
logger_1.logger.info(`Ready! Logged in as ${readyClient.user.tag} | extractionMode=${config.extractionMode}`);
|
|
1229
|
+
heartbeatService.init(readyClient, bridge);
|
|
1230
|
+
heartbeatService.start();
|
|
1053
1231
|
try {
|
|
1054
1232
|
await (0, registerSlashCommands_1.registerSlashCommands)(discordToken, discordClientId, config.guildId);
|
|
1055
1233
|
}
|
|
@@ -1090,6 +1268,74 @@ const startBot = async (cliLogLevel) => {
|
|
|
1090
1268
|
catch (error) {
|
|
1091
1269
|
logger_1.logger.warn('Failed to send startup dashboard embed:', error);
|
|
1092
1270
|
}
|
|
1271
|
+
// Restore scheduled tasks
|
|
1272
|
+
const scheduleJobCallback = async (schedule) => {
|
|
1273
|
+
logger_1.logger.info(`[Schedule] Trigger callback running for task ${schedule.id}. Workspace: ${schedule.workspacePath}, promptLength: ${schedule.prompt.length}`);
|
|
1274
|
+
try {
|
|
1275
|
+
let channelId = schedule.channelId;
|
|
1276
|
+
const isWindows = process.platform === 'win32';
|
|
1277
|
+
const bindings = workspaceBindingRepo.findAll().filter(b => {
|
|
1278
|
+
const absPath = workspaceService.getWorkspacePath(b.workspacePath);
|
|
1279
|
+
return isWindows
|
|
1280
|
+
? absPath.toLowerCase() === schedule.workspacePath.toLowerCase()
|
|
1281
|
+
: absPath === schedule.workspacePath;
|
|
1282
|
+
});
|
|
1283
|
+
if (!channelId || !bindings.some(b => b.channelId === channelId)) {
|
|
1284
|
+
channelId = bindings[0]?.channelId;
|
|
1285
|
+
}
|
|
1286
|
+
if (!channelId) {
|
|
1287
|
+
logger_1.logger.warn(`[Schedule] No channel bound to workspace ${schedule.workspacePath}. Skipping task ${schedule.id}.`);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
const channel = await readyClient.channels.fetch(channelId).catch(() => null);
|
|
1291
|
+
if (channel && channel.isTextBased() && 'send' in channel) {
|
|
1292
|
+
logger_1.logger.info(`[Schedule] Sending trigger notification to channel ${channelId} for task ${schedule.id}`);
|
|
1293
|
+
const message = await channel.send({
|
|
1294
|
+
content: '⏰ **Scheduled Task Triggered**',
|
|
1295
|
+
allowedMentions: { parse: [] }
|
|
1296
|
+
});
|
|
1297
|
+
const projectLabel = bridge.pool.extractProjectName(schedule.workspacePath);
|
|
1298
|
+
workspaceQueue.incrementDepth(schedule.workspacePath);
|
|
1299
|
+
await workspaceQueue.enqueue(schedule.workspacePath, async () => {
|
|
1300
|
+
try {
|
|
1301
|
+
logger_1.logger.info(`[Schedule] Dispatching prompt to promptDispatcher for task ${schedule.id}`);
|
|
1302
|
+
const preferredAccount = bridge.pool.getPreferredAccountForWorkspace(schedule.workspacePath)
|
|
1303
|
+
|| (config.antigravityAccounts?.[0]?.name ?? 'default');
|
|
1304
|
+
const cdp = await bridge.pool.getOrConnect(schedule.workspacePath, { name: preferredAccount });
|
|
1305
|
+
await promptDispatcher.send({
|
|
1306
|
+
message: message,
|
|
1307
|
+
prompt: schedule.prompt,
|
|
1308
|
+
cdp,
|
|
1309
|
+
inboundImages: [],
|
|
1310
|
+
options: {
|
|
1311
|
+
chatSessionService,
|
|
1312
|
+
chatSessionRepo,
|
|
1313
|
+
channelManager,
|
|
1314
|
+
titleGenerator,
|
|
1315
|
+
userPrefRepo,
|
|
1316
|
+
artifactService,
|
|
1317
|
+
extractionMode: config.extractionMode,
|
|
1318
|
+
},
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
finally {
|
|
1322
|
+
const remainingDepth = workspaceQueue.decrementDepth(schedule.workspacePath);
|
|
1323
|
+
if (remainingDepth > 0) {
|
|
1324
|
+
logger_1.logger.info(`[Queue:${projectLabel}] Task done, ${remainingDepth} remaining`);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
else {
|
|
1330
|
+
logger_1.logger.warn(`[Schedule] Channel ${channelId} not found or not text-based for task ${schedule.id}.`);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
catch (error) {
|
|
1334
|
+
logger_1.logger.error(`[Schedule] Failed to execute task ${schedule.id}:`, error);
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
const restoredCount = scheduleService.restoreAll(scheduleJobCallback);
|
|
1338
|
+
logger_1.logger.info(`[Schedule] Restored ${restoredCount} scheduled tasks.`);
|
|
1093
1339
|
});
|
|
1094
1340
|
// [Discord Interactions API] Slash command interaction handler
|
|
1095
1341
|
client.on(discord_js_1.Events.InteractionCreate, (0, interactionCreateHandler_1.createInteractionCreateHandler)({
|
|
@@ -1123,7 +1369,9 @@ const startBot = async (cliLogLevel) => {
|
|
|
1123
1369
|
channelManager,
|
|
1124
1370
|
titleGenerator,
|
|
1125
1371
|
antigravityAccounts: config.antigravityAccounts,
|
|
1126
|
-
|
|
1372
|
+
heartbeatService,
|
|
1373
|
+
scheduleService,
|
|
1374
|
+
handleSlashInteraction: async (interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg, accountPrefRepoArg, channelPrefRepoArg, antigravityAccountsArg, chatSessionRepoArg, scheduleServiceArg) => handleSlashInteraction(interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, chatSessionService, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg, promptDispatcher, templateRepo, joinHandler, userPrefRepo, accountPrefRepoArg, channelPrefRepoArg, antigravityAccountsArg, chatSessionRepoArg, artifactService, scheduleServiceArg, heartbeatService),
|
|
1127
1375
|
handleTemplateUse: async (interaction, templateId) => {
|
|
1128
1376
|
const template = templateRepo.findById(templateId);
|
|
1129
1377
|
if (!template) {
|
|
@@ -1245,6 +1493,8 @@ const startBot = async (cliLogLevel) => {
|
|
|
1245
1493
|
accountPrefRepo,
|
|
1246
1494
|
channelPrefRepo,
|
|
1247
1495
|
antigravityAccounts: config.antigravityAccounts,
|
|
1496
|
+
heartbeatService,
|
|
1497
|
+
workspaceQueue,
|
|
1248
1498
|
}));
|
|
1249
1499
|
await client.login(discordToken);
|
|
1250
1500
|
} // end: else (credentials present)
|
|
@@ -1515,10 +1765,22 @@ async function autoRenameChannel(message, chatSessionRepo, titleGenerator, chann
|
|
|
1515
1765
|
logger_1.logger.error('[AutoRename] Rename failed:', err);
|
|
1516
1766
|
}
|
|
1517
1767
|
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Utility to parse a cron expression and format the next run time.
|
|
1770
|
+
*/
|
|
1771
|
+
function formatNextRunTime(cronExpression) {
|
|
1772
|
+
try {
|
|
1773
|
+
const interval = cron_parser_1.default.parse(cronExpression);
|
|
1774
|
+
return interval.next().toDate().toLocaleString();
|
|
1775
|
+
}
|
|
1776
|
+
catch (err) {
|
|
1777
|
+
return 'Invalid Cron';
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1518
1780
|
/**
|
|
1519
1781
|
* Handle Discord Interactions API slash commands
|
|
1520
1782
|
*/
|
|
1521
|
-
async function handleSlashInteraction(interaction, handler, bridge, wsHandler, chatHandler, cleanupHandler, chatSessionService, modeService, modelService, autoAcceptService, _client, promptDispatcher, templateRepo, joinHandler, userPrefRepo, accountPrefRepo, channelPrefRepo, antigravityAccounts = [{ name: 'default', cdpPort: 9222 }], chatSessionRepo, artifactService, scheduleService) {
|
|
1783
|
+
async function handleSlashInteraction(interaction, handler, bridge, wsHandler, chatHandler, cleanupHandler, chatSessionService, modeService, modelService, autoAcceptService, _client, promptDispatcher, templateRepo, joinHandler, userPrefRepo, accountPrefRepo, channelPrefRepo, antigravityAccounts = [{ name: 'default', cdpPort: 9222 }], chatSessionRepo, artifactService, scheduleService, heartbeatService) {
|
|
1522
1784
|
const commandName = interaction.commandName;
|
|
1523
1785
|
const getAccountPort = (accountName) => {
|
|
1524
1786
|
const match = antigravityAccounts.find((account) => account.name === accountName);
|
|
@@ -2043,7 +2305,9 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
|
|
|
2043
2305
|
const candidatePath = path_1.default.isAbsolute(rawPath) ? rawPath : path_1.default.join(wsPath, rawPath);
|
|
2044
2306
|
const relative = path_1.default.relative(wsPath, candidatePath);
|
|
2045
2307
|
if (!relative.startsWith('..') && !path_1.default.isAbsolute(relative)) {
|
|
2046
|
-
|
|
2308
|
+
if (fs_1.default.existsSync(candidatePath)) {
|
|
2309
|
+
resolvedPath = candidatePath;
|
|
2310
|
+
}
|
|
2047
2311
|
}
|
|
2048
2312
|
}
|
|
2049
2313
|
}
|
|
@@ -2079,6 +2343,244 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
|
|
|
2079
2343
|
});
|
|
2080
2344
|
break;
|
|
2081
2345
|
}
|
|
2346
|
+
case 'heartbeat': {
|
|
2347
|
+
const subcommand = interaction.options.getSubcommand();
|
|
2348
|
+
if (!heartbeatService) {
|
|
2349
|
+
await interaction.editReply({ content: 'Heartbeat service not available.' });
|
|
2350
|
+
break;
|
|
2351
|
+
}
|
|
2352
|
+
const envOverrides = [];
|
|
2353
|
+
if (process.env.HEARTBEAT_ENABLED !== undefined)
|
|
2354
|
+
envOverrides.push('HEARTBEAT_ENABLED');
|
|
2355
|
+
if (process.env.HEARTBEAT_INTERVAL_MS !== undefined)
|
|
2356
|
+
envOverrides.push('HEARTBEAT_INTERVAL_MS');
|
|
2357
|
+
if (process.env.HEARTBEAT_CHANNEL_ID !== undefined)
|
|
2358
|
+
envOverrides.push('HEARTBEAT_CHANNEL_ID');
|
|
2359
|
+
let warningPrefix = '';
|
|
2360
|
+
if (envOverrides.length > 0) {
|
|
2361
|
+
warningPrefix = `⚠️ **Warning**: Environment override(s) active: ${envOverrides.join(', ')}. Changes saved to config.json may not take effect until overrides are removed.\n\n`;
|
|
2362
|
+
}
|
|
2363
|
+
if (subcommand === 'on') {
|
|
2364
|
+
const intervalStr = interaction.options.getString('interval') || '1h';
|
|
2365
|
+
const targetChannel = interaction.options.getChannel('channel') || interaction.channel;
|
|
2366
|
+
if (!targetChannel || typeof targetChannel.isTextBased !== 'function' || !targetChannel.isTextBased()) {
|
|
2367
|
+
await interaction.editReply({ content: '⚠️ Please select a valid text channel.' });
|
|
2368
|
+
break;
|
|
2369
|
+
}
|
|
2370
|
+
// Check permissions
|
|
2371
|
+
const botUser = interaction.client.user;
|
|
2372
|
+
const permissions = targetChannel.permissionsFor?.(botUser);
|
|
2373
|
+
if (!permissions || !permissions.has('SendMessages') || !permissions.has('EmbedLinks')) {
|
|
2374
|
+
await interaction.editReply({ content: '⚠️ Bot does not have permission to send messages and embed links in that channel.' });
|
|
2375
|
+
break;
|
|
2376
|
+
}
|
|
2377
|
+
const intervalMs = (0, heartbeatService_1.parseInterval)(intervalStr);
|
|
2378
|
+
if (intervalMs === null || intervalMs <= 0) {
|
|
2379
|
+
await interaction.editReply({ content: '⚠️ Invalid interval format. Use a value with a unit, e.g. "1d", "1h", "30m" (bare numbers are not allowed).' });
|
|
2380
|
+
break;
|
|
2381
|
+
}
|
|
2382
|
+
if (intervalMs < 10000) {
|
|
2383
|
+
await interaction.editReply({ content: '⚠️ Interval must be at least 10 seconds.' });
|
|
2384
|
+
break;
|
|
2385
|
+
}
|
|
2386
|
+
if (intervalMs > 2147483647) {
|
|
2387
|
+
await interaction.editReply({ content: '⚠️ Interval cannot be greater than 24.8 days (2147483647 ms).' });
|
|
2388
|
+
break;
|
|
2389
|
+
}
|
|
2390
|
+
await heartbeatService.updateConfig(true, intervalMs, targetChannel.id);
|
|
2391
|
+
await interaction.editReply({
|
|
2392
|
+
content: `${warningPrefix}💓 Heartbeat enabled! Sending updates every **${intervalStr}** to channel <#${targetChannel.id}>.`
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
else if (subcommand === 'off') {
|
|
2396
|
+
await heartbeatService.disable();
|
|
2397
|
+
await interaction.editReply({ content: `${warningPrefix}💓 Heartbeat disabled.` });
|
|
2398
|
+
}
|
|
2399
|
+
else if (subcommand === 'status') {
|
|
2400
|
+
const config = (0, config_1.loadConfig)();
|
|
2401
|
+
const uptimeMs = Date.now() - heartbeatService.botStartTime;
|
|
2402
|
+
const uptimeStr = (0, heartbeatService_1.formatDuration)(uptimeMs);
|
|
2403
|
+
const lastActivityStr = (0, heartbeatService_1.formatRelativeTime)(heartbeatService.lastActivityTimestamp);
|
|
2404
|
+
const activeWorkspaces = bridge.pool.getActiveWorkspaceNames();
|
|
2405
|
+
const activeCount = activeWorkspaces.length;
|
|
2406
|
+
const activeList = activeCount > 0 ? activeWorkspaces.join(', ') : 'None';
|
|
2407
|
+
const intervalVal = config.heartbeatIntervalMs != null ? (0, heartbeatService_1.formatDuration)(config.heartbeatIntervalMs) : 'N/A';
|
|
2408
|
+
const statusEmbed = new discord_js_1.EmbedBuilder()
|
|
2409
|
+
.setTitle('💓 Heartbeat Status')
|
|
2410
|
+
.setColor(config.heartbeatEnabled ? 0x00CC88 : 0x888888)
|
|
2411
|
+
.addFields({ name: 'Enabled', value: config.heartbeatEnabled ? '🟢 Yes' : '⚪ No', inline: true }, { name: 'Interval', value: config.heartbeatEnabled ? intervalVal : 'N/A', inline: true }, { name: 'Target Channel', value: config.heartbeatChannelId ? `<#${config.heartbeatChannelId}>` : 'N/A', inline: true }, { name: 'Active Sessions', value: `${activeCount} (${activeList})`, inline: true }, { name: 'Uptime', value: uptimeStr, inline: true }, { name: 'Last Activity', value: lastActivityStr, inline: true })
|
|
2412
|
+
.setTimestamp();
|
|
2413
|
+
await interaction.editReply({ embeds: [statusEmbed] });
|
|
2414
|
+
}
|
|
2415
|
+
break;
|
|
2416
|
+
}
|
|
2417
|
+
case 'schedule': {
|
|
2418
|
+
if (!scheduleService) {
|
|
2419
|
+
await interaction.editReply({ content: 'Schedule service not available.' });
|
|
2420
|
+
break;
|
|
2421
|
+
}
|
|
2422
|
+
const subcommand = interaction.options.getSubcommand();
|
|
2423
|
+
if (subcommand === 'list') {
|
|
2424
|
+
const schedules = scheduleService.listSchedules();
|
|
2425
|
+
if (schedules.length === 0) {
|
|
2426
|
+
await interaction.editReply({ content: 'No scheduled tasks found.' });
|
|
2427
|
+
break;
|
|
2428
|
+
}
|
|
2429
|
+
let formatted = '';
|
|
2430
|
+
let truncatedCount = 0;
|
|
2431
|
+
for (const s of schedules) {
|
|
2432
|
+
const nextRunStr = formatNextRunTime(s.cronExpression);
|
|
2433
|
+
const line = `**ID:** ${s.id} | **Cron:** \`${s.cronExpression}\` | **Next:** ${nextRunStr} | **Prompt:** ${s.prompt}\n`;
|
|
2434
|
+
if (formatted.length + line.length > 3900) {
|
|
2435
|
+
truncatedCount = schedules.length - schedules.indexOf(s);
|
|
2436
|
+
break;
|
|
2437
|
+
}
|
|
2438
|
+
formatted += line;
|
|
2439
|
+
}
|
|
2440
|
+
if (truncatedCount > 0) {
|
|
2441
|
+
formatted += `\n*...and ${truncatedCount} more task(s) (truncated due to Discord size limit).*`;
|
|
2442
|
+
}
|
|
2443
|
+
const embed = new discord_js_1.EmbedBuilder()
|
|
2444
|
+
.setTitle('🕒 Scheduled Tasks')
|
|
2445
|
+
.setDescription(formatted)
|
|
2446
|
+
.setColor(0x00CC88);
|
|
2447
|
+
await interaction.editReply({ embeds: [embed] });
|
|
2448
|
+
break;
|
|
2449
|
+
}
|
|
2450
|
+
if (subcommand === 'add') {
|
|
2451
|
+
const cronExpr = interaction.options.getString('cron', true);
|
|
2452
|
+
const promptText = interaction.options.getString('prompt', true);
|
|
2453
|
+
const workspacePath = wsHandler.getWorkspaceForChannel(interaction.channelId);
|
|
2454
|
+
if (!workspacePath) {
|
|
2455
|
+
await interaction.editReply({ content: '⚠️ This channel is not bound to a workspace. Please bind it first.' });
|
|
2456
|
+
break;
|
|
2457
|
+
}
|
|
2458
|
+
try {
|
|
2459
|
+
const jobCb = scheduleService.getJobCallback();
|
|
2460
|
+
if (!jobCb) {
|
|
2461
|
+
await interaction.editReply({ content: '⚠️ Schedule service is still initializing. Please try again in a few seconds.' });
|
|
2462
|
+
break;
|
|
2463
|
+
}
|
|
2464
|
+
const record = scheduleService.addSchedule(cronExpr, promptText, workspacePath, interaction.channelId, jobCb);
|
|
2465
|
+
const nextRun = formatNextRunTime(cronExpr);
|
|
2466
|
+
const nextRunStr = nextRun !== 'Invalid Cron' ? ` (Next run: ${nextRun})` : '';
|
|
2467
|
+
await interaction.editReply({ content: `✅ Scheduled task added! (ID: ${record.id})${nextRunStr}` });
|
|
2468
|
+
}
|
|
2469
|
+
catch (error) {
|
|
2470
|
+
await interaction.editReply({ content: `❌ Failed to add schedule: ${error.message}` });
|
|
2471
|
+
}
|
|
2472
|
+
break;
|
|
2473
|
+
}
|
|
2474
|
+
if (subcommand === 'remove') {
|
|
2475
|
+
const id = interaction.options.getInteger('id', true);
|
|
2476
|
+
const success = scheduleService.removeSchedule(id);
|
|
2477
|
+
if (success) {
|
|
2478
|
+
await interaction.editReply({ content: `✅ Removed scheduled task ID: ${id}` });
|
|
2479
|
+
}
|
|
2480
|
+
else {
|
|
2481
|
+
await interaction.editReply({ content: `⚠️ Scheduled task ID ${id} not found.` });
|
|
2482
|
+
}
|
|
2483
|
+
break;
|
|
2484
|
+
}
|
|
2485
|
+
if (subcommand === 'clear') {
|
|
2486
|
+
const initialSchedules = scheduleService.listSchedules();
|
|
2487
|
+
const count = initialSchedules.length;
|
|
2488
|
+
if (count === 0) {
|
|
2489
|
+
await interaction.editReply({ content: '📅 No scheduled tasks found to clear.' });
|
|
2490
|
+
break;
|
|
2491
|
+
}
|
|
2492
|
+
const initialIdsJson = JSON.stringify(initialSchedules.map(s => s.id).sort((a, b) => a - b));
|
|
2493
|
+
const confirmBtnId = `schedule_clear_confirm_${interaction.id}`;
|
|
2494
|
+
const cancelBtnId = `schedule_clear_cancel_${interaction.id}`;
|
|
2495
|
+
const row = new discord_js_1.ActionRowBuilder().addComponents(new discord_js_1.ButtonBuilder()
|
|
2496
|
+
.setCustomId(confirmBtnId)
|
|
2497
|
+
.setLabel(`${(0, i18n_1.t)('Confirm Clear')} (${count})`)
|
|
2498
|
+
.setStyle(discord_js_1.ButtonStyle.Danger), new discord_js_1.ButtonBuilder()
|
|
2499
|
+
.setCustomId(cancelBtnId)
|
|
2500
|
+
.setLabel((0, i18n_1.t)('Cancel'))
|
|
2501
|
+
.setStyle(discord_js_1.ButtonStyle.Secondary));
|
|
2502
|
+
const message = await interaction.editReply({
|
|
2503
|
+
content: `⚠️ **Warning**: This will delete all **${count}** scheduled task(s) and reset the ID counter to 0. Are you sure you want to proceed?`,
|
|
2504
|
+
components: [row]
|
|
2505
|
+
});
|
|
2506
|
+
const collector = message.createMessageComponentCollector({
|
|
2507
|
+
filter: (i) => i.user.id === interaction.user.id && (i.customId === confirmBtnId || i.customId === cancelBtnId),
|
|
2508
|
+
time: 30000,
|
|
2509
|
+
max: 1
|
|
2510
|
+
});
|
|
2511
|
+
collector.on('collect', async (i) => {
|
|
2512
|
+
if (i.customId === confirmBtnId) {
|
|
2513
|
+
const currentSchedules = scheduleService.listSchedules();
|
|
2514
|
+
const currentIdsJson = JSON.stringify(currentSchedules.map(s => s.id).sort((a, b) => a - b));
|
|
2515
|
+
if (initialIdsJson !== currentIdsJson) {
|
|
2516
|
+
await i.update({
|
|
2517
|
+
content: '⚠️ **Action aborted**: The scheduled tasks list changed while waiting for confirmation. No schedules were cleared.',
|
|
2518
|
+
components: []
|
|
2519
|
+
});
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
scheduleService.resetSchedules();
|
|
2523
|
+
await i.update({
|
|
2524
|
+
content: `✅ Successfully removed all **${count}** scheduled task(s) and reset the task ID counter to 0.`,
|
|
2525
|
+
components: []
|
|
2526
|
+
});
|
|
2527
|
+
}
|
|
2528
|
+
else {
|
|
2529
|
+
await i.update({
|
|
2530
|
+
content: '❌ Action cancelled. Scheduled tasks were not cleared.',
|
|
2531
|
+
components: []
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
});
|
|
2535
|
+
collector.on('end', async (collected) => {
|
|
2536
|
+
if (collected.size === 0) {
|
|
2537
|
+
await interaction.editReply({
|
|
2538
|
+
content: '⚠️ Action timed out. Scheduled tasks were not cleared.',
|
|
2539
|
+
components: []
|
|
2540
|
+
}).catch(() => { });
|
|
2541
|
+
}
|
|
2542
|
+
});
|
|
2543
|
+
break;
|
|
2544
|
+
}
|
|
2545
|
+
if (subcommand === 'backup') {
|
|
2546
|
+
const json = scheduleService.backupSchedules();
|
|
2547
|
+
const buffer = Buffer.from(json, 'utf-8');
|
|
2548
|
+
await interaction.editReply({
|
|
2549
|
+
content: '📋 **LazyGravity Schedules Backup**',
|
|
2550
|
+
files: [{
|
|
2551
|
+
attachment: buffer,
|
|
2552
|
+
name: 'schedules_backup.json'
|
|
2553
|
+
}]
|
|
2554
|
+
});
|
|
2555
|
+
break;
|
|
2556
|
+
}
|
|
2557
|
+
if (subcommand === 'restore') {
|
|
2558
|
+
const attachment = interaction.options.getAttachment('file', true);
|
|
2559
|
+
if (!attachment.name.endsWith('.json')) {
|
|
2560
|
+
await interaction.editReply({ content: '❌ Attachment must be a `.json` file.' });
|
|
2561
|
+
break;
|
|
2562
|
+
}
|
|
2563
|
+
try {
|
|
2564
|
+
// Download file content using global fetch (available in Node 18+)
|
|
2565
|
+
const response = await fetch(attachment.url);
|
|
2566
|
+
if (!response.ok)
|
|
2567
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
2568
|
+
const jsonText = await response.text();
|
|
2569
|
+
const jobCb = scheduleService.getJobCallback();
|
|
2570
|
+
if (!jobCb) {
|
|
2571
|
+
await interaction.editReply({ content: '⚠️ Schedule service is still initializing. Please try again in a few seconds.' });
|
|
2572
|
+
break;
|
|
2573
|
+
}
|
|
2574
|
+
const restoredCount = scheduleService.restoreSchedules(jsonText, jobCb);
|
|
2575
|
+
await interaction.editReply({ content: `✅ Successfully restored ${restoredCount} scheduled tasks from backup!` });
|
|
2576
|
+
}
|
|
2577
|
+
catch (error) {
|
|
2578
|
+
await interaction.editReply({ content: `❌ Failed to restore schedules: ${error.message}` });
|
|
2579
|
+
}
|
|
2580
|
+
break;
|
|
2581
|
+
}
|
|
2582
|
+
break;
|
|
2583
|
+
}
|
|
2082
2584
|
default:
|
|
2083
2585
|
await interaction.editReply({
|
|
2084
2586
|
content: `Unknown command: /${commandName}`,
|