@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.
- package/CHANGELOG.md +20 -0
- package/LICENSE +24 -0
- package/README.md +769 -0
- package/package.json +58 -0
- package/src/agent.lib.mjs +705 -0
- package/src/agent.prompts.lib.mjs +196 -0
- package/src/buildUserMention.lib.mjs +71 -0
- package/src/claude-limits.lib.mjs +389 -0
- package/src/claude.lib.mjs +1445 -0
- package/src/claude.prompts.lib.mjs +203 -0
- package/src/codex.lib.mjs +552 -0
- package/src/codex.prompts.lib.mjs +194 -0
- package/src/config.lib.mjs +207 -0
- package/src/contributing-guidelines.lib.mjs +268 -0
- package/src/exit-handler.lib.mjs +205 -0
- package/src/git.lib.mjs +145 -0
- package/src/github-issue-creator.lib.mjs +246 -0
- package/src/github-linking.lib.mjs +152 -0
- package/src/github.batch.lib.mjs +272 -0
- package/src/github.graphql.lib.mjs +258 -0
- package/src/github.lib.mjs +1479 -0
- package/src/hive.config.lib.mjs +254 -0
- package/src/hive.mjs +1500 -0
- package/src/instrument.mjs +191 -0
- package/src/interactive-mode.lib.mjs +1000 -0
- package/src/lenv-reader.lib.mjs +206 -0
- package/src/lib.mjs +490 -0
- package/src/lino.lib.mjs +176 -0
- package/src/local-ci-checks.lib.mjs +324 -0
- package/src/memory-check.mjs +419 -0
- package/src/model-mapping.lib.mjs +145 -0
- package/src/model-validation.lib.mjs +278 -0
- package/src/opencode.lib.mjs +479 -0
- package/src/opencode.prompts.lib.mjs +194 -0
- package/src/protect-branch.mjs +159 -0
- package/src/review.mjs +433 -0
- package/src/reviewers-hive.mjs +643 -0
- package/src/sentry.lib.mjs +284 -0
- package/src/solve.auto-continue.lib.mjs +568 -0
- package/src/solve.auto-pr.lib.mjs +1374 -0
- package/src/solve.branch-errors.lib.mjs +341 -0
- package/src/solve.branch.lib.mjs +230 -0
- package/src/solve.config.lib.mjs +342 -0
- package/src/solve.error-handlers.lib.mjs +256 -0
- package/src/solve.execution.lib.mjs +291 -0
- package/src/solve.feedback.lib.mjs +436 -0
- package/src/solve.mjs +1128 -0
- package/src/solve.preparation.lib.mjs +210 -0
- package/src/solve.repo-setup.lib.mjs +114 -0
- package/src/solve.repository.lib.mjs +961 -0
- package/src/solve.results.lib.mjs +558 -0
- package/src/solve.session.lib.mjs +135 -0
- package/src/solve.validation.lib.mjs +325 -0
- package/src/solve.watch.lib.mjs +572 -0
- package/src/start-screen.mjs +324 -0
- package/src/task.mjs +308 -0
- package/src/telegram-bot.mjs +1481 -0
- package/src/telegram-markdown.lib.mjs +64 -0
- package/src/usage-limit.lib.mjs +218 -0
- package/src/version.lib.mjs +41 -0
- package/src/youtrack/solve.youtrack.lib.mjs +116 -0
- package/src/youtrack/youtrack-sync.mjs +219 -0
- 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
|
+
});
|