@link-assistant/hive-mind 1.59.7 → 1.61.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,191 @@
1
+ import { buildUserMention } from './buildUserMention.lib.mjs';
2
+ import { validateModelName } from './models/index.mjs';
3
+ import { createTaskIssue, parseTaskIssueCreationInput, resolveTaskIssueCreationInput } from './task.issue-creation.lib.mjs';
4
+ import { parseTaskIssueUrl } from './task.split.lib.mjs';
5
+ import { escapeMarkdown } from './telegram-markdown.lib.mjs';
6
+ import { extractIsolationFromArgs, isValidPerCommandIsolation } from './telegram-isolation.lib.mjs';
7
+ import { moveArgumentToFront, parseCommandArgs } from './telegram-solve-command.lib.mjs';
8
+ import { formatStartingWorkSessionMessage } from './work-session-formatting.lib.mjs';
9
+
10
+ export const TASK_COMMAND_NAMES = Object.freeze(['task', 'split']);
11
+
12
+ export function getTaskCommandNameFromText(text) {
13
+ if (!text || typeof text !== 'string') return null;
14
+ const firstLine = text.split('\n')[0].trim();
15
+ const match = firstLine.match(/^\/(\w+)(?:@\S+)?(?:\s|$)/);
16
+ const command = match ? match[1].toLowerCase() : null;
17
+ return TASK_COMMAND_NAMES.includes(command) ? command : null;
18
+ }
19
+
20
+ export function hasTaskSplitFlag(args) {
21
+ return args.includes('--split') || args.some(arg => arg.startsWith('--split='));
22
+ }
23
+
24
+ export function applyTaskCommandDefaults(args, commandName = 'task') {
25
+ if (commandName !== 'split') return args;
26
+ const hasSplit = args.includes('--split') || args.some(arg => arg.startsWith('--split='));
27
+ return hasSplit ? args : [...args, '--split'];
28
+ }
29
+
30
+ export function findTaskIssueUrl(args) {
31
+ return args.find(arg => !arg.startsWith('-') && parseTaskIssueUrl(arg).valid) || null;
32
+ }
33
+
34
+ export function getTaskToolFromArgs(args) {
35
+ for (let i = 0; i < args.length; i++) {
36
+ if (args[i] === '--tool' && i + 1 < args.length) return args[i + 1];
37
+ if (args[i].startsWith('--tool=')) return args[i].substring('--tool='.length);
38
+ }
39
+ return 'claude';
40
+ }
41
+
42
+ function getModelFromArgs(args) {
43
+ for (let i = 0; i < args.length; i++) {
44
+ if ((args[i] === '--model' || args[i] === '-m') && i + 1 < args.length) return args[i + 1];
45
+ if (args[i].startsWith('--model=')) return args[i].substring('--model='.length);
46
+ }
47
+ return null;
48
+ }
49
+
50
+ function validateTaskModel(args) {
51
+ const model = getModelFromArgs(args);
52
+ if (!model) return null;
53
+ const validation = validateModelName(model, getTaskToolFromArgs(args));
54
+ return validation.valid ? null : validation.message;
55
+ }
56
+
57
+ export function buildTaskCommandArgs(text) {
58
+ const commandName = getTaskCommandNameFromText(text) || 'task';
59
+ const args = applyTaskCommandDefaults(parseCommandArgs(text), commandName);
60
+ const issueUrl = findTaskIssueUrl(args);
61
+ return {
62
+ args: issueUrl ? moveArgumentToFront(args, issueUrl) : args,
63
+ issueUrl,
64
+ };
65
+ }
66
+
67
+ function getReplyText(message) {
68
+ const reply = message?.reply_to_message;
69
+ if (!reply || reply.forum_topic_created) return '';
70
+ return reply.text || reply.caption || '';
71
+ }
72
+
73
+ function buildTaskIssueCreationUsage(commandDisplay) {
74
+ return [`Usage: ${commandDisplay} <github-repository-url> followed by issue text.`, '', `Or reply to a message containing a repository URL and issue text with \`${commandDisplay}\`.`, '', 'To split an existing issue, use `/split <github-issue-url>` or `/task --split <github-issue-url>`.'].join('\n');
75
+ }
76
+
77
+ async function editTelegramMessage(ctx, message, text) {
78
+ try {
79
+ await ctx.telegram.editMessageText(message.chat.id, message.message_id, undefined, text, { disable_web_page_preview: true });
80
+ } catch (error) {
81
+ console.error(`[telegram-task-command] Failed to edit status message: ${error.message}`);
82
+ }
83
+ }
84
+
85
+ export function registerTaskCommands(bot, options) {
86
+ const { VERBOSE, taskEnabled, addBreadcrumb, isOldMessage, isGroupChat, isTopicAuthorized, buildAuthErrorMessage, isChatStopped, getStoppedChatRejectMessage, safeReply, executeAndUpdateMessage, createTaskIssue: createTaskIssueFn = createTaskIssue } = options;
87
+
88
+ async function handleTaskCommand(ctx) {
89
+ const commandName = getTaskCommandNameFromText(ctx.message?.text) || 'task';
90
+ const commandDisplay = `/${commandName}`;
91
+ VERBOSE && console.log(`[VERBOSE] ${commandDisplay} command received`);
92
+
93
+ await addBreadcrumb({
94
+ category: 'telegram.command',
95
+ message: `${commandDisplay} command received`,
96
+ level: 'info',
97
+ data: { chatId: ctx.chat?.id, chatType: ctx.chat?.type, userId: ctx.from?.id, username: ctx.from?.username },
98
+ });
99
+
100
+ if (!taskEnabled) {
101
+ await ctx.reply('❌ The task command is disabled on this bot instance.');
102
+ return;
103
+ }
104
+ if (isOldMessage(ctx)) return;
105
+ if (!isGroupChat(ctx)) {
106
+ 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 });
107
+ return;
108
+ }
109
+ if (!isTopicAuthorized(ctx)) {
110
+ await ctx.reply(buildAuthErrorMessage(ctx), { reply_to_message_id: ctx.message.message_id });
111
+ return;
112
+ }
113
+ if (isChatStopped(ctx.chat.id)) {
114
+ await safeReply(ctx, getStoppedChatRejectMessage(ctx.chat.id, 'Task'), { reply_to_message_id: ctx.message.message_id });
115
+ return;
116
+ }
117
+
118
+ const parsedArgs = parseCommandArgs(ctx.message.text);
119
+ const splitMode = commandName === 'split' || hasTaskSplitFlag(parsedArgs);
120
+
121
+ if (!splitMode) {
122
+ const creationInput = resolveTaskIssueCreationInput({
123
+ commandText: ctx.message.text,
124
+ replyText: getReplyText(ctx.message),
125
+ });
126
+ const creation = parseTaskIssueCreationInput(creationInput);
127
+
128
+ if (!creation.valid) {
129
+ await safeReply(ctx, `❌ ${escapeMarkdown(creation.error)}\n\n${buildTaskIssueCreationUsage(commandDisplay)}`, { reply_to_message_id: ctx.message.message_id });
130
+ return;
131
+ }
132
+
133
+ const statusMessage = await ctx.reply(`Creating GitHub issue in ${creation.repository.fullName}...`, {
134
+ reply_to_message_id: ctx.message.message_id,
135
+ disable_web_page_preview: true,
136
+ });
137
+
138
+ try {
139
+ const createdIssue = await createTaskIssueFn({
140
+ repository: creation.repository,
141
+ title: creation.title,
142
+ body: creation.issueText,
143
+ });
144
+ await editTelegramMessage(ctx, statusMessage, `Created GitHub issue:\n${createdIssue.url}\n\nReply to this message with /solve to start a solution.`);
145
+ } catch (error) {
146
+ await editTelegramMessage(ctx, statusMessage, `Error creating GitHub issue:\n${error.message || String(error)}`);
147
+ }
148
+ return;
149
+ }
150
+
151
+ const built = buildTaskCommandArgs(ctx.message.text);
152
+ if (!built.issueUrl) {
153
+ 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 });
154
+ return;
155
+ }
156
+
157
+ const parsedIssue = parseTaskIssueUrl(built.issueUrl);
158
+ if (!parsedIssue.valid) {
159
+ await safeReply(ctx, `❌ ${escapeMarkdown(parsedIssue.error || 'Invalid GitHub issue URL')}`, { reply_to_message_id: ctx.message.message_id });
160
+ return;
161
+ }
162
+
163
+ const { backend: perCommandIsolation, filteredArgs } = extractIsolationFromArgs(built.args);
164
+ if (perCommandIsolation && !isValidPerCommandIsolation(perCommandIsolation)) {
165
+ await safeReply(ctx, `❌ Invalid --isolation value '${escapeMarkdown(perCommandIsolation)}'. Must be: screen, tmux, or docker`, { reply_to_message_id: ctx.message.message_id });
166
+ return;
167
+ }
168
+
169
+ const modelError = validateTaskModel(filteredArgs);
170
+ if (modelError) {
171
+ await safeReply(ctx, `❌ ${escapeMarkdown(modelError)}`, { reply_to_message_id: ctx.message.message_id });
172
+ return;
173
+ }
174
+
175
+ const requester = buildUserMention({ user: ctx.from, parseMode: 'Markdown' });
176
+ const userOptionsRaw = built.args.slice(1).join(' ');
177
+ let infoBlock = `Requested by: ${requester}\nIssue: ${escapeMarkdown(built.issueUrl)}`;
178
+ if (userOptionsRaw) infoBlock += `\n\n🛠 Options: ${escapeMarkdown(userOptionsRaw)}`;
179
+
180
+ const taskUrlContext = { owner: parsedIssue.owner, repo: parsedIssue.repo, number: parsedIssue.number, type: parsedIssue.type, normalized: parsedIssue.normalized || built.issueUrl };
181
+ const startingMessage = await safeReply(ctx, formatStartingWorkSessionMessage({ infoBlock }), { reply_to_message_id: ctx.message.message_id });
182
+ await executeAndUpdateMessage(ctx, startingMessage, 'task', filteredArgs, infoBlock, perCommandIsolation || null, getTaskToolFromArgs(filteredArgs), taskUrlContext);
183
+ }
184
+
185
+ bot.command(
186
+ TASK_COMMAND_NAMES.map(command => new RegExp(`^${command}$`, 'i')),
187
+ handleTaskCommand
188
+ );
189
+
190
+ return { handleTaskCommand, TASK_COMMAND_NAMES };
191
+ }