@link-assistant/hive-mind 1.60.0 → 1.62.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.
@@ -525,7 +525,7 @@ bot.command('help', async ctx => {
525
525
  message += '📝 *Available Commands:*\n\n';
526
526
 
527
527
  if (solveEnabled) {
528
- message += '*/solve* (aliases: */do*, */continue*, */claude*, */codex*, */opencode*, */agent*) - Solve a GitHub issue\n';
528
+ message += '*/solve* (aliases: */do*, */continue*, */claude*, */codex*, */opencode*, */agent*, */qwen*) - Solve a GitHub issue\n';
529
529
  message += 'Usage: `/solve <github-url> [options]`\n';
530
530
  message += 'Example: `/solve https://github.com/owner/repo/issues/123 --model sonnet`\n';
531
531
  message += 'Tool aliases imply `--tool <tool>`: `/codex <github-url>` equals `/solve <github-url> --tool codex`\n';
@@ -535,12 +535,15 @@ bot.command('help', async ctx => {
535
535
  }
536
536
  message += '\n';
537
537
  } else {
538
- message += '*/solve* (aliases: */do*, */continue*, */claude*, */codex*, */opencode*, */agent*) - ❌ Disabled\n\n';
538
+ message += '*/solve* (aliases: */do*, */continue*, */claude*, */codex*, */opencode*, */agent*, */qwen*) - ❌ Disabled\n\n';
539
539
  }
540
540
 
541
541
  if (taskEnabled) {
542
- message += '*/task* / */split* - Split a GitHub issue into smaller issues\n';
543
- message += 'Usage: `/split <github-issue-url> [options]`\n';
542
+ message += '*/task* - Create a GitHub issue from a repository link and issue text\n';
543
+ message += 'Usage: `/task <github-repository-url>` followed by issue text, or reply with `/task`\n';
544
+ message += 'Example: `/task https://github.com/owner/repo` then the issue text on following lines\n';
545
+ message += '*/split* - Split a GitHub issue into smaller issues\n';
546
+ message += 'Usage: `/split <github-issue-url> [options]` or `/task --split <github-issue-url>`\n';
544
547
  message += 'Example: `/split https://github.com/owner/repo/issues/123 --split-count 2`\n\n';
545
548
  } else {
546
549
  message += '*/task* / */split* - ❌ Disabled\n\n';
@@ -573,7 +576,7 @@ bot.command('help', async ctx => {
573
576
  message += '🔔 *Session Notifications:* Completion notifications are automatic; use /subscribe for private DM forwards.\n';
574
577
  if (ISOLATION_BACKEND) message += `🔒 *Isolation Mode:* \`${ISOLATION_BACKEND}\` (experimental)\n`;
575
578
  message += '\n';
576
- message += '⚠️ *Note:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /task, /split, /hive, /solve\\_queue, /limits, /version, /accept\\_invites, /merge, /stop and /start commands only work in group chats. /terminal\\_watch, /subscribe and /unsubscribe work in private and group chats.\n\n';
579
+ message += '⚠️ *Note:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /qwen, /task, /split, /hive, /solve\\_queue, /limits, /version, /accept\\_invites, /merge, /stop and /start commands only work in group chats. /terminal\\_watch, /subscribe and /unsubscribe work in private and group chats.\n\n';
577
580
  message += '🔧 *Common Options:*\n';
578
581
  message += `• \`--model <model>\` or \`-m\` - ${buildModelOptionDescription()}\n`;
579
582
  message += '• `--base-branch <branch>` or `-b` - Target branch for PR (default: repo default branch)\n';
@@ -13,6 +13,7 @@ export const TOOL_SOLVE_COMMAND_ALIASES = Object.freeze({
13
13
  codex: 'codex',
14
14
  opencode: 'opencode',
15
15
  agent: 'agent',
16
+ qwen: 'qwen',
16
17
  });
17
18
 
18
19
  export const SOLVE_COMMAND_NAMES = Object.freeze(['solve', 'do', 'continue', ...Object.keys(TOOL_SOLVE_COMMAND_ALIASES)]);
@@ -75,6 +75,15 @@ export async function getRunningCodexProcesses(verbose = false) {
75
75
  return getRunningProcesses('codex', verbose);
76
76
  }
77
77
 
78
+ /**
79
+ * Count running qwen processes.
80
+ * @param {boolean} verbose - Whether to log verbose output
81
+ * @returns {Promise<{count: number, processes: string[]}>}
82
+ */
83
+ export async function getRunningQwenProcesses(verbose = false) {
84
+ return getRunningProcesses('qwen', verbose);
85
+ }
86
+
78
87
  /**
79
88
  * Format a threshold as percentage for display.
80
89
  * @param {number} ratio - Ratio (0.0 - 1.0)
@@ -145,6 +154,8 @@ export function formatWaitingReason(metric, currentValue, threshold) {
145
154
  return 'Claude process is already running';
146
155
  case 'codex_running':
147
156
  return 'Codex process is already running';
157
+ case 'qwen_running':
158
+ return 'Qwen Code process is already running';
148
159
  default:
149
160
  return `${metric} threshold exceeded`;
150
161
  }
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import { getCachedClaudeLimits, getCachedCodexLimits, getCachedGitHubLimits, getCachedMemoryInfo, getCachedCpuInfo, getCachedDiskInfo, getLimitCache } from './limits.lib.mjs';
19
- export { formatDuration, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningCodexProcesses, getRunningProcesses } from './telegram-solve-queue.helpers.lib.mjs';
19
+ export { formatDuration, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningCodexProcesses, getRunningProcesses, getRunningQwenProcesses } from './telegram-solve-queue.helpers.lib.mjs';
20
20
  import { formatDuration, formatWaitingReason, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningProcesses } from './telegram-solve-queue.helpers.lib.mjs';
21
21
  export { QUEUE_CONFIG, THRESHOLD_STRATEGIES } from './queue-config.lib.mjs';
22
22
  import { QUEUE_CONFIG } from './queue-config.lib.mjs';
@@ -149,6 +149,7 @@ export class SolveQueue {
149
149
  claude: [],
150
150
  agent: [],
151
151
  codex: [],
152
+ qwen: [],
152
153
  };
153
154
  this.processing = new Map();
154
155
  this.completed = [];
@@ -160,6 +161,7 @@ export class SolveQueue {
160
161
  claude: null,
161
162
  agent: null,
162
163
  codex: null,
164
+ qwen: null,
163
165
  };
164
166
  // Legacy: keep for compatibility with existing code that uses lastStartTime
165
167
  this.lastStartTime = null;
@@ -561,8 +563,10 @@ export class SolveQueue {
561
563
  const claudeProcessCount = externalProcessing.byTool.claude || 0;
562
564
  const codexProcessCount = externalProcessing.byTool.codex || 0;
563
565
  const agentProcessCount = externalProcessing.byTool.agent || 0;
566
+ const qwenProcessCount = externalProcessing.byTool.qwen || 0;
564
567
  const hasRunningClaude = claudeProcessCount > 0;
565
568
  const hasRunningCodex = codexProcessCount > 0;
569
+ const hasRunningQwen = qwenProcessCount > 0;
566
570
 
567
571
  // Calculate total processing count for system resources (all tools)
568
572
  // System resources (RAM, CPU, disk) apply to all tools
@@ -574,6 +578,7 @@ export class SolveQueue {
574
578
  // See: https://github.com/link-assistant/hive-mind/issues/1159
575
579
  const claudeProcessingCount = this.getProcessingCountByTool('claude');
576
580
  const codexProcessingCount = this.getProcessingCountByTool('codex');
581
+ const qwenProcessingCount = this.getProcessingCountByTool('qwen');
577
582
 
578
583
  // Track claude_running as a metric (but don't add to reasons yet)
579
584
  if (hasRunningClaude) {
@@ -582,6 +587,9 @@ export class SolveQueue {
582
587
  if (hasRunningCodex) {
583
588
  this.recordThrottle('codex_running');
584
589
  }
590
+ if (hasRunningQwen) {
591
+ this.recordThrottle('qwen_running');
592
+ }
585
593
 
586
594
  // Check system resources with strategy support
587
595
  // System resources apply to ALL tools, not just Claude
@@ -604,8 +612,8 @@ export class SolveQueue {
604
612
  // This allows agent tasks to proceed when Claude limits are reached
605
613
  // See: https://github.com/link-assistant/hive-mind/issues/1159
606
614
  // See: https://github.com/link-assistant/hive-mind/issues/1253 (strategies)
607
- const hasRunningToolProcess = tool === 'codex' ? hasRunningCodex : hasRunningClaude;
608
- const toolProcessingCount = tool === 'codex' ? codexProcessingCount : claudeProcessingCount;
615
+ const hasRunningToolProcess = tool === 'codex' ? hasRunningCodex : tool === 'qwen' ? hasRunningQwen : hasRunningClaude;
616
+ const toolProcessingCount = tool === 'codex' ? codexProcessingCount : tool === 'qwen' ? qwenProcessingCount : claudeProcessingCount;
609
617
  const limitCheck = await this.checkApiLimits(hasRunningToolProcess, toolProcessingCount, tool);
610
618
  if (limitCheck.rejected) {
611
619
  rejected = true;
@@ -629,6 +637,9 @@ export class SolveQueue {
629
637
  if (tool === 'codex' && hasRunningCodex && reasons.length > 0) {
630
638
  reasons.push(formatWaitingReason('codex_running', codexProcessCount, 0) + ` (${codexProcessCount} processes)`);
631
639
  }
640
+ if (tool === 'qwen' && hasRunningQwen && reasons.length > 0) {
641
+ reasons.push(formatWaitingReason('qwen_running', qwenProcessCount, 0) + ` (${qwenProcessCount} processes)`);
642
+ }
632
643
 
633
644
  const canStart = reasons.length === 0 && !rejected;
634
645
 
@@ -650,10 +661,12 @@ export class SolveQueue {
650
661
  claudeProcesses: claudeProcessCount,
651
662
  codexProcesses: codexProcessCount,
652
663
  agentProcesses: agentProcessCount,
664
+ qwenProcesses: qwenProcessCount,
653
665
  isolatedProcesses: externalProcessing.isolatedTotal,
654
666
  totalProcessing,
655
667
  claudeProcessingCount,
656
668
  codexProcessingCount,
669
+ qwenProcessingCount,
657
670
  };
658
671
  }
659
672
 
@@ -1,5 +1,6 @@
1
1
  import { buildUserMention } from './buildUserMention.lib.mjs';
2
2
  import { validateModelName } from './models/index.mjs';
3
+ import { createTaskIssue, parseTaskIssueCreationInput, resolveTaskIssueCreationInput } from './task.issue-creation.lib.mjs';
3
4
  import { parseTaskIssueUrl } from './task.split.lib.mjs';
4
5
  import { escapeMarkdown } from './telegram-markdown.lib.mjs';
5
6
  import { extractIsolationFromArgs, isValidPerCommandIsolation } from './telegram-isolation.lib.mjs';
@@ -16,7 +17,12 @@ export function getTaskCommandNameFromText(text) {
16
17
  return TASK_COMMAND_NAMES.includes(command) ? command : null;
17
18
  }
18
19
 
19
- export function applyTaskCommandDefaults(args) {
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;
20
26
  const hasSplit = args.includes('--split') || args.some(arg => arg.startsWith('--split='));
21
27
  return hasSplit ? args : [...args, '--split'];
22
28
  }
@@ -49,7 +55,8 @@ function validateTaskModel(args) {
49
55
  }
50
56
 
51
57
  export function buildTaskCommandArgs(text) {
52
- const args = applyTaskCommandDefaults(parseCommandArgs(text));
58
+ const commandName = getTaskCommandNameFromText(text) || 'task';
59
+ const args = applyTaskCommandDefaults(parseCommandArgs(text), commandName);
53
60
  const issueUrl = findTaskIssueUrl(args);
54
61
  return {
55
62
  args: issueUrl ? moveArgumentToFront(args, issueUrl) : args,
@@ -57,8 +64,26 @@ export function buildTaskCommandArgs(text) {
57
64
  };
58
65
  }
59
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
+
60
85
  export function registerTaskCommands(bot, options) {
61
- const { VERBOSE, taskEnabled, addBreadcrumb, isOldMessage, isGroupChat, isTopicAuthorized, buildAuthErrorMessage, isChatStopped, getStoppedChatRejectMessage, safeReply, executeAndUpdateMessage } = options;
86
+ const { VERBOSE, taskEnabled, addBreadcrumb, isOldMessage, isGroupChat, isTopicAuthorized, buildAuthErrorMessage, isChatStopped, getStoppedChatRejectMessage, safeReply, executeAndUpdateMessage, createTaskIssue: createTaskIssueFn = createTaskIssue } = options;
62
87
 
63
88
  async function handleTaskCommand(ctx) {
64
89
  const commandName = getTaskCommandNameFromText(ctx.message?.text) || 'task';
@@ -90,6 +115,39 @@ export function registerTaskCommands(bot, options) {
90
115
  return;
91
116
  }
92
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
+
93
151
  const built = buildTaskCommandArgs(ctx.message.text);
94
152
  if (!built.issueUrl) {
95
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 });
@@ -0,0 +1,16 @@
1
+ export const validateToolConnection = async ({ tool = 'claude', model, verbose = false, validateClaudeConnection } = {}) => {
2
+ if (tool === 'opencode') {
3
+ return (await import('./opencode.lib.mjs')).validateOpenCodeConnection(model);
4
+ }
5
+ if (tool === 'codex') {
6
+ return (await import('./codex.lib.mjs')).validateCodexConnection(model, verbose);
7
+ }
8
+ if (tool === 'agent') {
9
+ return (await import('./agent.lib.mjs')).validateAgentConnection(model);
10
+ }
11
+ if (tool === 'qwen') {
12
+ return (await import('./qwen.lib.mjs')).validateQwenConnection(model);
13
+ }
14
+ const validateClaude = validateClaudeConnection || (await import('./claude.lib.mjs')).validateClaudeConnection;
15
+ return validateClaude(model);
16
+ };
@@ -554,7 +554,7 @@ const VERSION_COMMANDS = [
554
554
  { key: 'agent', command: 'agent --version 2>&1' },
555
555
  { key: 'codex', command: 'codex --version 2>&1' },
556
556
  { key: 'opencode', command: 'opencode --version 2>&1' },
557
- { key: 'qwenCode', command: 'qwen-code --version 2>&1' },
557
+ { key: 'qwenCode', command: 'qwen --version 2>&1' },
558
558
  { key: 'gemini', command: 'gemini --version 2>&1' },
559
559
  { key: 'copilot', command: 'copilot --version 2>&1' },
560
560