@link-assistant/hive-mind 1.59.7 → 1.60.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,133 @@
1
+ import { buildUserMention } from './buildUserMention.lib.mjs';
2
+ import { validateModelName } from './models/index.mjs';
3
+ import { parseTaskIssueUrl } from './task.split.lib.mjs';
4
+ import { escapeMarkdown } from './telegram-markdown.lib.mjs';
5
+ import { extractIsolationFromArgs, isValidPerCommandIsolation } from './telegram-isolation.lib.mjs';
6
+ import { moveArgumentToFront, parseCommandArgs } from './telegram-solve-command.lib.mjs';
7
+ import { formatStartingWorkSessionMessage } from './work-session-formatting.lib.mjs';
8
+
9
+ export const TASK_COMMAND_NAMES = Object.freeze(['task', 'split']);
10
+
11
+ export function getTaskCommandNameFromText(text) {
12
+ if (!text || typeof text !== 'string') return null;
13
+ const firstLine = text.split('\n')[0].trim();
14
+ const match = firstLine.match(/^\/(\w+)(?:@\S+)?(?:\s|$)/);
15
+ const command = match ? match[1].toLowerCase() : null;
16
+ return TASK_COMMAND_NAMES.includes(command) ? command : null;
17
+ }
18
+
19
+ export function applyTaskCommandDefaults(args) {
20
+ const hasSplit = args.includes('--split') || args.some(arg => arg.startsWith('--split='));
21
+ return hasSplit ? args : [...args, '--split'];
22
+ }
23
+
24
+ export function findTaskIssueUrl(args) {
25
+ return args.find(arg => !arg.startsWith('-') && parseTaskIssueUrl(arg).valid) || null;
26
+ }
27
+
28
+ export function getTaskToolFromArgs(args) {
29
+ for (let i = 0; i < args.length; i++) {
30
+ if (args[i] === '--tool' && i + 1 < args.length) return args[i + 1];
31
+ if (args[i].startsWith('--tool=')) return args[i].substring('--tool='.length);
32
+ }
33
+ return 'claude';
34
+ }
35
+
36
+ function getModelFromArgs(args) {
37
+ for (let i = 0; i < args.length; i++) {
38
+ if ((args[i] === '--model' || args[i] === '-m') && i + 1 < args.length) return args[i + 1];
39
+ if (args[i].startsWith('--model=')) return args[i].substring('--model='.length);
40
+ }
41
+ return null;
42
+ }
43
+
44
+ function validateTaskModel(args) {
45
+ const model = getModelFromArgs(args);
46
+ if (!model) return null;
47
+ const validation = validateModelName(model, getTaskToolFromArgs(args));
48
+ return validation.valid ? null : validation.message;
49
+ }
50
+
51
+ export function buildTaskCommandArgs(text) {
52
+ const args = applyTaskCommandDefaults(parseCommandArgs(text));
53
+ const issueUrl = findTaskIssueUrl(args);
54
+ return {
55
+ args: issueUrl ? moveArgumentToFront(args, issueUrl) : args,
56
+ issueUrl,
57
+ };
58
+ }
59
+
60
+ export function registerTaskCommands(bot, options) {
61
+ const { VERBOSE, taskEnabled, addBreadcrumb, isOldMessage, isGroupChat, isTopicAuthorized, buildAuthErrorMessage, isChatStopped, getStoppedChatRejectMessage, safeReply, executeAndUpdateMessage } = options;
62
+
63
+ async function handleTaskCommand(ctx) {
64
+ const commandName = getTaskCommandNameFromText(ctx.message?.text) || 'task';
65
+ const commandDisplay = `/${commandName}`;
66
+ VERBOSE && console.log(`[VERBOSE] ${commandDisplay} command received`);
67
+
68
+ await addBreadcrumb({
69
+ category: 'telegram.command',
70
+ message: `${commandDisplay} command received`,
71
+ level: 'info',
72
+ data: { chatId: ctx.chat?.id, chatType: ctx.chat?.type, userId: ctx.from?.id, username: ctx.from?.username },
73
+ });
74
+
75
+ if (!taskEnabled) {
76
+ await ctx.reply('❌ The task command is disabled on this bot instance.');
77
+ return;
78
+ }
79
+ if (isOldMessage(ctx)) return;
80
+ if (!isGroupChat(ctx)) {
81
+ 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 });
82
+ return;
83
+ }
84
+ if (!isTopicAuthorized(ctx)) {
85
+ await ctx.reply(buildAuthErrorMessage(ctx), { reply_to_message_id: ctx.message.message_id });
86
+ return;
87
+ }
88
+ if (isChatStopped(ctx.chat.id)) {
89
+ await safeReply(ctx, getStoppedChatRejectMessage(ctx.chat.id, 'Task'), { reply_to_message_id: ctx.message.message_id });
90
+ return;
91
+ }
92
+
93
+ const built = buildTaskCommandArgs(ctx.message.text);
94
+ if (!built.issueUrl) {
95
+ await safeReply(ctx, `❌ Missing GitHub issue URL. Usage: \`${commandDisplay} <github-issue-url> [options]\`\n\nExample: \`${commandDisplay} https://github.com/owner/repo/issues/123\``, { reply_to_message_id: ctx.message.message_id });
96
+ return;
97
+ }
98
+
99
+ const parsedIssue = parseTaskIssueUrl(built.issueUrl);
100
+ if (!parsedIssue.valid) {
101
+ await safeReply(ctx, `❌ ${escapeMarkdown(parsedIssue.error || 'Invalid GitHub issue URL')}`, { reply_to_message_id: ctx.message.message_id });
102
+ return;
103
+ }
104
+
105
+ const { backend: perCommandIsolation, filteredArgs } = extractIsolationFromArgs(built.args);
106
+ if (perCommandIsolation && !isValidPerCommandIsolation(perCommandIsolation)) {
107
+ await safeReply(ctx, `❌ Invalid --isolation value '${escapeMarkdown(perCommandIsolation)}'. Must be: screen, tmux, or docker`, { reply_to_message_id: ctx.message.message_id });
108
+ return;
109
+ }
110
+
111
+ const modelError = validateTaskModel(filteredArgs);
112
+ if (modelError) {
113
+ await safeReply(ctx, `❌ ${escapeMarkdown(modelError)}`, { reply_to_message_id: ctx.message.message_id });
114
+ return;
115
+ }
116
+
117
+ const requester = buildUserMention({ user: ctx.from, parseMode: 'Markdown' });
118
+ const userOptionsRaw = built.args.slice(1).join(' ');
119
+ let infoBlock = `Requested by: ${requester}\nIssue: ${escapeMarkdown(built.issueUrl)}`;
120
+ if (userOptionsRaw) infoBlock += `\n\n🛠 Options: ${escapeMarkdown(userOptionsRaw)}`;
121
+
122
+ const taskUrlContext = { owner: parsedIssue.owner, repo: parsedIssue.repo, number: parsedIssue.number, type: parsedIssue.type, normalized: parsedIssue.normalized || built.issueUrl };
123
+ const startingMessage = await safeReply(ctx, formatStartingWorkSessionMessage({ infoBlock }), { reply_to_message_id: ctx.message.message_id });
124
+ await executeAndUpdateMessage(ctx, startingMessage, 'task', filteredArgs, infoBlock, perCommandIsolation || null, getTaskToolFromArgs(filteredArgs), taskUrlContext);
125
+ }
126
+
127
+ bot.command(
128
+ TASK_COMMAND_NAMES.map(command => new RegExp(`^${command}$`, 'i')),
129
+ handleTaskCommand
130
+ );
131
+
132
+ return { handleTaskCommand, TASK_COMMAND_NAMES };
133
+ }