@link-assistant/hive-mind 1.74.6 → 1.74.7

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.74.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 8ea7110: Document the issue #1858 case study and add an experimental private Telegram
8
+ `/auth` command for allowlisted chat owners to check or start GitHub, Claude,
9
+ and Codex auth flows.
10
+
3
11
  ## 1.74.6
4
12
 
5
13
  ### Patch Changes
package/README.hi.md CHANGED
@@ -559,6 +559,7 @@ Shows:
559
559
  - ✅ **Screen सत्र**: कमांड डिटैच्ड screen सत्रों में चलते हैं
560
560
  - ✅ **Live Terminal Watch**: `/terminal_watch` और opt-in auto-start live session logs दिखाते हैं
561
561
  - ✅ **चैट प्रतिबंध**: अनुमत चैट ID की वैकल्पिक सफेद सूची
562
+ - ✅ **Private Auth Check**: allowlisted chat owners के लिए experimental `/auth --status <gh|claude|codex>` और `/auth --login <gh|claude|codex>`
562
563
  - ✅ **डायग्नोस्टिक टूल**: चैट ID और कॉन्फ़िगरेशन जानकारी प्राप्त करें
563
564
 
564
565
  #### Live Terminal Watch
@@ -577,6 +578,8 @@ sessions के लिए अपने आप एक अलग live terminal wat
577
578
 
578
579
  - केवल उन ग्रुप चैट में काम करता है जहाँ बॉट एडमिन है
579
580
  - `TELEGRAM_ALLOWED_CHATS` के माध्यम से वैकल्पिक चैट ID प्रतिबंध
581
+ - private `/auth` तब disabled रहता है जब `TELEGRAM_ALLOWED_CHATS` set नहीं है,
582
+ और इसे केवल listed chats के owners इस्तेमाल कर सकते हैं
580
583
  - बॉट चलाने वाले सिस्टम उपयोगकर्ता के रूप में कमांड चलते हैं
581
584
  - उचित प्रमाणीकरण सुनिश्चित करें (`gh auth login`, `claude-profiles`)
582
585
 
package/README.md CHANGED
@@ -580,6 +580,7 @@ Shows:
580
580
  - ✅ **Screen Sessions**: Commands run in detached screen sessions
581
581
  - ✅ **Live Terminal Watch**: `/terminal_watch` and opt-in auto-start show live session logs
582
582
  - ✅ **Chat Restrictions**: Optional whitelist of allowed chat IDs
583
+ - ✅ **Private Auth Check**: Experimental `/auth --status <gh|claude|codex>` and `/auth --login <gh|claude|codex>` for owners of allowlisted chats
583
584
  - ✅ **Diagnostic Tools**: Get chat ID and configuration info
584
585
 
585
586
  #### Live Terminal Watch
@@ -597,6 +598,8 @@ When enabled with `--auto-start-screen-watch-message`, the bot automatically sta
597
598
 
598
599
  - Only works in group chats where the bot is admin
599
600
  - Optional chat ID restrictions via `TELEGRAM_ALLOWED_CHATS`
601
+ - Private `/auth` is disabled unless `TELEGRAM_ALLOWED_CHATS` is set and only
602
+ owners of listed chats can use it
600
603
  - Commands run as the system user running the bot
601
604
  - Ensure proper authentication (`gh auth login`, `claude-profiles`)
602
605
 
package/README.ru.md CHANGED
@@ -561,6 +561,7 @@ Shows:
561
561
  - ✅ **Screen-сессии**: команды запускаются в отсоединённых screen-сессиях
562
562
  - ✅ **Live Terminal Watch**: `/terminal_watch` и opt-in auto-start показывают live session logs
563
563
  - ✅ **Ограничения по чатам**: опциональный белый список разрешённых ID чатов
564
+ - ✅ **Приватная проверка auth**: экспериментальные `/auth --status <gh|claude|codex>` и `/auth --login <gh|claude|codex>` для владельцев разрешённых чатов
564
565
  - ✅ **Диагностические инструменты**: получение ID чата и информации о конфигурации
565
566
 
566
567
  #### Live Terminal Watch
@@ -579,6 +580,8 @@ Shows:
579
580
 
580
581
  - Работает только в групповых чатах, где бот является администратором
581
582
  - Опциональное ограничение по ID чата через `TELEGRAM_ALLOWED_CHATS`
583
+ - Приватная `/auth` отключена, если `TELEGRAM_ALLOWED_CHATS` не задан, и
584
+ доступна только владельцам перечисленных чатов
582
585
  - Команды выполняются от имени системного пользователя, запустившего бота
583
586
  - Убедитесь в наличии надлежащей аутентификации (`gh auth login`, `claude-profiles`)
584
587
 
package/README.zh.md CHANGED
@@ -555,6 +555,7 @@ Shows:
555
555
  - ✅ **Screen 会话**:命令在后台 Screen 会话中运行
556
556
  - ✅ **Live Terminal Watch**:`/terminal_watch` 和 opt-in auto-start 显示 live session logs
557
557
  - ✅ **聊天限制**:可选配置允许的聊天 ID 白名单
558
+ - ✅ **私聊 Auth 检查**:为白名单聊天所有者提供实验性的 `/auth --status <gh|claude|codex>` 和 `/auth --login <gh|claude|codex>`
558
559
  - ✅ **诊断工具**:获取聊天 ID 和配置信息
559
560
 
560
561
  #### Live Terminal Watch
@@ -573,6 +574,7 @@ Shows:
573
574
 
574
575
  - 仅在机器人为管理员的群聊中有效
575
576
  - 可通过 `TELEGRAM_ALLOWED_CHATS` 配置可选的聊天 ID 限制
577
+ - 如果未设置 `TELEGRAM_ALLOWED_CHATS`,私聊 `/auth` 会被禁用,且只有所列聊天的所有者可以使用
576
578
  - 命令以运行机器人的系统用户身份执行
577
579
  - 请确保已完成正确的身份验证(`gh auth login`、`claude-profiles`)
578
580
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.74.6",
3
+ "version": "1.74.7",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -0,0 +1,298 @@
1
+ import { spawn } from 'child_process';
2
+ import { parseCommandArgs } from './telegram-solve-command.lib.mjs';
3
+
4
+ export const AUTH_PROVIDERS = Object.freeze(['gh', 'claude', 'codex']);
5
+
6
+ const AUTH_PROVIDER_SET = new Set(AUTH_PROVIDERS);
7
+ const AUTH_USAGE = 'Usage: /auth --status <gh|claude|codex> or /auth --login <gh|claude|codex>';
8
+ // eslint-disable-next-line no-control-regex
9
+ const ANSI_RE = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
10
+ const TOKEN_RE = /\b(?:gh[opsu]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk-proj-[A-Za-z0-9_-]{20,}|sk-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{20,})\b/g;
11
+ const TOKEN_FIELD_RE = /\b(token|access_token|refresh_token|api[_-]?key|authorization)\s*[:=]\s*["']?[^"'\s,}]+/gi;
12
+
13
+ function trimOutput(text, max = 3500) {
14
+ const value = String(text || '').trim();
15
+ if (value.length <= max) return value;
16
+ return value.slice(0, max) + `\n... truncated ${value.length - max} characters`;
17
+ }
18
+
19
+ function escapeCodeFence(text) {
20
+ return String(text || '').replace(/```/g, '` ` `');
21
+ }
22
+
23
+ function normalizeProvider(provider) {
24
+ return String(provider || '')
25
+ .trim()
26
+ .toLowerCase();
27
+ }
28
+
29
+ function readActionValue(arg) {
30
+ if (arg === '--status') return { action: 'status', provider: null, consumesNext: true };
31
+ if (arg === '--login') return { action: 'login', provider: null, consumesNext: true };
32
+ if (arg.startsWith('--status=')) return { action: 'status', provider: arg.slice('--status='.length), consumesNext: false };
33
+ if (arg.startsWith('--login=')) return { action: 'login', provider: arg.slice('--login='.length), consumesNext: false };
34
+ return null;
35
+ }
36
+
37
+ export function parseAuthRequest(text) {
38
+ const args = parseCommandArgs(text || '');
39
+ let action = null;
40
+ let provider = null;
41
+
42
+ for (let i = 0; i < args.length; i++) {
43
+ const parsed = readActionValue(args[i]);
44
+ if (!parsed) {
45
+ return { action: null, provider: null, error: `Unsupported /auth argument: ${args[i]}\n\n${AUTH_USAGE}` };
46
+ }
47
+ if (action) {
48
+ return { action: null, provider: null, error: `Use exactly one of --status or --login.\n\n${AUTH_USAGE}` };
49
+ }
50
+ action = parsed.action;
51
+ provider = normalizeProvider(parsed.provider);
52
+ if (parsed.consumesNext) {
53
+ const next = args[i + 1];
54
+ if (!next || next.startsWith('--')) {
55
+ return { action: null, provider: null, error: AUTH_USAGE };
56
+ }
57
+ provider = normalizeProvider(next);
58
+ i++;
59
+ }
60
+ }
61
+
62
+ if (!action || !provider) {
63
+ return { action: null, provider: null, error: AUTH_USAGE };
64
+ }
65
+ if (!AUTH_PROVIDER_SET.has(provider)) {
66
+ return { action, provider: null, error: `Unsupported auth provider: ${provider}\n\n${AUTH_USAGE}` };
67
+ }
68
+
69
+ return { action, provider, error: null };
70
+ }
71
+
72
+ export function buildAuthCommand(action, provider) {
73
+ if (action === 'status') {
74
+ if (provider === 'gh') return { command: 'gh', args: ['auth', 'status', '--hostname', 'github.com'] };
75
+ if (provider === 'claude') return { command: 'claude', args: ['auth', 'status'] };
76
+ if (provider === 'codex') return { command: 'codex', args: ['login', 'status'] };
77
+ }
78
+ if (action === 'login') {
79
+ if (provider === 'gh') return { command: 'gh', args: ['auth', 'login', '--hostname', 'github.com', '--git-protocol', 'https', '--web'] };
80
+ if (provider === 'claude') return { command: 'claude', args: ['auth', 'login', '--claudeai'] };
81
+ if (provider === 'codex') return { command: 'codex', args: ['login', '--device-auth'] };
82
+ }
83
+ throw new Error(`Unsupported auth command: ${action} ${provider}`);
84
+ }
85
+
86
+ export function redactAuthOutput(output) {
87
+ return String(output || '')
88
+ .replace(ANSI_RE, '')
89
+ .replace(TOKEN_RE, '[REDACTED_TOKEN]')
90
+ .replace(TOKEN_FIELD_RE, (_, name) => `${name}: [REDACTED_TOKEN]`);
91
+ }
92
+
93
+ function collectAuthOutput(result) {
94
+ return redactAuthOutput([result?.stdout, result?.stderr].filter(Boolean).join('\n'));
95
+ }
96
+
97
+ export function extractAuthStartDetails(output) {
98
+ const text = redactAuthOutput(output);
99
+ const urls = [...new Set([...text.matchAll(/https?:\/\/[^\s<>)"']+/g)].map(match => match[0].replace(/[.,;:!?]+$/, '')))];
100
+
101
+ const codePatterns = [/\bone-time code\s*[:=]\s*([A-Z0-9][A-Z0-9-]{3,})/i, /\b(?:user code|verification code|code)\s*[:=]\s*([A-Z0-9][A-Z0-9-]{3,})/i, /\b([A-Z0-9]{4,}-[A-Z0-9-]{4,})\b/];
102
+ let code = null;
103
+ for (const pattern of codePatterns) {
104
+ const match = text.match(pattern);
105
+ if (match) {
106
+ code = match[1].toUpperCase();
107
+ break;
108
+ }
109
+ }
110
+
111
+ return { urls, code };
112
+ }
113
+
114
+ export function formatAuthStatusMessage(provider, result) {
115
+ const code = result?.code;
116
+ const ok = code === 0;
117
+ const output = trimOutput(collectAuthOutput(result)) || '(no output)';
118
+ return `${ok ? 'OK' : 'ERROR'} *${provider} auth status*\n\nExit code: ${code ?? 'unknown'}\n\n\`\`\`\n${escapeCodeFence(output)}\n\`\`\``;
119
+ }
120
+
121
+ export function formatAuthLoginMessage(provider, result) {
122
+ const output = collectAuthOutput(result);
123
+ const details = extractAuthStartDetails(output);
124
+ const lines = [`*${provider} auth login started*`, '', 'The local login command was cancelled locally after capturing the browser step, so this bot command did not replace existing credentials.'];
125
+
126
+ if (details.urls.length > 0) {
127
+ lines.push('', 'Open this URL:');
128
+ for (const url of details.urls) lines.push(url);
129
+ }
130
+ if (details.code) {
131
+ lines.push('', `Code: \`${details.code}\``);
132
+ }
133
+ if (details.urls.length === 0 && !details.code) {
134
+ const shownOutput = trimOutput(output) || '(no output captured)';
135
+ lines.push('', 'Captured output:', '', '```', escapeCodeFence(shownOutput), '```');
136
+ }
137
+ if (result?.cancelled) {
138
+ lines.push('', 'Status: cancelled locally after capture.');
139
+ } else if (typeof result?.code === 'number') {
140
+ lines.push('', `Status: login command exited with code ${result.code}.`);
141
+ }
142
+ lines.push('', 'Continuation by replying with a provider code is not automated yet; this is the first experimental CLI-backed /auth path.');
143
+ return lines.join('\n');
144
+ }
145
+
146
+ export const resolveAllowedAuthChatIds = allowedChats => {
147
+ if (!allowedChats) return [];
148
+ const raw = typeof allowedChats === 'function' ? allowedChats() : allowedChats;
149
+ if (!Array.isArray(raw)) return [];
150
+ return raw.map(value => String(value)).filter(Boolean);
151
+ };
152
+
153
+ export async function isAuthOperator({ telegram, userId, allowedChatIds }) {
154
+ if (!telegram || !userId || !allowedChatIds || allowedChatIds.length === 0) {
155
+ return false;
156
+ }
157
+ for (const chatId of allowedChatIds) {
158
+ if (String(chatId) === String(userId)) return true;
159
+ try {
160
+ const member = await telegram.getChatMember(chatId, userId);
161
+ if (member?.status === 'creator') return true;
162
+ } catch {
163
+ // Try the next configured chat. The bot may no longer be a member.
164
+ }
165
+ }
166
+ return false;
167
+ }
168
+
169
+ export function runAuthCommand(command, args, options = {}) {
170
+ const { mode = 'status', loginCaptureMs = 15000, outputLimit = 20000, env = process.env } = options;
171
+ return new Promise(resolve => {
172
+ const child = spawn(command, args, {
173
+ stdio: ['ignore', 'pipe', 'pipe'],
174
+ env,
175
+ });
176
+ let stdout = '';
177
+ let stderr = '';
178
+ let settled = false;
179
+ let captureTimer = null;
180
+
181
+ const settle = result => {
182
+ if (settled) return;
183
+ settled = true;
184
+ if (captureTimer) clearTimeout(captureTimer);
185
+ resolve({
186
+ stdout: stdout.slice(0, outputLimit),
187
+ stderr: stderr.slice(0, outputLimit),
188
+ ...result,
189
+ });
190
+ };
191
+
192
+ const maybeCancelLogin = () => {
193
+ if (mode !== 'login' || settled) return;
194
+ const details = extractAuthStartDetails(`${stdout}\n${stderr}`);
195
+ if (details.urls.length === 0 && !details.code) return;
196
+ child.kill('SIGTERM');
197
+ settle({ code: null, signal: 'SIGTERM', cancelled: true });
198
+ };
199
+
200
+ child.stdout.on('data', data => {
201
+ stdout += data.toString();
202
+ maybeCancelLogin();
203
+ });
204
+ child.stderr.on('data', data => {
205
+ stderr += data.toString();
206
+ maybeCancelLogin();
207
+ });
208
+ child.on('error', error => {
209
+ settle({ code: null, error: error.message });
210
+ });
211
+ child.on('close', (code, signal) => {
212
+ settle({ code, signal, cancelled: false });
213
+ });
214
+
215
+ if (mode === 'login') {
216
+ captureTimer = setTimeout(() => {
217
+ child.kill('SIGTERM');
218
+ settle({ code: null, signal: 'SIGTERM', cancelled: true });
219
+ }, loginCaptureMs);
220
+ }
221
+ });
222
+ }
223
+
224
+ export function registerAuthCommand(bot, options = {}) {
225
+ const { VERBOSE = false, isOldMessage, isForwardedOrReply, allowedChats, authEnabled = true } = options;
226
+ const execute = options.runCommand || runAuthCommand;
227
+ const reply = options.safeReply || ((ctx, text, replyOptions) => ctx.reply(text, replyOptions));
228
+
229
+ async function handleAuthCommand(ctx) {
230
+ VERBOSE && console.log('[VERBOSE] /auth command received');
231
+
232
+ if (isOldMessage && isOldMessage(ctx)) {
233
+ VERBOSE && console.log('[VERBOSE] /auth ignored: old message');
234
+ return;
235
+ }
236
+ if (isForwardedOrReply && isForwardedOrReply(ctx)) {
237
+ VERBOSE && console.log('[VERBOSE] /auth ignored: forwarded or reply');
238
+ return;
239
+ }
240
+ if (!authEnabled) {
241
+ await reply(ctx, 'The /auth command is disabled on this bot instance.', { reply_to_message_id: ctx.message?.message_id });
242
+ return;
243
+ }
244
+ if (!ctx.chat || !ctx.from || !ctx.message) return;
245
+ if (ctx.chat.type !== 'private') {
246
+ await reply(ctx, 'The /auth command is only available in private messages.', { reply_to_message_id: ctx.message.message_id });
247
+ return;
248
+ }
249
+
250
+ const allowedChatIds = resolveAllowedAuthChatIds(allowedChats);
251
+ if (allowedChatIds.length === 0) {
252
+ await reply(ctx, 'The /auth command is disabled because TELEGRAM_ALLOWED_CHATS is not configured.', { reply_to_message_id: ctx.message.message_id });
253
+ return;
254
+ }
255
+
256
+ const authorized = await isAuthOperator({ telegram: ctx.telegram, userId: ctx.from.id, allowedChatIds });
257
+ if (!authorized) {
258
+ VERBOSE && console.log(`[VERBOSE] /auth denied: user ${ctx.from.id} is not creator of any allowed chat`);
259
+ await reply(ctx, 'The /auth command is only available to owners of allowlisted chats.', { reply_to_message_id: ctx.message.message_id });
260
+ return;
261
+ }
262
+
263
+ const request = parseAuthRequest(ctx.message.text || '');
264
+ if (request.error) {
265
+ await reply(ctx, request.error, { reply_to_message_id: ctx.message.message_id });
266
+ return;
267
+ }
268
+
269
+ const { command, args } = buildAuthCommand(request.action, request.provider);
270
+ let result;
271
+ try {
272
+ result = await execute(command, args, { mode: request.action, provider: request.provider });
273
+ } catch (error) {
274
+ await reply(ctx, `Failed to run ${request.provider} auth ${request.action}: ${error.message || String(error)}`, { reply_to_message_id: ctx.message.message_id });
275
+ return;
276
+ }
277
+
278
+ const message = request.action === 'status' ? formatAuthStatusMessage(request.provider, result) : formatAuthLoginMessage(request.provider, result);
279
+ await reply(ctx, message, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
280
+ }
281
+
282
+ bot.command('auth', handleAuthCommand);
283
+ return { handleAuthCommand };
284
+ }
285
+
286
+ export default {
287
+ AUTH_PROVIDERS,
288
+ buildAuthCommand,
289
+ extractAuthStartDetails,
290
+ formatAuthLoginMessage,
291
+ formatAuthStatusMessage,
292
+ isAuthOperator,
293
+ parseAuthRequest,
294
+ redactAuthOutput,
295
+ registerAuthCommand,
296
+ resolveAllowedAuthChatIds,
297
+ runAuthCommand,
298
+ };
@@ -100,6 +100,11 @@ const config = yargs(hideBin(process.argv))
100
100
  description: 'Enable /task and /split commands (use --no-task to disable)',
101
101
  default: getenv('TELEGRAM_TASK', 'true') !== 'false',
102
102
  })
103
+ .option('auth', {
104
+ type: 'boolean',
105
+ description: 'Enable experimental private /auth command for allowlisted chat owners (use --no-auth to disable)',
106
+ default: getenv('TELEGRAM_AUTH', 'true') !== 'false',
107
+ })
103
108
  .option('dryRun', {
104
109
  type: 'boolean',
105
110
  description: 'Validate configuration and options without starting the bot',
@@ -163,6 +168,7 @@ const hiveOverrides = resolvedHiveOverrides
163
168
  const solveEnabled = config.solve;
164
169
  const hiveEnabled = config.hive;
165
170
  const taskEnabled = config.task;
171
+ const authEnabled = config.auth;
166
172
  // Isolation mode (experimental): uses `$` from start-command with specified backend
167
173
  const ISOLATION_BACKEND = (config.isolation || getenv('TELEGRAM_ISOLATION', '')).trim().toLowerCase();
168
174
  let isolationRunner = null;
@@ -283,7 +289,7 @@ if (config.dryRun) {
283
289
  if (allowedTopics && allowedTopics.length > 0) {
284
290
  console.log(' Allowed topics:', lino.formatLinks(allowedTopics));
285
291
  }
286
- console.log(' Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled });
292
+ console.log(' Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled, auth: authEnabled });
287
293
  if (solveOverrides.length > 0) {
288
294
  console.log(' Solve overrides:', lino.format(solveOverrides));
289
295
  }
@@ -606,6 +612,8 @@ const { registerSubscribeCommands } = await import('./telegram-subscribers.lib.m
606
612
  registerSubscribeCommands(bot, sharedCommandOpts);
607
613
  const { registerTaskCommands } = await import('./telegram-task-command.lib.mjs');
608
614
  const { handleTaskCommand, TASK_COMMAND_NAMES } = registerTaskCommands(bot, { ...sharedCommandOpts, taskEnabled, safeReply, executeAndUpdateMessage, resolveLocale: resolveLocaleFromTelegramCtx });
615
+ const { registerAuthCommand } = await import('./telegram-auth-command.lib.mjs');
616
+ const { handleAuthCommand } = registerAuthCommand(bot, { ...sharedCommandOpts, allowedChats, authEnabled, safeReply });
609
617
 
610
618
  // Named handler for /solve command - extracted for reuse by text-based fallback (issue #1207)
611
619
  async function handleSolveCommand(ctx) {
@@ -1170,7 +1178,7 @@ bot.on('message', async (ctx, next) => {
1170
1178
  const solveHandlers = Object.fromEntries(SOLVE_COMMAND_NAMES.map(command => [command, handleSolveCommand]));
1171
1179
  const taskHandlers = Object.fromEntries(TASK_COMMAND_NAMES.map(command => [command, handleTaskCommand]));
1172
1180
  // /queue is the short alias for /solve_queue (issue #1837)
1173
- const handlers = { ...solveHandlers, ...taskHandlers, hive: handleHiveCommand, solve_queue: handleSolveQueueCommand, solvequeue: handleSolveQueueCommand, queue: handleSolveQueueCommand };
1181
+ const handlers = { ...solveHandlers, ...taskHandlers, auth: handleAuthCommand, hive: handleHiveCommand, solve_queue: handleSolveQueueCommand, solvequeue: handleSolveQueueCommand, queue: handleSolveQueueCommand };
1174
1182
 
1175
1183
  const handler = handlers[extracted.command];
1176
1184
  if (!handler) return next();
@@ -1279,7 +1287,7 @@ if (allowedChats && allowedChats.length > 0) {
1279
1287
  if (allowedTopics && allowedTopics.length > 0) {
1280
1288
  console.log('Allowed topics (lino):', lino.formatLinks(allowedTopics));
1281
1289
  }
1282
- console.log('Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled });
1290
+ console.log('Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled, auth: authEnabled });
1283
1291
  if (solveOverrides.length > 0) console.log('Solve overrides (lino):', lino.format(solveOverrides));
1284
1292
  if (hiveOverrides.length > 0) console.log('Hive overrides (lino):', lino.format(hiveOverrides));
1285
1293
  if (VERBOSE) {