@link-assistant/hive-mind 2.7.4 → 2.8.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,164 @@
1
+ /**
2
+ * Telegram `/fix` command (issue #1733).
3
+ *
4
+ * Spawns the `fix` CLI in a work session, exactly like `/solve` and `/task` do
5
+ * for their own CLIs. `fix` creates the CI/CD remediation issue and then hands
6
+ * it off to `/solve --development-log --deep-analysis --auto-merge` itself, so
7
+ * this handler only has to validate the request and start the session.
8
+ */
9
+
10
+ import { buildUserMention } from './buildUserMention.lib.mjs';
11
+ import { validateModelName } from './models/index.mjs';
12
+ import { parseFixRepository } from './fix.ci-cd.lib.mjs';
13
+ import { escapeMarkdown } from './telegram-markdown.lib.mjs';
14
+ import { extractIsolationFromArgs, isValidPerCommandIsolation } from './telegram-isolation.lib.mjs';
15
+ import { moveArgumentToFront, parseCommandArgs } from './telegram-solve-command.lib.mjs';
16
+ import { formatStartingWorkSessionMessage } from './work-session-formatting.lib.mjs';
17
+
18
+ export const FIX_COMMAND_NAMES = Object.freeze(['fix']);
19
+
20
+ export function getFixCommandNameFromText(text) {
21
+ if (!text || typeof text !== 'string') return null;
22
+ const firstLine = text.split('\n')[0].trim();
23
+ const match = firstLine.match(/^\/(\w+)(?:@\S+)?(?:\s|$)/);
24
+ const command = match ? match[1].toLowerCase() : null;
25
+ return FIX_COMMAND_NAMES.includes(command) ? command : null;
26
+ }
27
+
28
+ /**
29
+ * `--ci-cd` is the only mode `fix` supports today and it is required by the
30
+ * CLI, so the chat command implies it instead of making every user type it.
31
+ */
32
+ export function applyFixCommandDefaults(args) {
33
+ const hasCiCd = args.includes('--ci-cd');
34
+ return hasCiCd ? args : [...args, '--ci-cd'];
35
+ }
36
+
37
+ export function findFixRepositoryArg(args) {
38
+ return args.find(arg => !arg.startsWith('-') && parseFixRepository(arg)) || null;
39
+ }
40
+
41
+ export function getFixToolFromArgs(args) {
42
+ for (let i = 0; i < args.length; i++) {
43
+ if (args[i] === '--tool' && i + 1 < args.length) return args[i + 1];
44
+ if (args[i].startsWith('--tool=')) return args[i].substring('--tool='.length);
45
+ }
46
+ return 'claude';
47
+ }
48
+
49
+ function getModelFromArgs(args) {
50
+ for (let i = 0; i < args.length; i++) {
51
+ if ((args[i] === '--model' || args[i] === '-m') && i + 1 < args.length) return args[i + 1];
52
+ if (args[i].startsWith('--model=')) return args[i].substring('--model='.length);
53
+ }
54
+ return null;
55
+ }
56
+
57
+ function validateFixModel(args) {
58
+ const model = getModelFromArgs(args);
59
+ if (!model) return null;
60
+ const validation = validateModelName(model, getFixToolFromArgs(args));
61
+ return validation.valid ? null : validation.message;
62
+ }
63
+
64
+ export function buildFixCommandArgs(text) {
65
+ const args = applyFixCommandDefaults(parseCommandArgs(text));
66
+ const repositoryRaw = findFixRepositoryArg(args);
67
+ const repository = repositoryRaw ? parseFixRepository(repositoryRaw) : null;
68
+ return {
69
+ // `fix` reads the repository from the first bare argument, so normalize the
70
+ // shorthand (`owner/repo`) to a full URL and move it to the front.
71
+ args: repository ? moveArgumentToFront(args, repository.url, value => parseFixRepository(value)?.url || value) : args,
72
+ repositoryRaw,
73
+ repository,
74
+ };
75
+ }
76
+
77
+ // Issue #378: inject --language LOCALE into spawn args if no language flag is
78
+ // already present, so spawned fix sessions inherit the user's effective locale.
79
+ function injectLanguageIfMissing(args, locale) {
80
+ if (!locale || !args || !Array.isArray(args)) return args;
81
+ const langFlags = new Set(['--language', '--ui-language', '--work-language']);
82
+ for (const arg of args) {
83
+ const flag = arg.startsWith('--') ? arg.split('=')[0] : null;
84
+ if (flag && langFlags.has(flag)) return args;
85
+ }
86
+ return [...args, '--language', locale];
87
+ }
88
+
89
+ export function registerFixCommand(bot, options) {
90
+ const { VERBOSE, fixEnabled, addBreadcrumb, isOldMessage, isForwardedOrReply, isGroupChat, isTopicAuthorized, buildAuthErrorMessage, isChatStopped, getStoppedChatRejectMessage, safeReply, executeAndUpdateMessage, resolveLocale = null } = options;
91
+
92
+ async function handleFixCommand(ctx) {
93
+ const commandDisplay = '/fix';
94
+ VERBOSE && console.log(`[VERBOSE] ${commandDisplay} command received`);
95
+
96
+ await addBreadcrumb({
97
+ category: 'telegram.command',
98
+ message: `${commandDisplay} command received`,
99
+ level: 'info',
100
+ data: { chatId: ctx.chat?.id, chatType: ctx.chat?.type, userId: ctx.from?.id, username: ctx.from?.username },
101
+ });
102
+
103
+ if (!fixEnabled) {
104
+ await ctx.reply('āŒ The fix command is disabled on this bot instance.');
105
+ return;
106
+ }
107
+ if (isOldMessage(ctx)) return;
108
+ // Issue #1922: a forwarded or replied-to /fix command must never be
109
+ // re-executed. Unlike /task, /fix takes no input from the replied message,
110
+ // so both cases are ignored.
111
+ if (isForwardedOrReply && isForwardedOrReply(ctx)) {
112
+ VERBOSE && console.log(`[VERBOSE] ${commandDisplay} ignored: forwarded or reply message`);
113
+ return;
114
+ }
115
+ if (!isGroupChat(ctx)) {
116
+ await ctx.reply(`āŒ The ${commandDisplay} 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 });
117
+ return;
118
+ }
119
+ if (!isTopicAuthorized(ctx)) {
120
+ await ctx.reply(buildAuthErrorMessage(ctx), { reply_to_message_id: ctx.message.message_id });
121
+ return;
122
+ }
123
+ if (isChatStopped(ctx.chat.id)) {
124
+ await safeReply(ctx, getStoppedChatRejectMessage(ctx.chat.id, 'Fix'), { reply_to_message_id: ctx.message.message_id });
125
+ return;
126
+ }
127
+
128
+ const built = buildFixCommandArgs(ctx.message.text);
129
+ if (!built.repository) {
130
+ await safeReply(ctx, `āŒ Missing GitHub repository URL. Usage: \`${commandDisplay} <github-repository-url> [options]\`\n\nExample: \`${commandDisplay} https://github.com/owner/repo\``, { reply_to_message_id: ctx.message.message_id });
131
+ return;
132
+ }
133
+
134
+ const { backend: perCommandIsolation, filteredArgs } = extractIsolationFromArgs(built.args);
135
+ if (perCommandIsolation && !isValidPerCommandIsolation(perCommandIsolation)) {
136
+ await safeReply(ctx, `āŒ Invalid --isolation value '${escapeMarkdown(perCommandIsolation)}'. Must be: screen, tmux, or docker`, { reply_to_message_id: ctx.message.message_id });
137
+ return;
138
+ }
139
+
140
+ const modelError = validateFixModel(filteredArgs);
141
+ if (modelError) {
142
+ await safeReply(ctx, `āŒ ${escapeMarkdown(modelError)}`, { reply_to_message_id: ctx.message.message_id });
143
+ return;
144
+ }
145
+
146
+ const requester = buildUserMention({ user: ctx.from, parseMode: 'Markdown' });
147
+ const userOptionsRaw = built.args.slice(1).join(' ');
148
+ let infoBlock = `Requested by: ${requester}\nRepository: ${escapeMarkdown(built.repository.url)}`;
149
+ if (userOptionsRaw) infoBlock += `\n\nšŸ›  Options: ${escapeMarkdown(userOptionsRaw)}`;
150
+
151
+ const fixUrlContext = { owner: built.repository.owner, repo: built.repository.repo, normalized: built.repository.url };
152
+ const startingMessage = await safeReply(ctx, formatStartingWorkSessionMessage({ infoBlock }), { reply_to_message_id: ctx.message.message_id });
153
+ const fixLocale = resolveLocale ? resolveLocale(ctx) : null;
154
+ const argsForExec = injectLanguageIfMissing(filteredArgs, fixLocale);
155
+ await executeAndUpdateMessage(ctx, startingMessage, 'fix', argsForExec, infoBlock, perCommandIsolation || null, getFixToolFromArgs(argsForExec), fixUrlContext);
156
+ }
157
+
158
+ bot.command(
159
+ FIX_COMMAND_NAMES.map(command => new RegExp(`^${command}$`, 'i')),
160
+ handleFixCommand
161
+ );
162
+
163
+ return { handleFixCommand, FIX_COMMAND_NAMES };
164
+ }
@@ -23,7 +23,7 @@ export function buildSolveQueuedMessage({ locale = null, tool = 'claude', positi
23
23
  return message;
24
24
  }
25
25
 
26
- export function buildTelegramHelpMessage({ locale = null, chatId, chatType = '', chatTitle = '', topicId = null, isStopped = false, stopInfo = null, stopReason = '', solveEnabled = true, taskEnabled = true, hiveEnabled = true, solveOverrides = [], hiveOverrides = [], showLimitsEnabled = false, isolationBackend = null, modelDescription = '', restrictedMode = false, authorized = null, allowTopicHint = '' } = {}) {
26
+ export function buildTelegramHelpMessage({ locale = null, chatId, chatType = '', chatTitle = '', topicId = null, isStopped = false, stopInfo = null, stopReason = '', solveEnabled = true, taskEnabled = true, fixEnabled = true, hiveEnabled = true, solveOverrides = [], hiveOverrides = [], showLimitsEnabled = false, isolationBackend = null, modelDescription = '', restrictedMode = false, authorized = null, allowTopicHint = '' } = {}) {
27
27
  const message = [];
28
28
  addLine(message, 'telegram.help_title', {}, locale);
29
29
  message.push('');
@@ -71,6 +71,16 @@ export function buildTelegramHelpMessage({ locale = null, chatId, chatType = '',
71
71
  message.push('');
72
72
  }
73
73
 
74
+ if (fixEnabled) {
75
+ addLine(message, 'telegram.help_fix_enabled', {}, locale);
76
+ addLine(message, 'telegram.help_fix_usage', {}, locale);
77
+ addLine(message, 'telegram.help_fix_example', {}, locale);
78
+ message.push('');
79
+ } else {
80
+ addLine(message, 'telegram.help_fix_disabled', {}, locale);
81
+ message.push('');
82
+ }
83
+
74
84
  if (hiveEnabled) {
75
85
  addLine(message, 'telegram.help_hive_enabled', {}, locale);
76
86
  addLine(message, 'telegram.help_hive_usage', {}, locale);
@@ -0,0 +1,96 @@
1
+ // CLI configuration module for the telegram-bot command.
2
+ // Extracted from telegram-bot.mjs so the option surface can be reviewed, tested
3
+ // and reused without loading the bot itself.
4
+ //
5
+ // Defaults read the environment through getenv() when createYargsConfig() runs,
6
+ // so callers must load .env/.lenv configuration before calling it.
7
+
8
+ import { getenv } from './cli-arguments.lib.mjs';
9
+
10
+ export const createYargsConfig = yargsInstance =>
11
+ yargsInstance
12
+ .usage('Usage: hive-telegram-bot [options]')
13
+ .option('configuration', {
14
+ type: 'string',
15
+ description: 'LINO configuration string for environment variables',
16
+ alias: 'c',
17
+ default: getenv('TELEGRAM_CONFIGURATION', ''),
18
+ })
19
+ .option('token', {
20
+ type: 'string',
21
+ description: 'Telegram bot token from @BotFather',
22
+ alias: 't',
23
+ default: getenv('TELEGRAM_BOT_TOKEN', ''),
24
+ })
25
+ .option('allowedChats', {
26
+ type: 'string',
27
+ description: 'Allowed chat IDs in lino notation, e.g., "(\n 123456789\n 987654321\n)"',
28
+ alias: 'allowed-chats',
29
+ default: getenv('TELEGRAM_ALLOWED_CHATS', ''),
30
+ })
31
+ .option('allowedTopics', {
32
+ type: 'string',
33
+ description: 'Allowed topic IDs in Links Notation format "chatId topicId" pairs',
34
+ alias: 'allowed-topics',
35
+ default: getenv('TELEGRAM_ALLOWED_TOPICS', ''),
36
+ })
37
+ .option('solveOverrides', {
38
+ type: 'string',
39
+ description: 'Override options for /solve command in lino notation, e.g., "(\n --auto-continue\n --attach-logs\n)"',
40
+ alias: 'solve-overrides',
41
+ default: getenv('TELEGRAM_SOLVE_OVERRIDES', ''),
42
+ })
43
+ .option('hiveOverrides', {
44
+ type: 'string',
45
+ description: 'Override options for /hive command in lino notation, e.g., "(\n --verbose\n --all-issues\n)"',
46
+ alias: 'hive-overrides',
47
+ default: getenv('TELEGRAM_HIVE_OVERRIDES', ''),
48
+ })
49
+ .option('solve', {
50
+ type: 'boolean',
51
+ description: 'Enable /solve command (use --no-solve to disable)',
52
+ default: getenv('TELEGRAM_SOLVE', 'true') !== 'false',
53
+ })
54
+ .option('hive', {
55
+ type: 'boolean',
56
+ description: 'Enable /hive command (use --no-hive to disable)',
57
+ default: getenv('TELEGRAM_HIVE', 'true') !== 'false',
58
+ })
59
+ .option('task', {
60
+ type: 'boolean',
61
+ description: 'Enable /task and /split commands (use --no-task to disable)',
62
+ default: getenv('TELEGRAM_TASK', 'true') !== 'false',
63
+ })
64
+ .option('fix', {
65
+ type: 'boolean',
66
+ description: 'Enable /fix command (use --no-fix to disable)',
67
+ default: getenv('TELEGRAM_FIX', 'true') !== 'false',
68
+ })
69
+ .option('auth', {
70
+ type: 'boolean',
71
+ description: 'Enable experimental private /auth command for allowlisted chat owners (use --no-auth to disable)',
72
+ default: getenv('TELEGRAM_AUTH', 'true') !== 'false',
73
+ })
74
+ .option('dryRun', {
75
+ type: 'boolean',
76
+ description: 'Validate configuration and options without starting the bot',
77
+ alias: 'dry-run',
78
+ default: false,
79
+ })
80
+ .option('verbose', {
81
+ type: 'boolean',
82
+ description: 'Enable verbose logging for debugging',
83
+ alias: 'v',
84
+ default: getenv('TELEGRAM_BOT_VERBOSE', 'false') === 'true',
85
+ })
86
+ .option('autoStartScreenWatchMessage', { type: 'boolean', description: 'Experimental: auto-start separate /terminal_watch messages for public /solve sessions', alias: 'auto-start-screen-watch-message', default: getenv('TELEGRAM_AUTO_START_SCREEN_WATCH_MESSAGE', getenv('TELEGRAM_AUTO_WATCH_MESSAGE', 'false')) === 'true' })
87
+ // Issue #594: bot-owner toggle for --show-limits virtual option in /solve and /hive.
88
+ .option('showLimits', { type: 'boolean', description: 'Experimental: allow /solve and /hive callers to use --show-limits to embed Claude/Codex usage at start, end, and delta in the completion message', alias: 'show-limits', default: getenv('TELEGRAM_SHOW_LIMITS', 'true') !== 'false' })
89
+ .option('isolation', { type: 'string', description: "Isolation backend (screen/tmux/docker). Defaults to 'docker' so Telegram-bot work sessions run in Docker isolation; pass --isolation '' (or set TELEGRAM_ISOLATION='') to disable.", default: getenv('TELEGRAM_ISOLATION', 'docker') })
90
+ .help('h')
91
+ .alias('h', 'help')
92
+ .parserConfiguration({
93
+ 'boolean-negation': true,
94
+ 'strip-dashed': true, // Remove dashed keys from argv to simplify validation
95
+ })
96
+ .strict(); // Enable strict mode to reject unknown options (consistent with solve.mjs and hive.mjs)