@link-assistant/hive-mind 2.17.0 → 2.19.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.
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Telegram /models command implementation (issue #2202, R5).
3
+ *
4
+ * Shows the merged model catalogue for a tool: what this installation ships,
5
+ * what the live sources are serving right now, and which of the two a given
6
+ * model is in. Every source it reads is a listing endpoint that cannot bill a
7
+ * token (R7), and the answer is cached for an hour (R9), so asking often is
8
+ * free.
9
+ *
10
+ * Usage in chat:
11
+ * /models -> the default tool (claude)
12
+ * /models --tool codex -> one specific tool
13
+ * /models --all -> every tool, one message each
14
+ * /models --details -> add context window and pricing (R8)
15
+ * /models --refresh -> ignore the cache and re-read the sources
16
+ * /models --no-update -> skip the CLI version check (R6)
17
+ *
18
+ * @see https://github.com/link-assistant/hive-mind/issues/2202
19
+ */
20
+
21
+ import { ensureAgenticCliFreshness, describeFreshnessResult } from './agentic-cli-freshness.lib.mjs';
22
+ import { MODEL_CATALOGUE_TOOLS, getMergedModelCatalogue } from './model-catalogue.lib.mjs';
23
+ import { formatModelCatalogueTelegram } from './model-catalogue-render.lib.mjs';
24
+ import { safeReply as defaultSafeReply } from './telegram-safe-reply.lib.mjs';
25
+
26
+ const GROUP_ONLY_MESSAGE = '❌ The /models command only works in group chats. Please add this bot to a group and make it an admin.';
27
+
28
+ /** The tool answered when the operator names none — the one Hive Mind drives by default. */
29
+ export const DEFAULT_MODELS_COMMAND_TOOL = 'claude';
30
+
31
+ /**
32
+ * Parse the argument tail of a `/models` message.
33
+ *
34
+ * Deliberately forgiving: a chat is not a shell, so `--tool codex`,
35
+ * `--tool=codex`, and a bare `codex` all mean the same thing. Anything it
36
+ * cannot make sense of comes back as `error` so the handler can say so instead
37
+ * of silently answering a different question.
38
+ */
39
+ export const parseModelsCommandArgs = (text = '') => {
40
+ const result = { tools: [], all: false, refresh: false, details: false, update: true, error: null };
41
+ const tokens = String(text).trim().split(/\s+/).slice(1).filter(Boolean);
42
+
43
+ for (let index = 0; index < tokens.length; index += 1) {
44
+ const token = tokens[index];
45
+ const lower = token.toLowerCase();
46
+ if (lower === '--all' || lower === 'all') {
47
+ result.all = true;
48
+ continue;
49
+ }
50
+ if (lower === '--refresh' || lower === 'refresh') {
51
+ result.refresh = true;
52
+ continue;
53
+ }
54
+ if (lower === '--details' || lower === '--detail' || lower === 'details') {
55
+ result.details = true;
56
+ continue;
57
+ }
58
+ if (lower === '--no-update' || lower === '--no-tool-update') {
59
+ result.update = false;
60
+ continue;
61
+ }
62
+ let value;
63
+ if (lower.startsWith('--tool=')) value = lower.slice('--tool='.length);
64
+ else if (lower === '--tool' || lower === '-t') value = (tokens[++index] ?? '').toLowerCase();
65
+ else if (!lower.startsWith('-')) value = lower;
66
+ else {
67
+ result.error = `Unknown option: ${token}`;
68
+ return result;
69
+ }
70
+
71
+ for (const entry of value.split(',').filter(Boolean)) {
72
+ if (!MODEL_CATALOGUE_TOOLS.includes(entry)) {
73
+ result.error = `Unknown tool: ${entry}. Known tools: ${MODEL_CATALOGUE_TOOLS.join(', ')}`;
74
+ return result;
75
+ }
76
+ if (!result.tools.includes(entry)) result.tools.push(entry);
77
+ }
78
+ }
79
+
80
+ if (result.all) result.tools = [...MODEL_CATALOGUE_TOOLS];
81
+ if (result.tools.length === 0) result.tools = [DEFAULT_MODELS_COMMAND_TOOL];
82
+ return result;
83
+ };
84
+
85
+ /**
86
+ * Registers the /models command handler with the bot.
87
+ *
88
+ * @param {Object} bot Telegraf bot instance
89
+ * @param {Object} options the shared command options every telegram command takes
90
+ * @returns {{ handleModelsCommand: Function }} the handler, for the text fallback
91
+ */
92
+ export function registerModelsCommand(bot, options = {}) {
93
+ const { VERBOSE = false, isOldMessage, isForwardedOrReply, isGroupChat, isChatAuthorized, isTopicAuthorized, buildAuthErrorMessage, addBreadcrumb, safeReply, loadCatalogue = getMergedModelCatalogue, freshness = ensureAgenticCliFreshness, env = process.env } = options;
94
+
95
+ async function handleModelsCommand(ctx) {
96
+ VERBOSE && console.log('[VERBOSE] /models command received');
97
+
98
+ if (addBreadcrumb) {
99
+ await addBreadcrumb({
100
+ category: 'telegram.command',
101
+ message: '/models command received',
102
+ level: 'info',
103
+ data: { chatId: ctx.chat?.id, chatType: ctx.chat?.type, userId: ctx.from?.id, username: ctx.from?.username },
104
+ });
105
+ }
106
+
107
+ const reply = (text, replyOptions = {}) => (safeReply || defaultSafeReply)(ctx, text, { reply_to_message_id: ctx.message?.message_id, ...replyOptions });
108
+
109
+ if (isOldMessage?.(ctx)) {
110
+ VERBOSE && console.log('[VERBOSE] /models ignored: old message');
111
+ return;
112
+ }
113
+ if (isForwardedOrReply?.(ctx)) {
114
+ VERBOSE && console.log('[VERBOSE] /models ignored: forwarded or reply');
115
+ return;
116
+ }
117
+ if (isGroupChat && !isGroupChat(ctx)) {
118
+ VERBOSE && console.log('[VERBOSE] /models ignored: not a group chat');
119
+ await reply(GROUP_ONLY_MESSAGE);
120
+ return;
121
+ }
122
+ const authorize = isTopicAuthorized || (isChatAuthorized ? context => isChatAuthorized(context.chat.id) : () => true);
123
+ if (!authorize(ctx)) {
124
+ VERBOSE && console.log('[VERBOSE] /models ignored: not authorized');
125
+ await reply(buildAuthErrorMessage ? buildAuthErrorMessage(ctx) : `❌ This chat (ID: ${ctx.chat.id}) is not authorized.`);
126
+ return;
127
+ }
128
+
129
+ const args = parseModelsCommandArgs(ctx.message?.text ?? '');
130
+ if (args.error) {
131
+ await reply(`❌ ${args.error}`);
132
+ return;
133
+ }
134
+
135
+ // R6: give the agentic CLIs a chance to update before we describe what they
136
+ // can run. Best-effort — a failed refresh must not cost the operator their
137
+ // answer, so the outcome is reported and then ignored.
138
+ const refreshed = await freshness({ tools: args.tools, env, enabled: args.update, verbose: VERBOSE, log: async message => VERBOSE && console.log(`[VERBOSE] /models ${message}`) });
139
+ const freshnessLine = describeFreshnessResult(refreshed);
140
+ if (freshnessLine) await reply(freshnessLine);
141
+
142
+ for (const tool of args.tools) {
143
+ try {
144
+ const merged = await loadCatalogue({ tool, env, refresh: args.refresh });
145
+ await reply(formatModelCatalogueTelegram(merged, { details: args.details }));
146
+ } catch (error) {
147
+ await reply(`⚠️ Could not build the ${tool} catalogue: ${error?.message ?? error}`);
148
+ }
149
+ }
150
+ }
151
+
152
+ bot.command(/^models$/i, handleModelsCommand);
153
+
154
+ return { handleModelsCommand };
155
+ }
156
+
157
+ export default { DEFAULT_MODELS_COMMAND_TOOL, parseModelsCommandArgs, registerModelsCommand };
@@ -92,7 +92,7 @@ export function buildTelegramHelpMessage({ locale = null, chatId, chatType = '',
92
92
  message.push('');
93
93
  }
94
94
 
95
- const simpleCommandKeys = ['telegram.help_solve_queue', 'telegram.help_limits', 'telegram.help_version', 'telegram.help_language', 'telegram.help_accept_invites', 'telegram.help_merge', 'telegram.help_merge_usage', 'telegram.help_merge_description', 'telegram.help_subscribe', 'telegram.help_help', 'telegram.help_stop_start', 'telegram.help_stop_uuid', 'telegram.help_log', 'telegram.help_terminal_watch'];
95
+ const simpleCommandKeys = ['telegram.help_solve_queue', 'telegram.help_models', 'telegram.help_limits', 'telegram.help_version', 'telegram.help_language', 'telegram.help_accept_invites', 'telegram.help_merge', 'telegram.help_merge_usage', 'telegram.help_merge_description', 'telegram.help_subscribe', 'telegram.help_help', 'telegram.help_stop_start', 'telegram.help_stop_uuid', 'telegram.help_log', 'telegram.help_terminal_watch'];
96
96
  for (const key of simpleCommandKeys) addLine(message, key, {}, locale);
97
97
  message.push('');
98
98
  addLine(message, 'telegram.help_notifications', {}, locale);