@link-assistant/hive-mind 0.39.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +24 -0
  3. package/README.md +769 -0
  4. package/package.json +58 -0
  5. package/src/agent.lib.mjs +705 -0
  6. package/src/agent.prompts.lib.mjs +196 -0
  7. package/src/buildUserMention.lib.mjs +71 -0
  8. package/src/claude-limits.lib.mjs +389 -0
  9. package/src/claude.lib.mjs +1445 -0
  10. package/src/claude.prompts.lib.mjs +203 -0
  11. package/src/codex.lib.mjs +552 -0
  12. package/src/codex.prompts.lib.mjs +194 -0
  13. package/src/config.lib.mjs +207 -0
  14. package/src/contributing-guidelines.lib.mjs +268 -0
  15. package/src/exit-handler.lib.mjs +205 -0
  16. package/src/git.lib.mjs +145 -0
  17. package/src/github-issue-creator.lib.mjs +246 -0
  18. package/src/github-linking.lib.mjs +152 -0
  19. package/src/github.batch.lib.mjs +272 -0
  20. package/src/github.graphql.lib.mjs +258 -0
  21. package/src/github.lib.mjs +1479 -0
  22. package/src/hive.config.lib.mjs +254 -0
  23. package/src/hive.mjs +1500 -0
  24. package/src/instrument.mjs +191 -0
  25. package/src/interactive-mode.lib.mjs +1000 -0
  26. package/src/lenv-reader.lib.mjs +206 -0
  27. package/src/lib.mjs +490 -0
  28. package/src/lino.lib.mjs +176 -0
  29. package/src/local-ci-checks.lib.mjs +324 -0
  30. package/src/memory-check.mjs +419 -0
  31. package/src/model-mapping.lib.mjs +145 -0
  32. package/src/model-validation.lib.mjs +278 -0
  33. package/src/opencode.lib.mjs +479 -0
  34. package/src/opencode.prompts.lib.mjs +194 -0
  35. package/src/protect-branch.mjs +159 -0
  36. package/src/review.mjs +433 -0
  37. package/src/reviewers-hive.mjs +643 -0
  38. package/src/sentry.lib.mjs +284 -0
  39. package/src/solve.auto-continue.lib.mjs +568 -0
  40. package/src/solve.auto-pr.lib.mjs +1374 -0
  41. package/src/solve.branch-errors.lib.mjs +341 -0
  42. package/src/solve.branch.lib.mjs +230 -0
  43. package/src/solve.config.lib.mjs +342 -0
  44. package/src/solve.error-handlers.lib.mjs +256 -0
  45. package/src/solve.execution.lib.mjs +291 -0
  46. package/src/solve.feedback.lib.mjs +436 -0
  47. package/src/solve.mjs +1128 -0
  48. package/src/solve.preparation.lib.mjs +210 -0
  49. package/src/solve.repo-setup.lib.mjs +114 -0
  50. package/src/solve.repository.lib.mjs +961 -0
  51. package/src/solve.results.lib.mjs +558 -0
  52. package/src/solve.session.lib.mjs +135 -0
  53. package/src/solve.validation.lib.mjs +325 -0
  54. package/src/solve.watch.lib.mjs +572 -0
  55. package/src/start-screen.mjs +324 -0
  56. package/src/task.mjs +308 -0
  57. package/src/telegram-bot.mjs +1481 -0
  58. package/src/telegram-markdown.lib.mjs +64 -0
  59. package/src/usage-limit.lib.mjs +218 -0
  60. package/src/version.lib.mjs +41 -0
  61. package/src/youtrack/solve.youtrack.lib.mjs +116 -0
  62. package/src/youtrack/youtrack-sync.mjs +219 -0
  63. package/src/youtrack/youtrack.lib.mjs +425 -0
@@ -0,0 +1,1481 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from 'child_process';
4
+ import { promisify } from 'util';
5
+ import { exec as execCallback } from 'child_process';
6
+
7
+ const exec = promisify(execCallback);
8
+
9
+ if (typeof use === 'undefined') {
10
+ globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
11
+ }
12
+
13
+ const { lino } = await import('./lino.lib.mjs');
14
+ const { buildUserMention } = await import('./buildUserMention.lib.mjs');
15
+ const { reportError, initializeSentry, addBreadcrumb } = await import('./sentry.lib.mjs');
16
+ const { loadLenvConfig } = await import('./lenv-reader.lib.mjs');
17
+
18
+ const dotenvxModule = await use('@dotenvx/dotenvx');
19
+ const dotenvx = dotenvxModule.default || dotenvxModule;
20
+
21
+ const getenv = await use('getenv');
22
+
23
+ // Load .env configuration as base
24
+ dotenvx.config({ quiet: true });
25
+
26
+ // Load .lenv configuration (if exists)
27
+ // .lenv overrides .env
28
+ loadLenvConfig({ override: true, quiet: true });
29
+
30
+ const yargsModule = await use('yargs@17.7.2');
31
+ const yargs = yargsModule.default || yargsModule;
32
+ const { hideBin } = await use('yargs@17.7.2/helpers');
33
+
34
+ // Import solve and hive yargs configurations for validation
35
+ const solveConfigLib = await import('./solve.config.lib.mjs');
36
+ const { createYargsConfig: createSolveYargsConfig } = solveConfigLib;
37
+
38
+ const hiveConfigLib = await import('./hive.config.lib.mjs');
39
+ const { createYargsConfig: createHiveYargsConfig } = hiveConfigLib;
40
+
41
+ // Import GitHub URL parser for extracting URLs from messages
42
+ const { parseGitHubUrl } = await import('./github.lib.mjs');
43
+
44
+ // Import model validation for early validation with helpful error messages
45
+ const { validateModelName } = await import('./model-validation.lib.mjs');
46
+
47
+ // Import Claude limits library for /limits command
48
+ const { getClaudeUsageLimits, formatUsageMessage } = await import('./claude-limits.lib.mjs');
49
+
50
+ // Import Telegram markdown escaping utilities
51
+ const { escapeMarkdown, escapeMarkdownV2 } = await import('./telegram-markdown.lib.mjs');
52
+
53
+ const config = yargs(hideBin(process.argv))
54
+ .usage('Usage: hive-telegram-bot [options]')
55
+ .option('configuration', {
56
+ type: 'string',
57
+ description: 'LINO configuration string for environment variables',
58
+ alias: 'c',
59
+ default: getenv('TELEGRAM_CONFIGURATION', '')
60
+ })
61
+ .option('token', {
62
+ type: 'string',
63
+ description: 'Telegram bot token from @BotFather',
64
+ alias: 't',
65
+ default: getenv('TELEGRAM_BOT_TOKEN', '')
66
+ })
67
+ .option('allowedChats', {
68
+ type: 'string',
69
+ description: 'Allowed chat IDs in lino notation, e.g., "(\n 123456789\n 987654321\n)"',
70
+ alias: 'allowed-chats',
71
+ default: getenv('TELEGRAM_ALLOWED_CHATS', '')
72
+ })
73
+ .option('solveOverrides', {
74
+ type: 'string',
75
+ description: 'Override options for /solve command in lino notation, e.g., "(\n --auto-continue\n --attach-logs\n)"',
76
+ alias: 'solve-overrides',
77
+ default: getenv('TELEGRAM_SOLVE_OVERRIDES', '')
78
+ })
79
+ .option('hiveOverrides', {
80
+ type: 'string',
81
+ description: 'Override options for /hive command in lino notation, e.g., "(\n --verbose\n --all-issues\n)"',
82
+ alias: 'hive-overrides',
83
+ default: getenv('TELEGRAM_HIVE_OVERRIDES', '')
84
+ })
85
+ .option('solve', {
86
+ type: 'boolean',
87
+ description: 'Enable /solve command (use --no-solve to disable)',
88
+ default: getenv('TELEGRAM_SOLVE', 'true') !== 'false'
89
+ })
90
+ .option('hive', {
91
+ type: 'boolean',
92
+ description: 'Enable /hive command (use --no-hive to disable)',
93
+ default: getenv('TELEGRAM_HIVE', 'true') !== 'false'
94
+ })
95
+ .option('dryRun', {
96
+ type: 'boolean',
97
+ description: 'Validate configuration and options without starting the bot',
98
+ alias: 'dry-run',
99
+ default: false
100
+ })
101
+ .option('verbose', {
102
+ type: 'boolean',
103
+ description: 'Enable verbose logging for debugging',
104
+ alias: 'v',
105
+ default: getenv('TELEGRAM_BOT_VERBOSE', 'false') === 'true'
106
+ })
107
+ .help('h')
108
+ .alias('h', 'help')
109
+ .parserConfiguration({
110
+ 'boolean-negation': true,
111
+ 'strip-dashed': true // Remove dashed keys from argv to simplify validation
112
+ })
113
+ .strict() // Enable strict mode to reject unknown options (consistent with solve.mjs and hive.mjs)
114
+ .parse();
115
+
116
+ // Load configuration from --configuration option if provided
117
+ // This allows users to pass environment variables via command line
118
+ //
119
+ // Complete configuration priority order (highest priority last):
120
+ // 1. .env (base configuration, loaded first - already loaded above at line 24)
121
+ // 2. .lenv (overrides .env - already loaded above at line 28)
122
+ // 3. yargs CLI options parsed above (lines 41-102) use getenv() for defaults,
123
+ // which reads from process.env populated by .env and .lenv
124
+ // 4. --configuration option (overrides process.env, affecting getenv() calls below)
125
+ // 5. Final resolution (lines 116+): CLI option values > environment variables
126
+ // Pattern: config.X || getenv('VAR') means CLI options have highest priority
127
+ if (config.configuration) {
128
+ loadLenvConfig({ configuration: config.configuration, override: true, quiet: true });
129
+ }
130
+
131
+ // After loading configuration, resolve final values
132
+ // Priority: CLI option > environment variable
133
+ const BOT_TOKEN = config.token || getenv('TELEGRAM_BOT_TOKEN', '');
134
+ const VERBOSE = config.verbose || getenv('TELEGRAM_BOT_VERBOSE', 'false') === 'true';
135
+
136
+ if (!BOT_TOKEN) {
137
+ console.error('Error: TELEGRAM_BOT_TOKEN environment variable or --token option is not set');
138
+ console.error('Please set it with: export TELEGRAM_BOT_TOKEN=your_bot_token');
139
+ console.error('Or use: hive-telegram-bot --token your_bot_token');
140
+ process.exit(1);
141
+ }
142
+
143
+ // After loading configuration, resolve final values from environment or config
144
+ // Priority: CLI option > environment variable (from .lenv or .env)
145
+ // NOTE: This section moved BEFORE loading telegraf for faster dry-run mode (issue #801)
146
+ const resolvedAllowedChats = config.allowedChats || getenv('TELEGRAM_ALLOWED_CHATS', '');
147
+ const allowedChats = resolvedAllowedChats
148
+ ? lino.parseNumericIds(resolvedAllowedChats)
149
+ : null;
150
+
151
+ // Parse override options
152
+ const resolvedSolveOverrides = config.solveOverrides || getenv('TELEGRAM_SOLVE_OVERRIDES', '');
153
+ const solveOverrides = resolvedSolveOverrides
154
+ ? lino.parse(resolvedSolveOverrides).map(line => line.trim()).filter(line => line)
155
+ : [];
156
+
157
+ const resolvedHiveOverrides = config.hiveOverrides || getenv('TELEGRAM_HIVE_OVERRIDES', '');
158
+ const hiveOverrides = resolvedHiveOverrides
159
+ ? lino.parse(resolvedHiveOverrides).map(line => line.trim()).filter(line => line)
160
+ : [];
161
+
162
+ // Command enable/disable flags
163
+ // Note: yargs automatically supports --no-solve and --no-hive for negation
164
+ // Priority: CLI option > environment variable
165
+ const solveEnabled = config.solve;
166
+ const hiveEnabled = config.hive;
167
+
168
+ // Validate solve overrides early using solve's yargs config
169
+ // Only validate if solve command is enabled
170
+ if (solveEnabled && solveOverrides.length > 0) {
171
+ console.log('Validating solve overrides...');
172
+ try {
173
+ // Add a dummy URL as the first argument (required positional for solve)
174
+ const testArgs = ['https://github.com/test/test/issues/1', ...solveOverrides];
175
+
176
+ // Temporarily suppress stderr to avoid yargs error output during validation
177
+ const originalStderrWrite = process.stderr.write;
178
+ const stderrBuffer = [];
179
+ process.stderr.write = (chunk) => {
180
+ stderrBuffer.push(chunk);
181
+ return true;
182
+ };
183
+
184
+ try {
185
+ // Use .parse() instead of yargs(args).parseSync() to ensure .strict() mode works
186
+ const testYargs = createSolveYargsConfig(yargs());
187
+ // Suppress yargs error output - we'll handle errors ourselves
188
+ testYargs
189
+ .exitProcess(false)
190
+ .showHelpOnFail(false)
191
+ .fail((msg, err) => {
192
+ if (err) throw err;
193
+ throw new Error(msg);
194
+ });
195
+ await testYargs.parse(testArgs);
196
+ console.log('āœ… Solve overrides validated successfully');
197
+ } finally {
198
+ // Restore stderr
199
+ process.stderr.write = originalStderrWrite;
200
+ }
201
+ } catch (error) {
202
+ console.error(`āŒ Invalid solve-overrides: ${error.message || String(error)}`);
203
+ console.error(` Overrides: ${solveOverrides.join(' ')}`);
204
+ process.exit(1);
205
+ }
206
+ }
207
+
208
+ // Validate hive overrides early using hive's yargs config
209
+ // Only validate if hive command is enabled
210
+ if (hiveEnabled && hiveOverrides.length > 0) {
211
+ console.log('Validating hive overrides...');
212
+ try {
213
+ // Add a dummy URL as the first argument (required positional for hive)
214
+ const testArgs = ['https://github.com/test/test', ...hiveOverrides];
215
+
216
+ // Temporarily suppress stderr to avoid yargs error output during validation
217
+ const originalStderrWrite = process.stderr.write;
218
+ const stderrBuffer = [];
219
+ process.stderr.write = (chunk) => {
220
+ stderrBuffer.push(chunk);
221
+ return true;
222
+ };
223
+
224
+ try {
225
+ // Use .parse() instead of yargs(args).parseSync() to ensure .strict() mode works
226
+ const testYargs = createHiveYargsConfig(yargs());
227
+ // Suppress yargs error output - we'll handle errors ourselves
228
+ testYargs
229
+ .exitProcess(false)
230
+ .showHelpOnFail(false)
231
+ .fail((msg, err) => {
232
+ if (err) throw err;
233
+ throw new Error(msg);
234
+ });
235
+ await testYargs.parse(testArgs);
236
+ console.log('āœ… Hive overrides validated successfully');
237
+ } finally {
238
+ // Restore stderr
239
+ process.stderr.write = originalStderrWrite;
240
+ }
241
+ } catch (error) {
242
+ console.error(`āŒ Invalid hive-overrides: ${error.message || String(error)}`);
243
+ console.error(` Overrides: ${hiveOverrides.join(' ')}`);
244
+ process.exit(1);
245
+ }
246
+ }
247
+
248
+ // Handle dry-run mode - exit after validation WITHOUT loading heavy dependencies
249
+ // This significantly speeds up dry-run mode by skipping telegraf loading (~3-8 seconds)
250
+ // See issue #801 for details
251
+ if (config.dryRun) {
252
+ console.log('\nāœ… Dry-run mode: All validations passed successfully!');
253
+ console.log('\nConfiguration summary:');
254
+ console.log(' Token:', BOT_TOKEN ? `${BOT_TOKEN.substring(0, 10)}...` : 'not set');
255
+ if (allowedChats && allowedChats.length > 0) {
256
+ console.log(' Allowed chats:', lino.format(allowedChats));
257
+ } else {
258
+ console.log(' Allowed chats: All (no restrictions)');
259
+ }
260
+ console.log(' Commands enabled:', { solve: solveEnabled, hive: hiveEnabled });
261
+ if (solveOverrides.length > 0) {
262
+ console.log(' Solve overrides:', lino.format(solveOverrides));
263
+ }
264
+ if (hiveOverrides.length > 0) {
265
+ console.log(' Hive overrides:', lino.format(hiveOverrides));
266
+ }
267
+ console.log('\nšŸŽ‰ Bot configuration is valid. Exiting without starting the bot.');
268
+ process.exit(0);
269
+ }
270
+
271
+ // === HEAVY DEPENDENCIES LOADED BELOW (skipped in dry-run mode) ===
272
+ // These imports are placed after the dry-run check to significantly speed up
273
+ // configuration validation. The telegraf module in particular can take 3-8 seconds
274
+ // to load on cold start due to network fetch from unpkg.com CDN.
275
+ // See issue #801 for details.
276
+
277
+ // Initialize Sentry for error tracking
278
+ await initializeSentry({
279
+ debug: VERBOSE,
280
+ environment: process.env.NODE_ENV || 'production',
281
+ });
282
+
283
+ const telegrafModule = await use('telegraf');
284
+ const { Telegraf } = telegrafModule;
285
+
286
+ const bot = new Telegraf(BOT_TOKEN, {
287
+ // Remove the default 90-second timeout for message handlers
288
+ // This is important because command handlers (like /solve) spawn long-running processes
289
+ handlerTimeout: Infinity
290
+ });
291
+
292
+ // Track bot startup time to ignore messages sent before bot started
293
+ // Using Unix timestamp (seconds since epoch) to match Telegram's message.date format
294
+ const BOT_START_TIME = Math.floor(Date.now() / 1000);
295
+
296
+ function isChatAuthorized(chatId) {
297
+ if (!allowedChats) {
298
+ return true;
299
+ }
300
+ return allowedChats.includes(chatId);
301
+ }
302
+
303
+ function isOldMessage(ctx) {
304
+ // Ignore messages sent before the bot started
305
+ // This prevents processing old/pending messages from before current bot instance startup
306
+ const messageDate = ctx.message?.date;
307
+ if (!messageDate) {
308
+ return false;
309
+ }
310
+ return messageDate < BOT_START_TIME;
311
+ }
312
+
313
+ function isGroupChat(ctx) {
314
+ const chatType = ctx.chat?.type;
315
+ return chatType === 'group' || chatType === 'supergroup';
316
+ }
317
+
318
+ function isForwardedOrReply(ctx) {
319
+ const message = ctx.message;
320
+ if (!message) {
321
+ if (VERBOSE) {
322
+ console.log('[VERBOSE] isForwardedOrReply: No message object');
323
+ }
324
+ return false;
325
+ }
326
+
327
+ if (VERBOSE) {
328
+ console.log('[VERBOSE] isForwardedOrReply: Checking message fields...');
329
+ console.log('[VERBOSE] message.forward_origin:', JSON.stringify(message.forward_origin));
330
+ console.log('[VERBOSE] message.forward_origin?.type:', message.forward_origin?.type);
331
+ console.log('[VERBOSE] message.forward_from:', JSON.stringify(message.forward_from));
332
+ console.log('[VERBOSE] message.forward_from_chat:', JSON.stringify(message.forward_from_chat));
333
+ console.log('[VERBOSE] message.forward_from_message_id:', message.forward_from_message_id);
334
+ console.log('[VERBOSE] message.forward_signature:', message.forward_signature);
335
+ console.log('[VERBOSE] message.forward_sender_name:', message.forward_sender_name);
336
+ console.log('[VERBOSE] message.forward_date:', message.forward_date);
337
+ console.log('[VERBOSE] message.reply_to_message:', JSON.stringify(message.reply_to_message));
338
+ console.log('[VERBOSE] message.reply_to_message?.message_id:', message.reply_to_message?.message_id);
339
+ }
340
+
341
+ // Check if message is forwarded (has forward_origin field with actual content)
342
+ // Note: We check for .type because Telegram might send empty objects {}
343
+ // which are truthy in JavaScript but don't indicate a forwarded message
344
+ if (message.forward_origin && message.forward_origin.type) {
345
+ if (VERBOSE) {
346
+ console.log('[VERBOSE] isForwardedOrReply: TRUE - forward_origin.type exists:', message.forward_origin.type);
347
+ }
348
+ return true;
349
+ }
350
+ // Also check old forwarding API fields for backward compatibility
351
+ if (message.forward_from || message.forward_from_chat ||
352
+ message.forward_from_message_id || message.forward_signature ||
353
+ message.forward_sender_name || message.forward_date) {
354
+ if (VERBOSE) {
355
+ console.log('[VERBOSE] isForwardedOrReply: TRUE - old forwarding API field detected');
356
+ if (message.forward_from) console.log('[VERBOSE] Triggered by: forward_from');
357
+ if (message.forward_from_chat) console.log('[VERBOSE] Triggered by: forward_from_chat');
358
+ if (message.forward_from_message_id) console.log('[VERBOSE] Triggered by: forward_from_message_id');
359
+ if (message.forward_signature) console.log('[VERBOSE] Triggered by: forward_signature');
360
+ if (message.forward_sender_name) console.log('[VERBOSE] Triggered by: forward_sender_name');
361
+ if (message.forward_date) console.log('[VERBOSE] Triggered by: forward_date');
362
+ }
363
+ return true;
364
+ }
365
+ // Check if message is a reply (has reply_to_message field with actual content)
366
+ // Note: We check for .message_id because Telegram might send empty objects {}
367
+ // IMPORTANT: In forum groups, messages in topics have reply_to_message pointing to the topic's
368
+ // first message (with forum_topic_created). These are NOT user replies, just part of the thread.
369
+ // We must exclude these to allow commands in forum topics.
370
+ if (message.reply_to_message && message.reply_to_message.message_id) {
371
+ // If the reply_to_message is a forum topic creation message, this is NOT a user reply
372
+ if (message.reply_to_message.forum_topic_created) {
373
+ if (VERBOSE) {
374
+ console.log('[VERBOSE] isForwardedOrReply: FALSE - reply is to forum topic creation, not user reply');
375
+ console.log('[VERBOSE] Forum topic:', message.reply_to_message.forum_topic_created);
376
+ }
377
+ // This is just a message in a forum topic, not a reply to another user
378
+ // Allow the message to proceed
379
+ } else {
380
+ // This is an actual reply to another user's message
381
+ if (VERBOSE) {
382
+ console.log('[VERBOSE] isForwardedOrReply: TRUE - reply_to_message.message_id exists:', message.reply_to_message.message_id);
383
+ }
384
+ return true;
385
+ }
386
+ }
387
+
388
+ if (VERBOSE) {
389
+ console.log('[VERBOSE] isForwardedOrReply: FALSE - no forwarding or reply detected');
390
+ }
391
+ return false;
392
+ }
393
+
394
+ async function findStartScreenCommand() {
395
+ try {
396
+ const { stdout } = await exec('which start-screen');
397
+ return stdout.trim();
398
+ } catch {
399
+ return null;
400
+ }
401
+ }
402
+
403
+ async function executeStartScreen(command, args) {
404
+ try {
405
+ // Check if start-screen is available BEFORE first execution
406
+ const whichPath = await findStartScreenCommand();
407
+
408
+ if (!whichPath) {
409
+ const warningMsg = 'āš ļø WARNING: start-screen command not found in PATH\n' +
410
+ 'Please ensure @link-assistant/hive-mind is properly installed\n' +
411
+ 'You may need to run: npm install -g @link-assistant/hive-mind';
412
+ console.warn(warningMsg);
413
+
414
+ // Still try to execute with 'start-screen' in case it's available in PATH but 'which' failed
415
+ return {
416
+ success: false,
417
+ warning: warningMsg,
418
+ error: 'start-screen command not found in PATH'
419
+ };
420
+ }
421
+
422
+ // Use the resolved path from which
423
+ if (VERBOSE) {
424
+ console.log(`[VERBOSE] Found start-screen at: ${whichPath}`);
425
+ }
426
+
427
+ return await executeWithCommand(whichPath, command, args);
428
+ } catch (error) {
429
+ console.error('Error executing start-screen:', error);
430
+ return {
431
+ success: false,
432
+ output: '',
433
+ error: error.message
434
+ };
435
+ }
436
+ }
437
+
438
+ function executeWithCommand(startScreenCmd, command, args) {
439
+ return new Promise((resolve) => {
440
+ const allArgs = [command, ...args];
441
+
442
+ if (VERBOSE) {
443
+ console.log(`[VERBOSE] Executing: ${startScreenCmd} ${allArgs.join(' ')}`);
444
+ } else {
445
+ console.log(`Executing: ${startScreenCmd} ${allArgs.join(' ')}`);
446
+ }
447
+
448
+ const child = spawn(startScreenCmd, allArgs, {
449
+ stdio: ['ignore', 'pipe', 'pipe'],
450
+ detached: false,
451
+ env: process.env
452
+ });
453
+
454
+ let stdout = '';
455
+ let stderr = '';
456
+
457
+ child.stdout.on('data', (data) => {
458
+ stdout += data.toString();
459
+ });
460
+
461
+ child.stderr.on('data', (data) => {
462
+ stderr += data.toString();
463
+ });
464
+
465
+ child.on('error', (error) => {
466
+ resolve({
467
+ success: false,
468
+ output: stdout,
469
+ error: error.message
470
+ });
471
+ });
472
+
473
+ child.on('close', (code) => {
474
+ if (code === 0) {
475
+ resolve({
476
+ success: true,
477
+ output: stdout
478
+ });
479
+ } else {
480
+ resolve({
481
+ success: false,
482
+ output: stdout,
483
+ error: stderr || `Command exited with code ${code}`
484
+ });
485
+ }
486
+ });
487
+ });
488
+ }
489
+
490
+ /**
491
+ * Validates the model name in the args array and returns an error message if invalid
492
+ * @param {string[]} args - Array of command arguments
493
+ * @param {string} tool - The tool to validate against ('claude' or 'opencode')
494
+ * @returns {string|null} Error message if invalid, null if valid or no model specified
495
+ */
496
+ function validateModelInArgs(args, tool = 'claude') {
497
+ // Find --model or -m flag and its value
498
+ for (let i = 0; i < args.length; i++) {
499
+ if (args[i] === '--model' || args[i] === '-m') {
500
+ if (i + 1 < args.length) {
501
+ const modelName = args[i + 1];
502
+ const validation = validateModelName(modelName, tool);
503
+ if (!validation.valid) {
504
+ return validation.message;
505
+ }
506
+ }
507
+ } else if (args[i].startsWith('--model=')) {
508
+ const modelName = args[i].substring('--model='.length);
509
+ const validation = validateModelName(modelName, tool);
510
+ if (!validation.valid) {
511
+ return validation.message;
512
+ }
513
+ }
514
+ }
515
+ return null;
516
+ }
517
+
518
+ function parseCommandArgs(text) {
519
+ // Use only first line and trim it
520
+ const firstLine = text.split('\n')[0].trim();
521
+ const argsText = firstLine.replace(/^\/\w+\s*/, '');
522
+
523
+ if (!argsText.trim()) {
524
+ return [];
525
+ }
526
+
527
+ // Replace em-dash (—) with double-dash (--) to fix Telegram auto-replacement
528
+ const normalizedArgsText = argsText.replace(/—/g, '--');
529
+
530
+ const args = [];
531
+ let currentArg = '';
532
+ let inQuotes = false;
533
+ let quoteChar = null;
534
+
535
+ for (let i = 0; i < normalizedArgsText.length; i++) {
536
+ const char = normalizedArgsText[i];
537
+
538
+ if ((char === '"' || char === "'") && !inQuotes) {
539
+ inQuotes = true;
540
+ quoteChar = char;
541
+ } else if (char === quoteChar && inQuotes) {
542
+ inQuotes = false;
543
+ quoteChar = null;
544
+ } else if (char === ' ' && !inQuotes) {
545
+ if (currentArg) {
546
+ args.push(currentArg);
547
+ currentArg = '';
548
+ }
549
+ } else {
550
+ currentArg += char;
551
+ }
552
+ }
553
+
554
+ if (currentArg) {
555
+ args.push(currentArg);
556
+ }
557
+
558
+ return args;
559
+ }
560
+
561
+ function mergeArgsWithOverrides(userArgs, overrides) {
562
+ if (!overrides || overrides.length === 0) {
563
+ return userArgs;
564
+ }
565
+
566
+ // Parse overrides to identify flags and their values
567
+ const overrideFlags = new Map(); // Map of flag -> value (or null for boolean flags)
568
+
569
+ for (let i = 0; i < overrides.length; i++) {
570
+ const arg = overrides[i];
571
+ if (arg.startsWith('--')) {
572
+ // Check if next item is a value (doesn't start with --)
573
+ if (i + 1 < overrides.length && !overrides[i + 1].startsWith('--')) {
574
+ overrideFlags.set(arg, overrides[i + 1]);
575
+ i++; // Skip the value in next iteration
576
+ } else {
577
+ overrideFlags.set(arg, null); // Boolean flag
578
+ }
579
+ }
580
+ }
581
+
582
+ // Filter user args to remove any that conflict with overrides
583
+ const filteredArgs = [];
584
+ for (let i = 0; i < userArgs.length; i++) {
585
+ const arg = userArgs[i];
586
+ if (arg.startsWith('--')) {
587
+ // If this flag exists in overrides, skip it and its value
588
+ if (overrideFlags.has(arg)) {
589
+ // Skip the flag
590
+ // Also skip next arg if it's a value (doesn't start with --)
591
+ if (i + 1 < userArgs.length && !userArgs[i + 1].startsWith('--')) {
592
+ i++; // Skip the value too
593
+ }
594
+ continue;
595
+ }
596
+ }
597
+ filteredArgs.push(arg);
598
+ }
599
+
600
+ // Merge: filtered user args + overrides
601
+ return [...filteredArgs, ...overrides];
602
+ }
603
+
604
+ /**
605
+ * Validate GitHub URL for Telegram bot commands
606
+ *
607
+ * @param {string[]} args - Command arguments (first arg should be URL)
608
+ * @param {Object} options - Validation options
609
+ * @param {string[]} options.allowedTypes - Allowed URL types (e.g., ['issue', 'pull'] or ['repository', 'organization', 'user'])
610
+ * @param {string} options.commandName - Command name for error messages (e.g., 'solve' or 'hive')
611
+ * @param {string} options.exampleUrl - Example URL for error messages
612
+ * @returns {{ valid: boolean, error?: string }}
613
+ */
614
+ function validateGitHubUrl(args, options = {}) {
615
+ // Default options for /solve command (backward compatibility)
616
+ const {
617
+ allowedTypes = ['issue', 'pull'],
618
+ commandName = 'solve'
619
+ } = options;
620
+
621
+ if (args.length === 0) {
622
+ return {
623
+ valid: false,
624
+ error: `Missing GitHub URL. Usage: /${commandName} <github-url> [options]`
625
+ };
626
+ }
627
+
628
+ const url = args[0];
629
+ if (!url.includes('github.com')) {
630
+ return {
631
+ valid: false,
632
+ error: 'First argument must be a GitHub URL'
633
+ };
634
+ }
635
+
636
+ // Parse the URL to validate structure
637
+ const parsed = parseGitHubUrl(url);
638
+ if (!parsed.valid) {
639
+ return {
640
+ valid: false,
641
+ error: parsed.error || 'Invalid GitHub URL'
642
+ };
643
+ }
644
+
645
+ // Check if the URL type is allowed for this command
646
+ if (!allowedTypes.includes(parsed.type)) {
647
+ const allowedTypesStr = allowedTypes.map(t => t === 'pull' ? 'pull request' : t).join(', ');
648
+ return {
649
+ valid: false,
650
+ error: `URL must be a GitHub ${allowedTypesStr} (not ${parsed.type})`
651
+ };
652
+ }
653
+
654
+ return { valid: true };
655
+ }
656
+
657
+ /**
658
+ * Escape special characters for Telegram's legacy Markdown parser.
659
+ * In Telegram's Markdown, these characters need escaping: _ * [ ] ( ) ~ ` > # + - = | { } . !
660
+ * However, for plain text (not inside markup), we primarily need to escape _ and *
661
+ * to prevent them from being interpreted as formatting.
662
+ *
663
+ * @param {string} text - Text to escape
664
+ * @returns {string} Escaped text safe for Markdown parse_mode
665
+ */
666
+ /**
667
+ * Extract GitHub issue/PR URL from message text
668
+ * Validates that message contains exactly one GitHub issue/PR link
669
+ *
670
+ * @param {string} text - Message text to search
671
+ * @returns {{ url: string|null, error: string|null, linkCount: number }}
672
+ */
673
+ function extractGitHubUrl(text) {
674
+ if (!text || typeof text !== 'string') {
675
+ return { url: null, error: null, linkCount: 0 };
676
+ }
677
+
678
+ // Split text into words and check each one
679
+ const words = text.split(/\s+/);
680
+ const foundUrls = [];
681
+
682
+ for (const word of words) {
683
+ // Try to parse as GitHub URL
684
+ const parsed = parseGitHubUrl(word);
685
+
686
+ // Accept issue or PR URLs
687
+ if (parsed.valid && (parsed.type === 'issue' || parsed.type === 'pull')) {
688
+ foundUrls.push(parsed.normalized);
689
+ }
690
+ }
691
+
692
+ // Check if multiple links were found
693
+ if (foundUrls.length === 0) {
694
+ return { url: null, error: null, linkCount: 0 };
695
+ } else if (foundUrls.length === 1) {
696
+ return { url: foundUrls[0], error: null, linkCount: 1 };
697
+ } else {
698
+ return {
699
+ url: null,
700
+ error: `Found ${foundUrls.length} GitHub links in the message. Please reply to a message with only one GitHub issue or PR link.`,
701
+ linkCount: foundUrls.length
702
+ };
703
+ }
704
+ }
705
+
706
+ bot.command('help', async (ctx) => {
707
+ if (VERBOSE) {
708
+ console.log('[VERBOSE] /help command received');
709
+ }
710
+
711
+ // Ignore messages sent before bot started
712
+ if (isOldMessage(ctx)) {
713
+ if (VERBOSE) {
714
+ console.log('[VERBOSE] /help ignored: old message');
715
+ }
716
+ return;
717
+ }
718
+
719
+ // Ignore forwarded or reply messages
720
+ if (isForwardedOrReply(ctx)) {
721
+ if (VERBOSE) {
722
+ console.log('[VERBOSE] /help ignored: forwarded or reply');
723
+ }
724
+ return;
725
+ }
726
+
727
+ const chatId = ctx.chat.id;
728
+ const chatType = ctx.chat.type;
729
+ const chatTitle = ctx.chat.title || 'Private Chat';
730
+
731
+ let message = 'šŸ¤– *SwarmMindBot Help*\n\n';
732
+ message += 'šŸ“‹ *Diagnostic Information:*\n';
733
+ message += `• Chat ID: \`${chatId}\`\n`;
734
+ message += `• Chat Type: ${chatType}\n`;
735
+ message += `• Chat Title: ${chatTitle}\n\n`;
736
+ message += 'šŸ“ *Available Commands:*\n\n';
737
+
738
+ if (solveEnabled) {
739
+ message += '*/solve* - Solve a GitHub issue\n';
740
+ message += 'Usage: `/solve <github-url> [options]`\n';
741
+ message += 'Example: `/solve https://github.com/owner/repo/issues/123`\n';
742
+ message += 'Or reply to a message with a GitHub link: `/solve`\n';
743
+ if (solveOverrides.length > 0) {
744
+ message += `šŸ”’ Locked options: \`${solveOverrides.join(' ')}\`\n`;
745
+ }
746
+ message += '\n';
747
+ } else {
748
+ message += '*/solve* - āŒ Disabled\n\n';
749
+ }
750
+
751
+ if (hiveEnabled) {
752
+ message += '*/hive* - Run hive command\n';
753
+ message += 'Usage: `/hive <github-url> [options]`\n';
754
+ message += 'Example: `/hive https://github.com/owner/repo --model sonnet`\n';
755
+ if (hiveOverrides.length > 0) {
756
+ message += `šŸ”’ Locked options: \`${hiveOverrides.join(' ')}\`\n`;
757
+ }
758
+ message += '\n';
759
+ } else {
760
+ message += '*/hive* - āŒ Disabled\n\n';
761
+ }
762
+
763
+ message += '*/limits* - Show Claude usage limits\n';
764
+ message += 'Usage: `/limits`\n';
765
+ message += 'Shows current session and weekly usage percentages\n\n';
766
+
767
+ message += '*/help* - Show this help message\n\n';
768
+ message += 'āš ļø *Note:* /solve, /hive and /limits commands only work in group chats.\n\n';
769
+ message += 'šŸ”§ *Available Options:*\n';
770
+ message += '• `--fork` - Fork the repository\n';
771
+ message += '• `--auto-fork` - Automatically fork public repos without write access\n';
772
+ message += '• `--auto-continue` - Continue working on existing pull request to the issue, if exists\n';
773
+ message += '• `--attach-logs` - Attach logs to PR\n';
774
+ message += '• `--verbose` - Verbose output\n';
775
+ message += '• `--model <model>` - Specify AI model (sonnet, opus, haiku, haiku-3-5, haiku-3)\n';
776
+ message += '• `--think <level>` - Thinking level (low/medium/high/max)\n';
777
+ message += '• `--interactive-mode` - Post Claude output as PR comments in real-time (experimental)\n';
778
+
779
+ if (allowedChats) {
780
+ message += '\nšŸ”’ *Restricted Mode:* This bot only accepts commands from authorized chats.\n';
781
+ message += `Authorized: ${isChatAuthorized(chatId) ? 'āœ… Yes' : 'āŒ No'}`;
782
+ }
783
+
784
+ message += '\n\nšŸ”§ *Troubleshooting:*\n';
785
+ message += 'If bot is not receiving messages:\n';
786
+ message += '1. Check privacy mode in @BotFather\n';
787
+ message += ' • Send `/setprivacy` to @BotFather\n';
788
+ message += ' • Choose "Disable" for your bot\n';
789
+ message += ' • Remove bot from group and re-add\n';
790
+ message += '2. Or make bot an admin in the group\n';
791
+ message += '3. Restart bot with `--verbose` flag for diagnostics';
792
+
793
+ await ctx.reply(message, { parse_mode: 'Markdown' });
794
+ });
795
+
796
+ bot.command('limits', async (ctx) => {
797
+ if (VERBOSE) {
798
+ console.log('[VERBOSE] /limits command received');
799
+ }
800
+
801
+ // Add breadcrumb for error tracking
802
+ await addBreadcrumb({
803
+ category: 'telegram.command',
804
+ message: '/limits command received',
805
+ level: 'info',
806
+ data: {
807
+ chatId: ctx.chat?.id,
808
+ chatType: ctx.chat?.type,
809
+ userId: ctx.from?.id,
810
+ username: ctx.from?.username,
811
+ },
812
+ });
813
+
814
+ // Ignore messages sent before bot started
815
+ if (isOldMessage(ctx)) {
816
+ if (VERBOSE) {
817
+ console.log('[VERBOSE] /limits ignored: old message');
818
+ }
819
+ return;
820
+ }
821
+
822
+ // Ignore forwarded or reply messages
823
+ if (isForwardedOrReply(ctx)) {
824
+ if (VERBOSE) {
825
+ console.log('[VERBOSE] /limits ignored: forwarded or reply');
826
+ }
827
+ return;
828
+ }
829
+
830
+ if (!isGroupChat(ctx)) {
831
+ if (VERBOSE) {
832
+ console.log('[VERBOSE] /limits ignored: not a group chat');
833
+ }
834
+ await ctx.reply('āŒ The /limits command only works in group chats. Please add this bot to a group and make it an admin.', { reply_to_message_id: ctx.message.message_id });
835
+ return;
836
+ }
837
+
838
+ const chatId = ctx.chat.id;
839
+ if (!isChatAuthorized(chatId)) {
840
+ if (VERBOSE) {
841
+ console.log('[VERBOSE] /limits ignored: chat not authorized');
842
+ }
843
+ await ctx.reply(`āŒ This chat (ID: ${chatId}) is not authorized to use this bot. Please contact the bot administrator.`, { reply_to_message_id: ctx.message.message_id });
844
+ return;
845
+ }
846
+
847
+ // Send "fetching" message to indicate work is in progress
848
+ const fetchingMessage = await ctx.reply('šŸ”„ Fetching Claude usage limits...', { reply_to_message_id: ctx.message.message_id });
849
+
850
+ // Get the usage limits using the library function
851
+ const result = await getClaudeUsageLimits(VERBOSE);
852
+
853
+ if (!result.success) {
854
+ // Edit the fetching message to show the error
855
+ // Escape the error message for MarkdownV2, preserving inline code blocks
856
+ const escapedError = escapeMarkdownV2(result.error, { preserveCodeBlocks: true });
857
+ await ctx.telegram.editMessageText(
858
+ fetchingMessage.chat.id,
859
+ fetchingMessage.message_id,
860
+ undefined,
861
+ `āŒ ${escapedError}`,
862
+ { parse_mode: 'MarkdownV2' }
863
+ );
864
+ return;
865
+ }
866
+
867
+ // Format and edit the fetching message with the results
868
+ const message = 'šŸ“Š *Claude Usage Limits*\n\n' + formatUsageMessage(result.usage);
869
+ await ctx.telegram.editMessageText(
870
+ fetchingMessage.chat.id,
871
+ fetchingMessage.message_id,
872
+ undefined,
873
+ message,
874
+ { parse_mode: 'Markdown' }
875
+ );
876
+ });
877
+
878
+ bot.command(/^solve$/i, async (ctx) => {
879
+ if (VERBOSE) {
880
+ console.log('[VERBOSE] /solve command received');
881
+ }
882
+
883
+ // Add breadcrumb for error tracking
884
+ await addBreadcrumb({
885
+ category: 'telegram.command',
886
+ message: '/solve command received',
887
+ level: 'info',
888
+ data: {
889
+ chatId: ctx.chat?.id,
890
+ chatType: ctx.chat?.type,
891
+ userId: ctx.from?.id,
892
+ username: ctx.from?.username,
893
+ },
894
+ });
895
+
896
+ if (!solveEnabled) {
897
+ if (VERBOSE) {
898
+ console.log('[VERBOSE] /solve ignored: command disabled');
899
+ }
900
+ await ctx.reply('āŒ The /solve command is disabled on this bot instance.');
901
+ return;
902
+ }
903
+
904
+ // Ignore messages sent before bot started
905
+ if (isOldMessage(ctx)) {
906
+ if (VERBOSE) {
907
+ console.log('[VERBOSE] /solve ignored: old message');
908
+ }
909
+ return;
910
+ }
911
+
912
+ // Check if this is a forwarded message (not allowed)
913
+ // But allow reply messages for URL extraction feature
914
+ const message = ctx.message;
915
+ const isForwarded = message.forward_origin && message.forward_origin.type;
916
+ const isOldApiForwarded = message.forward_from || message.forward_from_chat ||
917
+ message.forward_from_message_id || message.forward_signature ||
918
+ message.forward_sender_name || message.forward_date;
919
+
920
+ if (isForwarded || isOldApiForwarded) {
921
+ if (VERBOSE) {
922
+ console.log('[VERBOSE] /solve ignored: forwarded message');
923
+ }
924
+ return;
925
+ }
926
+
927
+ if (!isGroupChat(ctx)) {
928
+ if (VERBOSE) {
929
+ console.log('[VERBOSE] /solve ignored: not a group chat');
930
+ }
931
+ await ctx.reply('āŒ The /solve command only works in group chats. Please add this bot to a group and make it an admin.', { reply_to_message_id: ctx.message.message_id });
932
+ return;
933
+ }
934
+
935
+ const chatId = ctx.chat.id;
936
+ if (!isChatAuthorized(chatId)) {
937
+ if (VERBOSE) {
938
+ console.log('[VERBOSE] /solve ignored: chat not authorized');
939
+ }
940
+ await ctx.reply(`āŒ This chat (ID: ${chatId}) is not authorized to use this bot. Please contact the bot administrator.`, { reply_to_message_id: ctx.message.message_id });
941
+ return;
942
+ }
943
+
944
+ if (VERBOSE) {
945
+ console.log('[VERBOSE] /solve passed all checks, executing...');
946
+ }
947
+
948
+ let userArgs = parseCommandArgs(ctx.message.text);
949
+
950
+ // Check if this is a reply to a message and user didn't provide URL
951
+ // In that case, try to extract GitHub URL from the replied message
952
+ const isReply = message.reply_to_message &&
953
+ message.reply_to_message.message_id &&
954
+ !message.reply_to_message.forum_topic_created;
955
+
956
+ if (isReply && userArgs.length === 0) {
957
+ if (VERBOSE) {
958
+ console.log('[VERBOSE] /solve is a reply without URL, extracting from replied message...');
959
+ }
960
+
961
+ const replyText = message.reply_to_message.text || '';
962
+ const extraction = extractGitHubUrl(replyText);
963
+
964
+ if (extraction.error) {
965
+ // Multiple links found
966
+ if (VERBOSE) {
967
+ console.log('[VERBOSE] Multiple GitHub URLs found in replied message');
968
+ }
969
+ await ctx.reply(`āŒ ${extraction.error}`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
970
+ return;
971
+ } else if (extraction.url) {
972
+ // Single link found
973
+ if (VERBOSE) {
974
+ console.log('[VERBOSE] Extracted URL from reply:', extraction.url);
975
+ }
976
+ // Add the extracted URL as the first argument
977
+ userArgs = [extraction.url];
978
+ } else {
979
+ // No link found
980
+ if (VERBOSE) {
981
+ console.log('[VERBOSE] No GitHub URL found in replied message');
982
+ }
983
+ await ctx.reply('āŒ No GitHub issue/PR link found in the replied message.\n\nExample: Reply to a message containing a GitHub issue link with `/solve`', { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
984
+ return;
985
+ }
986
+ }
987
+
988
+ const validation = validateGitHubUrl(userArgs);
989
+ if (!validation.valid) {
990
+ await ctx.reply(`āŒ ${validation.error}\n\nExample: \`/solve https://github.com/owner/repo/issues/123\`\n\nOr reply to a message containing a GitHub link with \`/solve\``, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
991
+ return;
992
+ }
993
+
994
+ // Merge user args with overrides
995
+ const args = mergeArgsWithOverrides(userArgs, solveOverrides);
996
+
997
+ // Determine tool from args (default: claude)
998
+ let solveTool = 'claude';
999
+ for (let i = 0; i < args.length; i++) {
1000
+ if (args[i] === '--tool' && i + 1 < args.length) {
1001
+ solveTool = args[i + 1];
1002
+ } else if (args[i].startsWith('--tool=')) {
1003
+ solveTool = args[i].substring('--tool='.length);
1004
+ }
1005
+ }
1006
+
1007
+ // Validate model name with helpful error message (before yargs validation)
1008
+ const modelError = validateModelInArgs(args, solveTool);
1009
+ if (modelError) {
1010
+ await ctx.reply(`āŒ ${modelError}`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1011
+ return;
1012
+ }
1013
+
1014
+ // Validate merged arguments using solve's yargs config
1015
+ try {
1016
+ // Use .parse() instead of yargs(args).parseSync() to ensure .strict() mode works
1017
+ const testYargs = createSolveYargsConfig(yargs());
1018
+
1019
+ // Configure yargs to throw errors instead of trying to exit the process
1020
+ // This prevents confusing error messages when validation fails but execution continues
1021
+ let failureMessage = null;
1022
+ testYargs
1023
+ .exitProcess(false)
1024
+ .fail((msg, err) => {
1025
+ // Capture the failure message instead of letting yargs print it
1026
+ failureMessage = msg || (err && err.message) || 'Unknown validation error';
1027
+ throw new Error(failureMessage);
1028
+ });
1029
+
1030
+ testYargs.parse(args);
1031
+ } catch (error) {
1032
+ await ctx.reply(`āŒ Invalid options: ${error.message || String(error)}\n\nUse /help to see available options`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1033
+ return;
1034
+ }
1035
+
1036
+ const requester = buildUserMention({ user: ctx.from, parseMode: 'Markdown' });
1037
+ // Escape URL to prevent Markdown parsing errors with underscores and asterisks
1038
+ const escapedUrl = escapeMarkdown(args[0]);
1039
+ let statusMsg = `šŸš€ Starting solve command...\nRequested by: ${requester}\nURL: ${escapedUrl}\nOptions: ${args.slice(1).join(' ') || 'none'}`;
1040
+ if (solveOverrides.length > 0) {
1041
+ statusMsg += `\nšŸ”’ Locked options: ${solveOverrides.join(' ')}`;
1042
+ }
1043
+ await ctx.reply(statusMsg, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1044
+
1045
+ const result = await executeStartScreen('solve', args);
1046
+
1047
+ if (result.warning) {
1048
+ await ctx.reply(`āš ļø ${result.warning}`, { parse_mode: 'Markdown' });
1049
+ return;
1050
+ }
1051
+
1052
+ if (result.success) {
1053
+ const sessionNameMatch = result.output.match(/session:\s*(\S+)/i) ||
1054
+ result.output.match(/screen -r\s+(\S+)/);
1055
+ const sessionName = sessionNameMatch ? sessionNameMatch[1] : 'unknown';
1056
+
1057
+ let response = 'āœ… Solve command started successfully!\n\n';
1058
+ response += `šŸ“Š *Session:* \`${sessionName}\`\n`;
1059
+
1060
+ await ctx.reply(response, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1061
+ } else {
1062
+ let response = 'āŒ Error executing solve command:\n\n';
1063
+ response += `\`\`\`\n${result.error || result.output}\n\`\`\``;
1064
+ await ctx.reply(response, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1065
+ }
1066
+ });
1067
+
1068
+ bot.command(/^hive$/i, async (ctx) => {
1069
+ if (VERBOSE) {
1070
+ console.log('[VERBOSE] /hive command received');
1071
+ }
1072
+
1073
+ // Add breadcrumb for error tracking
1074
+ await addBreadcrumb({
1075
+ category: 'telegram.command',
1076
+ message: '/hive command received',
1077
+ level: 'info',
1078
+ data: {
1079
+ chatId: ctx.chat?.id,
1080
+ chatType: ctx.chat?.type,
1081
+ userId: ctx.from?.id,
1082
+ username: ctx.from?.username,
1083
+ },
1084
+ });
1085
+
1086
+ if (!hiveEnabled) {
1087
+ if (VERBOSE) {
1088
+ console.log('[VERBOSE] /hive ignored: command disabled');
1089
+ }
1090
+ await ctx.reply('āŒ The /hive command is disabled on this bot instance.');
1091
+ return;
1092
+ }
1093
+
1094
+ // Ignore messages sent before bot started
1095
+ if (isOldMessage(ctx)) {
1096
+ if (VERBOSE) {
1097
+ console.log('[VERBOSE] /hive ignored: old message');
1098
+ }
1099
+ return;
1100
+ }
1101
+
1102
+ // Ignore forwarded or reply messages
1103
+ if (isForwardedOrReply(ctx)) {
1104
+ if (VERBOSE) {
1105
+ console.log('[VERBOSE] /hive ignored: forwarded or reply');
1106
+ }
1107
+ return;
1108
+ }
1109
+
1110
+ if (!isGroupChat(ctx)) {
1111
+ if (VERBOSE) {
1112
+ console.log('[VERBOSE] /hive ignored: not a group chat');
1113
+ }
1114
+ await ctx.reply('āŒ The /hive command only works in group chats. Please add this bot to a group and make it an admin.', { reply_to_message_id: ctx.message.message_id });
1115
+ return;
1116
+ }
1117
+
1118
+ const chatId = ctx.chat.id;
1119
+ if (!isChatAuthorized(chatId)) {
1120
+ if (VERBOSE) {
1121
+ console.log('[VERBOSE] /hive ignored: chat not authorized');
1122
+ }
1123
+ await ctx.reply(`āŒ This chat (ID: ${chatId}) is not authorized to use this bot. Please contact the bot administrator.`, { reply_to_message_id: ctx.message.message_id });
1124
+ return;
1125
+ }
1126
+
1127
+ if (VERBOSE) {
1128
+ console.log('[VERBOSE] /hive passed all checks, executing...');
1129
+ }
1130
+
1131
+ const userArgs = parseCommandArgs(ctx.message.text);
1132
+
1133
+ const validation = validateGitHubUrl(userArgs, {
1134
+ allowedTypes: ['repo', 'organization', 'user'],
1135
+ commandName: 'hive',
1136
+ exampleUrl: 'https://github.com/owner/repo'
1137
+ });
1138
+ if (!validation.valid) {
1139
+ await ctx.reply(`āŒ ${validation.error}\n\nExample: \`/hive https://github.com/owner/repo\``, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1140
+ return;
1141
+ }
1142
+
1143
+ // Merge user args with overrides
1144
+ const args = mergeArgsWithOverrides(userArgs, hiveOverrides);
1145
+
1146
+ // Determine tool from args (default: claude)
1147
+ let hiveTool = 'claude';
1148
+ for (let i = 0; i < args.length; i++) {
1149
+ if (args[i] === '--tool' && i + 1 < args.length) {
1150
+ hiveTool = args[i + 1];
1151
+ } else if (args[i].startsWith('--tool=')) {
1152
+ hiveTool = args[i].substring('--tool='.length);
1153
+ }
1154
+ }
1155
+
1156
+ // Validate model name with helpful error message (before yargs validation)
1157
+ const hiveModelError = validateModelInArgs(args, hiveTool);
1158
+ if (hiveModelError) {
1159
+ await ctx.reply(`āŒ ${hiveModelError}`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1160
+ return;
1161
+ }
1162
+
1163
+ // Validate merged arguments using hive's yargs config
1164
+ try {
1165
+ // Use .parse() instead of yargs(args).parseSync() to ensure .strict() mode works
1166
+ const testYargs = createHiveYargsConfig(yargs());
1167
+
1168
+ // Configure yargs to throw errors instead of trying to exit the process
1169
+ // This prevents confusing error messages when validation fails but execution continues
1170
+ let failureMessage = null;
1171
+ testYargs
1172
+ .exitProcess(false)
1173
+ .fail((msg, err) => {
1174
+ // Capture the failure message instead of letting yargs print it
1175
+ failureMessage = msg || (err && err.message) || 'Unknown validation error';
1176
+ throw new Error(failureMessage);
1177
+ });
1178
+
1179
+ testYargs.parse(args);
1180
+ } catch (error) {
1181
+ await ctx.reply(`āŒ Invalid options: ${error.message || String(error)}\n\nUse /help to see available options`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1182
+ return;
1183
+ }
1184
+
1185
+ const requester = buildUserMention({ user: ctx.from, parseMode: 'Markdown' });
1186
+ // Escape URL to prevent Markdown parsing errors with underscores and asterisks
1187
+ const escapedUrl = escapeMarkdown(args[0]);
1188
+ let statusMsg = `šŸš€ Starting hive command...\nRequested by: ${requester}\nURL: ${escapedUrl}\nOptions: ${args.slice(1).join(' ') || 'none'}`;
1189
+ if (hiveOverrides.length > 0) {
1190
+ statusMsg += `\nšŸ”’ Locked options: ${hiveOverrides.join(' ')}`;
1191
+ }
1192
+ await ctx.reply(statusMsg, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1193
+
1194
+ const result = await executeStartScreen('hive', args);
1195
+
1196
+ if (result.warning) {
1197
+ await ctx.reply(`āš ļø ${result.warning}`, { parse_mode: 'Markdown' });
1198
+ return;
1199
+ }
1200
+
1201
+ if (result.success) {
1202
+ const sessionNameMatch = result.output.match(/session:\s*(\S+)/i) ||
1203
+ result.output.match(/screen -r\s+(\S+)/);
1204
+ const sessionName = sessionNameMatch ? sessionNameMatch[1] : 'unknown';
1205
+
1206
+ let response = 'āœ… Hive command started successfully!\n\n';
1207
+ response += `šŸ“Š *Session:* \`${sessionName}\`\n`;
1208
+
1209
+ await ctx.reply(response, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1210
+ } else {
1211
+ let response = 'āŒ Error executing hive command:\n\n';
1212
+ response += `\`\`\`\n${result.error || result.output}\n\`\`\``;
1213
+ await ctx.reply(response, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1214
+ }
1215
+ });
1216
+
1217
+ // Add message listener for verbose debugging
1218
+ // This helps diagnose if bot is receiving messages at all
1219
+ if (VERBOSE) {
1220
+ bot.on('message', (ctx, next) => {
1221
+ console.log('[VERBOSE] Message received:');
1222
+ console.log('[VERBOSE] Chat ID:', ctx.chat?.id);
1223
+ console.log('[VERBOSE] Chat type:', ctx.chat?.type);
1224
+ console.log('[VERBOSE] Is forum:', ctx.chat?.is_forum);
1225
+ console.log('[VERBOSE] Is topic message:', ctx.message?.is_topic_message);
1226
+ console.log('[VERBOSE] Message thread ID:', ctx.message?.message_thread_id);
1227
+ console.log('[VERBOSE] Message date:', ctx.message?.date);
1228
+ console.log('[VERBOSE] Message text:', ctx.message?.text?.substring(0, 100));
1229
+ console.log('[VERBOSE] From user:', ctx.from?.username || ctx.from?.id);
1230
+ console.log('[VERBOSE] Bot start time:', BOT_START_TIME);
1231
+ console.log('[VERBOSE] Is old message:', isOldMessage(ctx));
1232
+
1233
+ // Detailed forwarding/reply detection debug info
1234
+ const msg = ctx.message;
1235
+ const isForwarded = isForwardedOrReply(ctx);
1236
+ console.log('[VERBOSE] Is forwarded/reply:', isForwarded);
1237
+ if (msg) {
1238
+ // Log ALL message fields to diagnose what Telegram is actually sending
1239
+ console.log('[VERBOSE] Full message object keys:', Object.keys(msg));
1240
+ console.log('[VERBOSE] - forward_origin:', JSON.stringify(msg.forward_origin));
1241
+ console.log('[VERBOSE] - forward_origin type:', typeof msg.forward_origin);
1242
+ console.log('[VERBOSE] - forward_origin truthy?:', !!msg.forward_origin);
1243
+ console.log('[VERBOSE] - forward_origin.type:', msg.forward_origin?.type);
1244
+ console.log('[VERBOSE] - forward_from:', JSON.stringify(msg.forward_from));
1245
+ console.log('[VERBOSE] - forward_from_chat:', JSON.stringify(msg.forward_from_chat));
1246
+ console.log('[VERBOSE] - forward_date:', msg.forward_date);
1247
+ console.log('[VERBOSE] - reply_to_message:', JSON.stringify(msg.reply_to_message));
1248
+ console.log('[VERBOSE] - reply_to_message type:', typeof msg.reply_to_message);
1249
+ console.log('[VERBOSE] - reply_to_message truthy?:', !!msg.reply_to_message);
1250
+ console.log('[VERBOSE] - reply_to_message.message_id:', msg.reply_to_message?.message_id);
1251
+ console.log('[VERBOSE] - reply_to_message.forum_topic_created:', JSON.stringify(msg.reply_to_message?.forum_topic_created));
1252
+ }
1253
+
1254
+ console.log('[VERBOSE] Is authorized:', isChatAuthorized(ctx.chat?.id));
1255
+ // Continue to next handler
1256
+ return next();
1257
+ });
1258
+ }
1259
+
1260
+ // Add global error handler for uncaught errors in middleware
1261
+ bot.catch((error, ctx) => {
1262
+ console.error('Unhandled error while processing update', ctx.update.update_id);
1263
+ console.error('Error:', error);
1264
+
1265
+ // Log detailed error information
1266
+ console.error('Error details:', {
1267
+ name: error.name,
1268
+ message: error.message,
1269
+ stack: error.stack?.split('\n').slice(0, 10).join('\n'),
1270
+ });
1271
+
1272
+ // Log context information for debugging
1273
+ if (VERBOSE) {
1274
+ console.log('[VERBOSE] Error context:', {
1275
+ chatId: ctx.chat?.id,
1276
+ chatType: ctx.chat?.type,
1277
+ messageText: ctx.message?.text?.substring(0, 100),
1278
+ fromUser: ctx.from?.username || ctx.from?.id,
1279
+ updateId: ctx.update.update_id,
1280
+ });
1281
+ }
1282
+
1283
+ // Report error to Sentry with context
1284
+ reportError(error, {
1285
+ telegramContext: {
1286
+ chatId: ctx.chat?.id,
1287
+ chatType: ctx.chat?.type,
1288
+ updateId: ctx.update.update_id,
1289
+ command: ctx.message?.text?.split(' ')[0],
1290
+ userId: ctx.from?.id,
1291
+ username: ctx.from?.username,
1292
+ },
1293
+ });
1294
+
1295
+ // Try to notify the user about the error with more details
1296
+ if (ctx?.reply) {
1297
+ // Build a more informative error message
1298
+ let errorMessage = 'āŒ An error occurred while processing your request.\n\n';
1299
+
1300
+ // Add error type/name if available
1301
+ if (error.name && error.name !== 'Error') {
1302
+ errorMessage += `**Error type:** ${error.name}\n`;
1303
+ }
1304
+
1305
+ // Add sanitized error message (avoid leaking sensitive info)
1306
+ if (error.message) {
1307
+ // Filter out potentially sensitive information
1308
+ const sanitizedMessage = error.message
1309
+ .replace(/token[s]?\s*[:=]\s*[\w-]+/gi, 'token: [REDACTED]')
1310
+ .replace(/password[s]?\s*[:=]\s*[\w-]+/gi, 'password: [REDACTED]')
1311
+ .replace(/api[_-]?key[s]?\s*[:=]\s*[\w-]+/gi, 'api_key: [REDACTED]');
1312
+
1313
+ errorMessage += `**Details:** ${sanitizedMessage}\n`;
1314
+ }
1315
+
1316
+ errorMessage += '\nšŸ’” **Troubleshooting:**\n';
1317
+ errorMessage += '• Try running the command again\n';
1318
+ errorMessage += '• Check if all required parameters are correct\n';
1319
+ errorMessage += '• If the issue persists, contact support with the error details above\n';
1320
+
1321
+ if (VERBOSE) {
1322
+ errorMessage += `\nšŸ” **Debug info:** Update ID: ${ctx.update.update_id}`;
1323
+ }
1324
+
1325
+ ctx.reply(errorMessage, { parse_mode: 'Markdown' })
1326
+ .catch(replyError => {
1327
+ console.error('Failed to send error message to user:', replyError);
1328
+ // Try sending a simple text message without Markdown if Markdown parsing failed
1329
+ ctx.reply('āŒ An error occurred while processing your request. Please try again or contact support.')
1330
+ .catch(fallbackError => {
1331
+ console.error('Failed to send fallback error message:', fallbackError);
1332
+ });
1333
+ });
1334
+ }
1335
+ });
1336
+
1337
+ // Track shutdown state to prevent startup messages after shutdown
1338
+ let isShuttingDown = false;
1339
+
1340
+ console.log('šŸ¤– SwarmMindBot is starting...');
1341
+ console.log('Bot token:', BOT_TOKEN.substring(0, 10) + '...');
1342
+ if (allowedChats && allowedChats.length > 0) {
1343
+ console.log('Allowed chats (lino):', lino.format(allowedChats));
1344
+ } else {
1345
+ console.log('Allowed chats: All (no restrictions)');
1346
+ }
1347
+ console.log('Commands enabled:', {
1348
+ solve: solveEnabled,
1349
+ hive: hiveEnabled
1350
+ });
1351
+ if (solveOverrides.length > 0) {
1352
+ console.log('Solve overrides (lino):', lino.format(solveOverrides));
1353
+ }
1354
+ if (hiveOverrides.length > 0) {
1355
+ console.log('Hive overrides (lino):', lino.format(hiveOverrides));
1356
+ }
1357
+ if (VERBOSE) {
1358
+ console.log('[VERBOSE] Verbose logging enabled');
1359
+ console.log('[VERBOSE] Bot start time (Unix):', BOT_START_TIME);
1360
+ console.log('[VERBOSE] Bot start time (ISO):', new Date(BOT_START_TIME * 1000).toISOString());
1361
+ }
1362
+
1363
+ // Delete any existing webhook before starting polling
1364
+ // This is critical because a webhook prevents polling from working
1365
+ // If the bot was previously configured with a webhook (or if one exists),
1366
+ // we must delete it to allow polling mode to receive messages
1367
+ if (VERBOSE) {
1368
+ console.log('[VERBOSE] Deleting webhook...');
1369
+ }
1370
+ bot.telegram.deleteWebhook({ drop_pending_updates: true })
1371
+ .then((result) => {
1372
+ if (VERBOSE) {
1373
+ console.log('[VERBOSE] Webhook deletion result:', result);
1374
+ }
1375
+ console.log('šŸ”„ Webhook deleted (if existed), starting polling mode...');
1376
+ if (VERBOSE) {
1377
+ console.log('[VERBOSE] Launching bot with config:', {
1378
+ allowedUpdates: ['message'],
1379
+ dropPendingUpdates: true
1380
+ });
1381
+ }
1382
+ return bot.launch({
1383
+ // Only receive message updates (commands, text messages)
1384
+ // This ensures the bot receives all message types including commands
1385
+ allowedUpdates: ['message'],
1386
+ // Drop any pending updates that were sent before the bot started
1387
+ // This ensures we only process new messages sent after this bot instance started
1388
+ dropPendingUpdates: true
1389
+ });
1390
+ })
1391
+ .then(async () => {
1392
+ // Check if shutdown was initiated before printing success messages
1393
+ if (isShuttingDown) {
1394
+ return; // Skip success messages if shutting down
1395
+ }
1396
+
1397
+ console.log('āœ… SwarmMindBot is now running!');
1398
+ console.log('Press Ctrl+C to stop');
1399
+ if (VERBOSE) {
1400
+ console.log('[VERBOSE] Bot launched successfully');
1401
+ console.log('[VERBOSE] Polling is active, waiting for messages...');
1402
+
1403
+ // Get bot info and webhook status for diagnostics
1404
+ try {
1405
+ const botInfo = await bot.telegram.getMe();
1406
+ const webhookInfo = await bot.telegram.getWebhookInfo();
1407
+
1408
+ console.log('[VERBOSE] Bot info:');
1409
+ console.log('[VERBOSE] Username: @' + botInfo.username);
1410
+ console.log('[VERBOSE] Bot ID:', botInfo.id);
1411
+ console.log('[VERBOSE] Webhook info:');
1412
+ console.log('[VERBOSE] URL:', webhookInfo.url || 'none (polling mode)');
1413
+ console.log('[VERBOSE] Pending updates:', webhookInfo.pending_update_count);
1414
+ if (webhookInfo.last_error_date) {
1415
+ console.log('[VERBOSE] Last error:', new Date(webhookInfo.last_error_date * 1000).toISOString());
1416
+ console.log('[VERBOSE] Error message:', webhookInfo.last_error_message);
1417
+ }
1418
+
1419
+ console.log('[VERBOSE]');
1420
+ console.log('[VERBOSE] āš ļø IMPORTANT: If bot is not receiving messages in group chats:');
1421
+ console.log('[VERBOSE] 1. Privacy Mode: Check if bot has privacy mode enabled in @BotFather');
1422
+ console.log('[VERBOSE] - Send /setprivacy to @BotFather');
1423
+ console.log('[VERBOSE] - Select @' + botInfo.username);
1424
+ console.log('[VERBOSE] - Choose "Disable" to receive all group messages');
1425
+ console.log('[VERBOSE] - IMPORTANT: Remove bot from group and re-add after changing!');
1426
+ console.log('[VERBOSE] 2. Admin Status: Make bot an admin in the group (admins see all messages)');
1427
+ console.log('[VERBOSE] 3. Run diagnostic: node experiments/test-telegram-bot-privacy-mode.mjs');
1428
+ console.log('[VERBOSE]');
1429
+ } catch (err) {
1430
+ console.log('[VERBOSE] Could not fetch bot info:', err.message);
1431
+ }
1432
+
1433
+ console.log('[VERBOSE] Send a message to the bot to test message reception');
1434
+ }
1435
+ })
1436
+ .catch((error) => {
1437
+ console.error('āŒ Failed to start bot:', error);
1438
+ console.error('Error details:', {
1439
+ message: error.message,
1440
+ code: error.code,
1441
+ stack: error.stack?.split('\n').slice(0, 5).join('\n')
1442
+ });
1443
+ if (VERBOSE) {
1444
+ console.error('[VERBOSE] Full error:', error);
1445
+ }
1446
+ process.exit(1);
1447
+ });
1448
+
1449
+ process.once('SIGINT', () => {
1450
+ isShuttingDown = true;
1451
+ console.log('\nšŸ›‘ Received SIGINT (Ctrl+C), stopping bot...');
1452
+ if (VERBOSE) {
1453
+ console.log('[VERBOSE] Signal: SIGINT');
1454
+ console.log('[VERBOSE] Process ID:', process.pid);
1455
+ console.log('[VERBOSE] Parent Process ID:', process.ppid);
1456
+ }
1457
+ bot.stop('SIGINT');
1458
+ });
1459
+
1460
+ process.once('SIGTERM', () => {
1461
+ isShuttingDown = true;
1462
+ console.log('\nšŸ›‘ Received SIGTERM, stopping bot...');
1463
+ if (VERBOSE) {
1464
+ console.log('[VERBOSE] Signal: SIGTERM');
1465
+ console.log('[VERBOSE] Process ID:', process.pid);
1466
+ console.log('[VERBOSE] Parent Process ID:', process.ppid);
1467
+ console.log('[VERBOSE] Possible causes:');
1468
+ console.log('[VERBOSE] - System shutdown/restart');
1469
+ console.log('[VERBOSE] - Process manager (systemd, pm2, etc.) stopping the service');
1470
+ console.log('[VERBOSE] - Manual kill command: kill <pid>');
1471
+ console.log('[VERBOSE] - Container orchestration (Docker, Kubernetes) stopping container');
1472
+ console.log('[VERBOSE] - Out of memory (OOM) killer');
1473
+ }
1474
+ console.log('ā„¹ļø SIGTERM is typically sent by:');
1475
+ console.log(' - System shutdown/restart');
1476
+ console.log(' - Process manager stopping the service');
1477
+ console.log(' - Manual termination (kill command)');
1478
+ console.log(' - Container/orchestration platform');
1479
+ console.log('šŸ’” Check system logs for more details: journalctl -u <service> or dmesg');
1480
+ bot.stop('SIGTERM');
1481
+ });