@link-assistant/hive-mind 0.42.1 โ 0.42.3
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/CHANGELOG.md +32 -0
- package/package.json +1 -1
- package/src/hive.mjs +23 -23
- package/src/telegram-bot.mjs +28 -36
- package/src/telegram-top-command.lib.mjs +325 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 0.42.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 64d6cf8: Add experimental /top command to Telegram bot
|
|
8
|
+
|
|
9
|
+
- Added /top command to show live system monitor in Telegram
|
|
10
|
+
- Displays auto-updating `top` output in a single message (updates every 2 seconds)
|
|
11
|
+
- Owner-only access with chat authorization checks
|
|
12
|
+
- Session isolation per chat using GNU screen
|
|
13
|
+
- Clean stop button to terminate monitoring session
|
|
14
|
+
- Marked as EXPERIMENTAL feature with user warnings
|
|
15
|
+
- Not documented in /help as requested
|
|
16
|
+
- Requires GNU screen to be installed on the system
|
|
17
|
+
|
|
18
|
+
Fixes #500
|
|
19
|
+
|
|
20
|
+
## 0.42.2
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- dca5bed: Make --auto-continue enabled by default
|
|
25
|
+
|
|
26
|
+
- Changed default value from false to true for --auto-continue in both hive and solve commands
|
|
27
|
+
- Smart handling of -s (--skip-issues-with-prs) flag interaction:
|
|
28
|
+
- When -s is used, auto-continue is automatically disabled to avoid conflicts
|
|
29
|
+
- Explicit --auto-continue with -s shows proper error message
|
|
30
|
+
- Users can still use --no-auto-continue to explicitly disable
|
|
31
|
+
- This improves user experience as users typically want to continue working on existing PRs
|
|
32
|
+
|
|
33
|
+
Fixes #454
|
|
34
|
+
|
|
3
35
|
## 0.42.1
|
|
4
36
|
|
|
5
37
|
### Patch Changes
|
package/package.json
CHANGED
package/src/hive.mjs
CHANGED
|
@@ -22,11 +22,9 @@ if (earlyArgs.includes('--help') || earlyArgs.includes('-h')) {
|
|
|
22
22
|
const yargs = yargsModule.default || yargsModule;
|
|
23
23
|
const { hideBin } = await use('yargs@17.7.2/helpers');
|
|
24
24
|
const rawArgs = hideBin(process.argv);
|
|
25
|
-
|
|
26
25
|
// Reuse createYargsConfig from shared module to avoid duplication
|
|
27
26
|
const { createYargsConfig } = await import('./hive.config.lib.mjs');
|
|
28
27
|
const helpYargs = createYargsConfig(yargs(rawArgs)).version(false);
|
|
29
|
-
|
|
30
28
|
// Show help and exit
|
|
31
29
|
helpYargs.showHelp();
|
|
32
30
|
process.exit(0);
|
|
@@ -48,7 +46,6 @@ export { createYargsConfig } from './hive.config.lib.mjs';
|
|
|
48
46
|
import { fileURLToPath } from 'url';
|
|
49
47
|
const isDirectExecution = process.argv[1] === fileURLToPath(import.meta.url) ||
|
|
50
48
|
(process.argv[1] && (process.argv[1].includes('/hive') || process.argv[1].endsWith('hive')));
|
|
51
|
-
|
|
52
49
|
if (isDirectExecution) {
|
|
53
50
|
console.log('๐ Hive Mind - AI-powered issue solver');
|
|
54
51
|
console.log(' Initializing...');
|
|
@@ -63,7 +60,6 @@ const withTimeout = (promise, timeoutMs, operation) => {
|
|
|
63
60
|
)
|
|
64
61
|
]);
|
|
65
62
|
};
|
|
66
|
-
|
|
67
63
|
// Use use-m to dynamically import modules for cross-runtime compatibility
|
|
68
64
|
if (typeof use === 'undefined') {
|
|
69
65
|
try {
|
|
@@ -157,7 +153,6 @@ async function fetchIssuesFromRepositories(owner, scope, monitorTag, fetchAllIss
|
|
|
157
153
|
return graphqlResult.issues;
|
|
158
154
|
}
|
|
159
155
|
}
|
|
160
|
-
|
|
161
156
|
// Strategy 2: Fallback to gh api --paginate approach (comprehensive but slower)
|
|
162
157
|
await log(' ๐ Using gh api --paginate approach for comprehensive coverage...', { verbose: true });
|
|
163
158
|
|
|
@@ -170,18 +165,15 @@ async function fetchIssuesFromRepositories(owner, scope, monitorTag, fetchAllIss
|
|
|
170
165
|
} else {
|
|
171
166
|
repoListCmd = `gh api users/${owner}/repos --paginate --jq '.[] | {name: .name, owner: .owner.login, isArchived: .archived}'`;
|
|
172
167
|
}
|
|
173
|
-
|
|
174
168
|
await log(' ๐ Fetching repository list (using --paginate for unlimited pagination)...', { verbose: true });
|
|
175
169
|
await log(` ๐ Command: ${repoListCmd}`, { verbose: true });
|
|
176
170
|
|
|
177
171
|
// Add delay for rate limiting
|
|
178
172
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
179
|
-
|
|
180
173
|
const { stdout: repoOutput } = await execAsync(repoListCmd, { encoding: 'utf8', env: process.env });
|
|
181
174
|
// Parse the output line by line, as gh api with --jq outputs one JSON object per line
|
|
182
175
|
const repoLines = repoOutput.trim().split('\n').filter(line => line.trim());
|
|
183
176
|
const allRepositories = repoLines.map(line => JSON.parse(line));
|
|
184
|
-
|
|
185
177
|
await log(` ๐ Found ${allRepositories.length} repositories`);
|
|
186
178
|
|
|
187
179
|
// Filter repositories to only include those owned by the target user/org
|
|
@@ -190,30 +182,24 @@ async function fetchIssuesFromRepositories(owner, scope, monitorTag, fetchAllIss
|
|
|
190
182
|
return repoOwner === owner;
|
|
191
183
|
});
|
|
192
184
|
const unownedCount = allRepositories.length - ownedRepositories.length;
|
|
193
|
-
|
|
194
185
|
if (unownedCount > 0) {
|
|
195
186
|
await log(` โญ๏ธ Skipping ${unownedCount} repository(ies) not owned by ${owner}`);
|
|
196
187
|
}
|
|
197
|
-
|
|
198
188
|
// Filter out archived repositories from owned repositories
|
|
199
189
|
const repositories = ownedRepositories.filter(repo => !repo.isArchived);
|
|
200
190
|
const archivedCount = ownedRepositories.length - repositories.length;
|
|
201
|
-
|
|
202
191
|
if (archivedCount > 0) {
|
|
203
192
|
await log(` โญ๏ธ Skipping ${archivedCount} archived repository(ies)`);
|
|
204
193
|
}
|
|
205
|
-
|
|
206
194
|
await log(` โ
Processing ${repositories.length} non-archived repositories owned by ${owner}`);
|
|
207
195
|
|
|
208
196
|
let collectedIssues = [];
|
|
209
197
|
let processedRepos = 0;
|
|
210
|
-
|
|
211
198
|
// Process repositories in batches to avoid overwhelming the API
|
|
212
199
|
for (const repo of repositories) {
|
|
213
200
|
try {
|
|
214
201
|
const repoName = repo.name;
|
|
215
202
|
const ownerName = repo.owner?.login || owner;
|
|
216
|
-
|
|
217
203
|
await log(` ๐ Fetching issues from ${ownerName}/${repoName}...`, { verbose: true });
|
|
218
204
|
|
|
219
205
|
// Build the appropriate issue list command
|
|
@@ -223,7 +209,6 @@ async function fetchIssuesFromRepositories(owner, scope, monitorTag, fetchAllIss
|
|
|
223
209
|
} else {
|
|
224
210
|
issueCmd = `gh issue list --repo ${ownerName}/${repoName} --state open --label "${monitorTag}" --json url,title,number,createdAt`;
|
|
225
211
|
}
|
|
226
|
-
|
|
227
212
|
// Add delay between repository requests
|
|
228
213
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
229
214
|
|
|
@@ -488,13 +473,26 @@ if (argv.projectMode) {
|
|
|
488
473
|
const tool = argv.tool || 'claude';
|
|
489
474
|
await validateAndExitOnInvalidModel(argv.model, tool, safeExit);
|
|
490
475
|
|
|
491
|
-
//
|
|
492
|
-
if
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
476
|
+
// Handle -s (--skip-issues-with-prs) and --auto-continue interaction
|
|
477
|
+
// Detect if user explicitly passed --auto-continue or --no-auto-continue
|
|
478
|
+
const hasExplicitAutoContinue = rawArgs.includes('--auto-continue');
|
|
479
|
+
const hasExplicitNoAutoContinue = rawArgs.includes('--no-auto-continue');
|
|
480
|
+
|
|
481
|
+
if (argv.skipIssuesWithPrs) {
|
|
482
|
+
// If user explicitly passed --auto-continue with -s, that's a conflict
|
|
483
|
+
if (hasExplicitAutoContinue) {
|
|
484
|
+
await log('โ Conflicting options: --skip-issues-with-prs and --auto-continue cannot be used together', { level: 'error' });
|
|
485
|
+
await log(' --skip-issues-with-prs: Skips issues that have any open PRs', { level: 'error' });
|
|
486
|
+
await log(' --auto-continue: Continues with existing PRs instead of creating new ones', { level: 'error' });
|
|
487
|
+
await log(` ๐ Full log file: ${absoluteLogPath}`, { level: 'error' });
|
|
488
|
+
await safeExit(1, 'Error occurred');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// If user didn't explicitly set auto-continue, disable it when -s is used
|
|
492
|
+
// This is because -s means "skip issues with PRs" which conflicts with auto-continue
|
|
493
|
+
if (!hasExplicitNoAutoContinue) {
|
|
494
|
+
argv.autoContinue = false;
|
|
495
|
+
}
|
|
498
496
|
}
|
|
499
497
|
|
|
500
498
|
// Helper function to check GitHub permissions - moved to github.lib.mjs
|
|
@@ -751,7 +749,7 @@ async function worker(workerId) {
|
|
|
751
749
|
const dryRunFlag = argv.dryRun ? ' --dry-run' : '';
|
|
752
750
|
const skipToolConnectionCheckFlag = (argv.skipToolConnectionCheck || argv.toolConnectionCheck === false) ? ' --skip-tool-connection-check' : '';
|
|
753
751
|
const toolFlag = argv.tool ? ` --tool ${argv.tool}` : '';
|
|
754
|
-
const autoContinueFlag = argv.autoContinue ? ' --auto-continue' : '';
|
|
752
|
+
const autoContinueFlag = argv.autoContinue ? ' --auto-continue' : ' --no-auto-continue';
|
|
755
753
|
const thinkFlag = argv.think ? ` --think ${argv.think}` : '';
|
|
756
754
|
const promptPlanSubAgentFlag = argv.promptPlanSubAgent ? ' --prompt-plan-sub-agent' : '';
|
|
757
755
|
const noSentryFlag = !argv.sentry ? ' --no-sentry' : '';
|
|
@@ -794,6 +792,8 @@ async function worker(workerId) {
|
|
|
794
792
|
}
|
|
795
793
|
if (argv.autoContinue) {
|
|
796
794
|
args.push('--auto-continue');
|
|
795
|
+
} else {
|
|
796
|
+
args.push('--no-auto-continue');
|
|
797
797
|
}
|
|
798
798
|
if (argv.think) {
|
|
799
799
|
args.push('--think', argv.think);
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -1225,45 +1225,37 @@ bot.command(/^hive$/i, async (ctx) => {
|
|
|
1225
1225
|
}
|
|
1226
1226
|
});
|
|
1227
1227
|
|
|
1228
|
+
// Register /top command from separate module
|
|
1229
|
+
// This keeps telegram-bot.mjs under the 1500 line limit
|
|
1230
|
+
const { registerTopCommand } = await import('./telegram-top-command.lib.mjs');
|
|
1231
|
+
registerTopCommand(bot, {
|
|
1232
|
+
VERBOSE,
|
|
1233
|
+
isOldMessage,
|
|
1234
|
+
isForwardedOrReply,
|
|
1235
|
+
isGroupChat,
|
|
1236
|
+
isChatAuthorized
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1228
1239
|
// Add message listener for verbose debugging
|
|
1229
|
-
// This helps diagnose if bot is receiving messages at all
|
|
1230
1240
|
if (VERBOSE) {
|
|
1231
1241
|
bot.on('message', (ctx, next) => {
|
|
1232
|
-
console.log('[VERBOSE] Message received:');
|
|
1233
|
-
console.log('[VERBOSE] Chat ID:', ctx.chat?.id);
|
|
1234
|
-
console.log('[VERBOSE] Chat type:', ctx.chat?.type);
|
|
1235
|
-
console.log('[VERBOSE] Is forum:', ctx.chat?.is_forum);
|
|
1236
|
-
console.log('[VERBOSE] Is topic message:', ctx.message?.is_topic_message);
|
|
1237
|
-
console.log('[VERBOSE] Message thread ID:', ctx.message?.message_thread_id);
|
|
1238
|
-
console.log('[VERBOSE] Message date:', ctx.message?.date);
|
|
1239
|
-
console.log('[VERBOSE] Message text:', ctx.message?.text?.substring(0, 100));
|
|
1240
|
-
console.log('[VERBOSE] From user:', ctx.from?.username || ctx.from?.id);
|
|
1241
|
-
console.log('[VERBOSE] Bot start time:', BOT_START_TIME);
|
|
1242
|
-
console.log('[VERBOSE] Is old message:', isOldMessage(ctx));
|
|
1243
|
-
|
|
1244
|
-
// Detailed forwarding/reply detection debug info
|
|
1245
1242
|
const msg = ctx.message;
|
|
1246
|
-
|
|
1247
|
-
|
|
1243
|
+
console.log('[VERBOSE] Message:', {
|
|
1244
|
+
chatId: ctx.chat?.id, chatType: ctx.chat?.type, isForum: ctx.chat?.is_forum,
|
|
1245
|
+
isTopicMsg: msg?.is_topic_message, threadId: msg?.message_thread_id, date: msg?.date,
|
|
1246
|
+
text: msg?.text?.substring(0, 100), user: ctx.from?.username || ctx.from?.id,
|
|
1247
|
+
botStartTime: BOT_START_TIME, isOld: isOldMessage(ctx), isForwarded: isForwardedOrReply(ctx),
|
|
1248
|
+
isAuthorized: isChatAuthorized(ctx.chat?.id)
|
|
1249
|
+
});
|
|
1248
1250
|
if (msg) {
|
|
1249
|
-
|
|
1250
|
-
console.log('[VERBOSE]
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
console.log('[VERBOSE] - forward_from_chat:', JSON.stringify(msg.forward_from_chat));
|
|
1257
|
-
console.log('[VERBOSE] - forward_date:', msg.forward_date);
|
|
1258
|
-
console.log('[VERBOSE] - reply_to_message:', JSON.stringify(msg.reply_to_message));
|
|
1259
|
-
console.log('[VERBOSE] - reply_to_message type:', typeof msg.reply_to_message);
|
|
1260
|
-
console.log('[VERBOSE] - reply_to_message truthy?:', !!msg.reply_to_message);
|
|
1261
|
-
console.log('[VERBOSE] - reply_to_message.message_id:', msg.reply_to_message?.message_id);
|
|
1262
|
-
console.log('[VERBOSE] - reply_to_message.forum_topic_created:', JSON.stringify(msg.reply_to_message?.forum_topic_created));
|
|
1251
|
+
console.log('[VERBOSE] Msg fields:', Object.keys(msg));
|
|
1252
|
+
console.log('[VERBOSE] Forward/reply:', {
|
|
1253
|
+
forward_origin: msg.forward_origin, forward_from: msg.forward_from,
|
|
1254
|
+
forward_from_chat: msg.forward_from_chat, forward_date: msg.forward_date,
|
|
1255
|
+
reply_to_message: msg.reply_to_message, reply_id: msg.reply_to_message?.message_id,
|
|
1256
|
+
forum_topic_created: msg.reply_to_message?.forum_topic_created
|
|
1257
|
+
});
|
|
1263
1258
|
}
|
|
1264
|
-
|
|
1265
|
-
console.log('[VERBOSE] Is authorized:', isChatAuthorized(ctx.chat?.id));
|
|
1266
|
-
// Continue to next handler
|
|
1267
1259
|
return next();
|
|
1268
1260
|
});
|
|
1269
1261
|
}
|
|
@@ -1391,9 +1383,9 @@ bot.telegram.deleteWebhook({ drop_pending_updates: true })
|
|
|
1391
1383
|
});
|
|
1392
1384
|
}
|
|
1393
1385
|
return bot.launch({
|
|
1394
|
-
//
|
|
1395
|
-
// This ensures the bot receives all message types including commands
|
|
1396
|
-
allowedUpdates: ['message'],
|
|
1386
|
+
// Receive message updates (commands, text messages) and callback queries (button clicks)
|
|
1387
|
+
// This ensures the bot receives all message types including commands and button interactions
|
|
1388
|
+
allowedUpdates: ['message', 'callback_query'],
|
|
1397
1389
|
// Drop any pending updates that were sent before the bot started
|
|
1398
1390
|
// This ensures we only process new messages sent after this bot instance started
|
|
1399
1391
|
dropPendingUpdates: true
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram /top command implementation
|
|
3
|
+
*
|
|
4
|
+
* This module provides the /top command functionality for the Telegram bot,
|
|
5
|
+
* allowing chat owners to view live system monitor output in an auto-updating message.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Live system monitoring using GNU screen and top command
|
|
9
|
+
* - Auto-updates every 2 seconds
|
|
10
|
+
* - Owner-only access control
|
|
11
|
+
* - Session management per chat
|
|
12
|
+
* - Clean cleanup on stop
|
|
13
|
+
*
|
|
14
|
+
* @experimental This feature is marked as experimental
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { promisify } from 'util';
|
|
18
|
+
import { exec as execCallback } from 'child_process';
|
|
19
|
+
|
|
20
|
+
const exec = promisify(execCallback);
|
|
21
|
+
|
|
22
|
+
// Store active top sessions: Map<chatId, { messageId, screenName, intervalId }>
|
|
23
|
+
const activeTopSessions = new Map();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Captures top output from the file for a given chat
|
|
27
|
+
* @param {number} chatId - The chat ID
|
|
28
|
+
* @returns {Promise<string|null>} The formatted top output or null on error
|
|
29
|
+
*/
|
|
30
|
+
async function captureTopOutput(chatId) {
|
|
31
|
+
try {
|
|
32
|
+
const outputFile = `/tmp/top-output-${chatId}.txt`;
|
|
33
|
+
const { readFile } = await import('fs/promises');
|
|
34
|
+
const output = await readFile(outputFile, 'utf-8');
|
|
35
|
+
|
|
36
|
+
// Format output for Telegram (limit to first 30 lines to fit in message)
|
|
37
|
+
const lines = output.split('\n').slice(0, 30);
|
|
38
|
+
return lines.join('\n');
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('[ERROR] Failed to capture top output:', error);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Registers the /top command handler with the bot
|
|
47
|
+
* @param {Object} bot - The Telegraf bot instance
|
|
48
|
+
* @param {Object} options - Options object
|
|
49
|
+
* @param {boolean} options.VERBOSE - Whether to enable verbose logging
|
|
50
|
+
* @param {Function} options.isOldMessage - Function to check if message is old
|
|
51
|
+
* @param {Function} options.isForwardedOrReply - Function to check if message is forwarded/reply
|
|
52
|
+
* @param {Function} options.isGroupChat - Function to check if chat is a group
|
|
53
|
+
* @param {Function} options.isChatAuthorized - Function to check if chat is authorized
|
|
54
|
+
*/
|
|
55
|
+
export function registerTopCommand(bot, options) {
|
|
56
|
+
const {
|
|
57
|
+
VERBOSE = false,
|
|
58
|
+
isOldMessage,
|
|
59
|
+
isForwardedOrReply,
|
|
60
|
+
isGroupChat,
|
|
61
|
+
isChatAuthorized
|
|
62
|
+
} = options;
|
|
63
|
+
|
|
64
|
+
// /top command - show system top output in an auto-updating message (EXPERIMENTAL)
|
|
65
|
+
// Only accessible by chat owner
|
|
66
|
+
// Not documented in /help as requested in issue #500
|
|
67
|
+
bot.command('top', async (ctx) => {
|
|
68
|
+
if (VERBOSE) {
|
|
69
|
+
console.log('[VERBOSE] /top command received');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Ignore messages sent before bot started
|
|
73
|
+
if (isOldMessage(ctx)) {
|
|
74
|
+
if (VERBOSE) {
|
|
75
|
+
console.log('[VERBOSE] /top ignored: old message');
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Ignore forwarded or reply messages
|
|
81
|
+
if (isForwardedOrReply(ctx)) {
|
|
82
|
+
if (VERBOSE) {
|
|
83
|
+
console.log('[VERBOSE] /top ignored: forwarded or reply');
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!isGroupChat(ctx)) {
|
|
89
|
+
if (VERBOSE) {
|
|
90
|
+
console.log('[VERBOSE] /top ignored: not a group chat');
|
|
91
|
+
}
|
|
92
|
+
await ctx.reply('โ The /top command only works in group chats.', { reply_to_message_id: ctx.message.message_id });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const chatId = ctx.chat.id;
|
|
97
|
+
if (!isChatAuthorized(chatId)) {
|
|
98
|
+
if (VERBOSE) {
|
|
99
|
+
console.log('[VERBOSE] /top ignored: chat not authorized');
|
|
100
|
+
}
|
|
101
|
+
await ctx.reply(`โ This chat (ID: ${chatId}) is not authorized to use this bot.`, { reply_to_message_id: ctx.message.message_id });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check if user is chat owner
|
|
106
|
+
try {
|
|
107
|
+
const chatMember = await ctx.telegram.getChatMember(chatId, ctx.from.id);
|
|
108
|
+
if (chatMember.status !== 'creator') {
|
|
109
|
+
if (VERBOSE) {
|
|
110
|
+
console.log('[VERBOSE] /top ignored: user is not chat owner');
|
|
111
|
+
}
|
|
112
|
+
await ctx.reply('โ This command is only available to the chat owner.', { reply_to_message_id: ctx.message.message_id });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error('[ERROR] Failed to check chat member status:', error);
|
|
117
|
+
await ctx.reply('โ Failed to verify permissions.', { reply_to_message_id: ctx.message.message_id });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (VERBOSE) {
|
|
122
|
+
console.log('[VERBOSE] /top passed all checks, starting...');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Show experimental feature warning
|
|
126
|
+
await ctx.reply('๐งช *EXPERIMENTAL FEATURE*\n\nThis command is experimental and may have issues. Use with caution.', {
|
|
127
|
+
parse_mode: 'Markdown',
|
|
128
|
+
reply_to_message_id: ctx.message.message_id
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Check if there's already an active top session for this chat
|
|
132
|
+
if (activeTopSessions.has(chatId)) {
|
|
133
|
+
await ctx.reply('โ A top session is already running for this chat. Stop it first using the button.', { reply_to_message_id: ctx.message.message_id });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Generate screen session name with chat ID
|
|
138
|
+
const screenName = `top-chat-${chatId}`;
|
|
139
|
+
|
|
140
|
+
// Check if screen session already exists
|
|
141
|
+
let sessionExists = false;
|
|
142
|
+
try {
|
|
143
|
+
const { stdout } = await exec('screen -ls');
|
|
144
|
+
sessionExists = stdout.includes(screenName);
|
|
145
|
+
} catch {
|
|
146
|
+
// screen -ls returns non-zero when no sessions exist
|
|
147
|
+
sessionExists = false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Create screen session if it doesn't exist
|
|
151
|
+
// We'll use a different approach: run top in batch mode with output redirected to a file
|
|
152
|
+
// that we continuously read instead of using screen hardcopy
|
|
153
|
+
const outputFile = `/tmp/top-output-${chatId}.txt`;
|
|
154
|
+
|
|
155
|
+
if (!sessionExists) {
|
|
156
|
+
try {
|
|
157
|
+
// Start top in a screen session with batch mode, outputting to a file
|
|
158
|
+
// -b: batch mode, -d 2: 2 second delay between updates, -n: number of iterations (unlimited)
|
|
159
|
+
await exec(`screen -dmS ${screenName} bash -c 'while true; do top -b -n 1 > ${outputFile}; sleep 2; done'`);
|
|
160
|
+
if (VERBOSE) {
|
|
161
|
+
console.log(`[VERBOSE] Created screen session: ${screenName}`);
|
|
162
|
+
}
|
|
163
|
+
// Give top a moment to start and produce first output
|
|
164
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error('[ERROR] Failed to create screen session:', error);
|
|
167
|
+
await ctx.reply('โ Failed to start top command.', { reply_to_message_id: ctx.message.message_id });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Send initial message with loading indicator
|
|
173
|
+
const initialMessage = await ctx.reply('๐งช ๐ Loading system monitor... (EXPERIMENTAL)', {
|
|
174
|
+
reply_to_message_id: ctx.message.message_id,
|
|
175
|
+
reply_markup: {
|
|
176
|
+
inline_keyboard: [[
|
|
177
|
+
{ text: '๐ Stop', callback_data: `stop_top_${chatId}` }
|
|
178
|
+
]]
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Capture and display first output
|
|
183
|
+
const firstOutput = await captureTopOutput(chatId);
|
|
184
|
+
if (firstOutput) {
|
|
185
|
+
try {
|
|
186
|
+
await ctx.telegram.editMessageText(
|
|
187
|
+
chatId,
|
|
188
|
+
initialMessage.message_id,
|
|
189
|
+
undefined,
|
|
190
|
+
`\`\`\`\n${firstOutput}\n\`\`\``,
|
|
191
|
+
{
|
|
192
|
+
parse_mode: 'Markdown',
|
|
193
|
+
reply_markup: {
|
|
194
|
+
inline_keyboard: [[
|
|
195
|
+
{ text: '๐ Stop', callback_data: `stop_top_${chatId}` }
|
|
196
|
+
]]
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.error('[ERROR] Failed to update message:', error);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Set up periodic update (every 2 seconds)
|
|
206
|
+
const intervalId = setInterval(async () => {
|
|
207
|
+
const output = await captureTopOutput(chatId);
|
|
208
|
+
if (output) {
|
|
209
|
+
try {
|
|
210
|
+
await ctx.telegram.editMessageText(
|
|
211
|
+
chatId,
|
|
212
|
+
initialMessage.message_id,
|
|
213
|
+
undefined,
|
|
214
|
+
`\`\`\`\n${output}\n\`\`\``,
|
|
215
|
+
{
|
|
216
|
+
parse_mode: 'Markdown',
|
|
217
|
+
reply_markup: {
|
|
218
|
+
inline_keyboard: [[
|
|
219
|
+
{ text: '๐ Stop', callback_data: `stop_top_${chatId}` }
|
|
220
|
+
]]
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
// Ignore "message is not modified" errors
|
|
226
|
+
if (!error.message?.includes('message is not modified')) {
|
|
227
|
+
console.error('[ERROR] Failed to update message:', error);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}, 2000);
|
|
232
|
+
|
|
233
|
+
// Store session info
|
|
234
|
+
activeTopSessions.set(chatId, {
|
|
235
|
+
messageId: initialMessage.message_id,
|
|
236
|
+
screenName,
|
|
237
|
+
intervalId
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (VERBOSE) {
|
|
241
|
+
console.log(`[VERBOSE] Top session started for chat ${chatId}`);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Handle stop button callback
|
|
246
|
+
bot.action(/^stop_top_(.+)$/, async (ctx) => {
|
|
247
|
+
const chatId = parseInt(ctx.match[1]);
|
|
248
|
+
|
|
249
|
+
if (VERBOSE) {
|
|
250
|
+
console.log(`[VERBOSE] Stop top callback received for chat ${chatId}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Check if user is chat owner
|
|
254
|
+
try {
|
|
255
|
+
const chatMember = await ctx.telegram.getChatMember(chatId, ctx.from.id);
|
|
256
|
+
if (chatMember.status !== 'creator') {
|
|
257
|
+
await ctx.answerCbQuery('โ Only the chat owner can stop the top session.');
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
} catch (error) {
|
|
261
|
+
console.error('[ERROR] Failed to check chat member status:', error);
|
|
262
|
+
await ctx.answerCbQuery('โ Failed to verify permissions.');
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const session = activeTopSessions.get(chatId);
|
|
267
|
+
if (!session) {
|
|
268
|
+
await ctx.answerCbQuery('โ No active top session found.');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Stop the update interval
|
|
273
|
+
clearInterval(session.intervalId);
|
|
274
|
+
|
|
275
|
+
// Kill the screen session
|
|
276
|
+
try {
|
|
277
|
+
await exec(`screen -S ${session.screenName} -X quit`);
|
|
278
|
+
if (VERBOSE) {
|
|
279
|
+
console.log(`[VERBOSE] Killed screen session: ${session.screenName}`);
|
|
280
|
+
}
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.error('[ERROR] Failed to kill screen session:', error);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Clean up the output file
|
|
286
|
+
try {
|
|
287
|
+
const { unlink } = await import('fs/promises');
|
|
288
|
+
await unlink(`/tmp/top-output-${chatId}.txt`);
|
|
289
|
+
if (VERBOSE) {
|
|
290
|
+
console.log(`[VERBOSE] Cleaned up output file for chat ${chatId}`);
|
|
291
|
+
}
|
|
292
|
+
} catch (error) {
|
|
293
|
+
// Ignore file cleanup errors
|
|
294
|
+
if (VERBOSE) {
|
|
295
|
+
console.log(`[VERBOSE] Could not clean up output file: ${error.message}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Remove from active sessions
|
|
300
|
+
activeTopSessions.delete(chatId);
|
|
301
|
+
|
|
302
|
+
// Update the message to show it's stopped
|
|
303
|
+
try {
|
|
304
|
+
await ctx.editMessageText('๐ Top session stopped.', {
|
|
305
|
+
parse_mode: 'Markdown'
|
|
306
|
+
});
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.error('[ERROR] Failed to edit message:', error);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
await ctx.answerCbQuery('โ
Top session stopped successfully.');
|
|
312
|
+
|
|
313
|
+
if (VERBOSE) {
|
|
314
|
+
console.log(`[VERBOSE] Top session stopped for chat ${chatId}`);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Gets information about active top sessions
|
|
321
|
+
* @returns {Map} Map of active sessions
|
|
322
|
+
*/
|
|
323
|
+
export function getActiveTopSessions() {
|
|
324
|
+
return activeTopSessions;
|
|
325
|
+
}
|