@link-assistant/hive-mind 1.8.0 → 1.9.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.9.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 06da02c: Improve /accept_invites command output with grouped items and real-time updates
8
+
9
+ **Changes:**
10
+ - Group output by "Repositories:" and "Organizations:" instead of repeating "Repository:" for each item
11
+ - Add clickable GitHub links for each repository and organization
12
+ - Implement real-time message updates after each invitation is processed
13
+ - Show progress indicator (e.g., "Processing GitHub Invitations (3/10)") during processing
14
+
15
+ Fixes #1148
16
+
17
+ ## 1.9.0
18
+
19
+ ### Minor Changes
20
+
21
+ - e15f307: Add bidirectional translation between --think and --thinking-budget options for Claude Code
22
+
23
+ **Changes:**
24
+ - Add 'off' option to --think values: ['off', 'low', 'medium', 'high', 'max']
25
+ - Add --thinking-budget-claude-minimum-version option (default: 2.1.12)
26
+ - For Claude Code >= 2.1.12: translate --think to --thinking-budget (off→0, low→8000, medium→16000, high→24000, max→31999)
27
+ - For Claude Code < 2.1.12: translate --thinking-budget back to --think thinking keywords
28
+ - Both options now coexist and support all Claude Code versions
29
+
30
+ **Rationale:**
31
+ Claude Code v2.1.12+ no longer responds to thinking keywords (think, think hard, ultrathink) because extended thinking is enabled by default. The only way to control thinking budget programmatically is via MAX_THINKING_TOKENS environment variable.
32
+
33
+ Fixes #1146
34
+
3
35
  ## 1.8.0
4
36
 
5
37
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.8.0",
3
+ "version": "1.9.1",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -68,7 +68,8 @@
68
68
  "@sentry/node": "^10.15.0",
69
69
  "@sentry/profiling-node": "^10.15.0",
70
70
  "dayjs": "^1.11.19",
71
- "secretlint": "^11.2.5"
71
+ "secretlint": "^11.2.5",
72
+ "semver": "^7.7.3"
72
73
  },
73
74
  "lint-staged": {
74
75
  "*.{js,mjs,json,md}": [
@@ -10,7 +10,7 @@ const path = (await use('path')).default;
10
10
  // Import log from general lib
11
11
  import { log } from './lib.mjs';
12
12
  import { reportError } from './sentry.lib.mjs';
13
- import { timeouts, retryLimits, claudeCode, getClaudeEnv } from './config.lib.mjs';
13
+ import { timeouts, retryLimits, claudeCode, getClaudeEnv, getThinkingLevelToTokens, getTokensToThinkingLevel, supportsThinkingBudget, DEFAULT_MAX_THINKING_BUDGET } from './config.lib.mjs';
14
14
  import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
15
15
  import { createInteractiveHandler } from './interactive-mode.lib.mjs';
16
16
  import { displayBudgetStats } from './claude.budget-stats.lib.mjs';
@@ -68,6 +68,8 @@ export const validateClaudeConnection = async (model = 'haiku-3') => {
68
68
  const versionResult = await $`timeout ${Math.floor(timeouts.claudeCli / 6000)} claude --version`;
69
69
  if (versionResult.code === 0) {
70
70
  const version = versionResult.stdout?.toString().trim();
71
+ // Store the version for thinking settings translation (issue #1146)
72
+ detectedClaudeVersion = version;
71
73
  if (retryCount === 0) {
72
74
  await log(`📦 Claude CLI version: ${version}`);
73
75
  }
@@ -219,6 +221,76 @@ export const validateClaudeConnection = async (model = 'haiku-3') => {
219
221
  // handleClaudeRuntimeSwitch is imported from ./claude.runtime-switch.lib.mjs (see issue #1141)
220
222
  // Re-export it for backwards compatibility
221
223
  export { handleClaudeRuntimeSwitch };
224
+
225
+ // Store Claude Code version globally (set during validation)
226
+ let detectedClaudeVersion = null;
227
+
228
+ /**
229
+ * Get the detected Claude Code version
230
+ * @returns {string|null} The detected version or null if not yet detected
231
+ */
232
+ export const getClaudeVersion = () => detectedClaudeVersion;
233
+
234
+ /**
235
+ * Set the detected Claude Code version (called during validation)
236
+ * @param {string} version - The detected version string
237
+ */
238
+ export const setClaudeVersion = version => {
239
+ detectedClaudeVersion = version;
240
+ };
241
+
242
+ /**
243
+ * Resolve thinking settings based on --think and --thinking-budget options
244
+ * Handles translation between thinking levels and token budgets based on Claude Code version
245
+ * @param {Object} argv - Command line arguments
246
+ * @param {Function} log - Logging function
247
+ * @returns {Object} { thinkingBudget, thinkLevel, translation, maxBudget } - Resolved settings
248
+ */
249
+ export const resolveThinkingSettings = async (argv, log) => {
250
+ const minVersion = argv.thinkingBudgetClaudeMinimumVersion || '2.1.12';
251
+ const version = detectedClaudeVersion || '0.0.0'; // Assume old version if not detected
252
+ const isNewVersion = supportsThinkingBudget(version, minVersion);
253
+
254
+ // Get max thinking budget from argv or use default (see issue #1146)
255
+ const maxBudget = argv.maxThinkingBudget ?? DEFAULT_MAX_THINKING_BUDGET;
256
+
257
+ // Get thinking level mappings calculated from maxBudget
258
+ const thinkingLevelToTokens = getThinkingLevelToTokens(maxBudget);
259
+ const tokensToThinkingLevel = getTokensToThinkingLevel(maxBudget);
260
+
261
+ let thinkingBudget = argv.thinkingBudget;
262
+ let thinkLevel = argv.think;
263
+ let translation = null;
264
+
265
+ if (isNewVersion) {
266
+ // Claude Code >= 2.1.12: translate --think to --thinking-budget
267
+ if (thinkLevel !== undefined && thinkingBudget === undefined) {
268
+ thinkingBudget = thinkingLevelToTokens[thinkLevel];
269
+ translation = `--think ${thinkLevel} → --thinking-budget ${thinkingBudget}`;
270
+ if (argv.verbose) {
271
+ await log(`📊 Translating for Claude Code ${version} (>= ${minVersion}):`, { verbose: true });
272
+ await log(` ${translation}`, { verbose: true });
273
+ if (maxBudget !== DEFAULT_MAX_THINKING_BUDGET) {
274
+ await log(` Using custom --max-thinking-budget: ${maxBudget}`, { verbose: true });
275
+ }
276
+ }
277
+ }
278
+ } else {
279
+ // Claude Code < 2.1.12: translate --thinking-budget to --think keywords
280
+ if (thinkingBudget !== undefined && thinkLevel === undefined) {
281
+ thinkLevel = tokensToThinkingLevel(thinkingBudget);
282
+ translation = `--thinking-budget ${thinkingBudget} → --think ${thinkLevel}`;
283
+ if (argv.verbose) {
284
+ await log(`📊 Translating for Claude Code ${version} (< ${minVersion}):`, { verbose: true });
285
+ await log(` ${translation}`, { verbose: true });
286
+ }
287
+ // Clear thinkingBudget since old versions don't support it
288
+ thinkingBudget = undefined;
289
+ }
290
+ }
291
+
292
+ return { thinkingBudget, thinkLevel, translation, isNewVersion, maxBudget };
293
+ };
222
294
  /**
223
295
  * Check if Playwright MCP is available and connected to Claude
224
296
  * @returns {Promise<boolean>} True if Playwright MCP is available, false otherwise
@@ -805,8 +877,20 @@ export const executeClaudeCommand = async params => {
805
877
  await log('', { verbose: true });
806
878
  }
807
879
  try {
808
- const claudeEnv = getClaudeEnv(); // Set CLAUDE_CODE_MAX_OUTPUT_TOKENS (see issue #1076)
880
+ // Resolve thinking settings (handles translation between --think and --thinking-budget based on Claude version)
881
+ // See issue #1146 for details on thinking budget translation
882
+ const { thinkingBudget: resolvedThinkingBudget, thinkLevel, isNewVersion } = await resolveThinkingSettings(argv, log);
883
+
884
+ // Set CLAUDE_CODE_MAX_OUTPUT_TOKENS (see issue #1076) and optionally MAX_THINKING_TOKENS (see issue #1146)
885
+ const claudeEnv = getClaudeEnv({ thinkingBudget: resolvedThinkingBudget });
809
886
  if (argv.verbose) await log(`📊 CLAUDE_CODE_MAX_OUTPUT_TOKENS: ${claudeCode.maxOutputTokens}`, { verbose: true });
887
+ if (resolvedThinkingBudget !== undefined) {
888
+ await log(`📊 MAX_THINKING_TOKENS: ${resolvedThinkingBudget}`, { verbose: true });
889
+ }
890
+ // Log thinking level for older Claude Code versions that use thinking keywords
891
+ if (!isNewVersion && thinkLevel) {
892
+ await log(`📊 Thinking level (via keywords): ${thinkLevel}`, { verbose: true });
893
+ }
810
894
  if (argv.resume) {
811
895
  // When resuming, pass prompt directly with -p flag. Escape double quotes for shell.
812
896
  const simpleEscapedPrompt = prompt.replace(/"/g, '\\"');
@@ -1351,4 +1435,7 @@ export default {
1351
1435
  executeClaudeCommand,
1352
1436
  checkForUncommittedChanges,
1353
1437
  calculateSessionTokens,
1438
+ getClaudeVersion,
1439
+ setClaudeVersion,
1440
+ resolveThinkingSettings,
1354
1441
  };
@@ -63,7 +63,10 @@ export const buildUserPrompt = params => {
63
63
  promptLines.push('');
64
64
  }
65
65
 
66
- // Add thinking instruction based on --think level
66
+ // Note: --think keywords are deprecated for Claude Code >= 2.1.12
67
+ // Thinking is now enabled by default with 31,999 token budget
68
+ // Use --thinking-budget to control MAX_THINKING_TOKENS instead
69
+ // Keeping keywords for backward compatibility with older Claude Code versions
67
70
  if (argv && argv.think) {
68
71
  const thinkMessages = {
69
72
  low: 'Think.',
@@ -89,7 +92,10 @@ export const buildUserPrompt = params => {
89
92
  export const buildSystemPrompt = params => {
90
93
  const { owner, repo, issueNumber, prNumber, branchName, workspaceTmpDir, argv } = params;
91
94
 
92
- // Build thinking instruction based on --think level
95
+ // Note: --think keywords are deprecated for Claude Code >= 2.1.12
96
+ // Thinking is now enabled by default with 31,999 token budget
97
+ // Use --thinking-budget to control MAX_THINKING_TOKENS instead
98
+ // Keeping keywords for backward compatibility with older Claude Code versions
93
99
  let thinkLine = '';
94
100
  if (argv && argv.think) {
95
101
  const thinkMessages = {
@@ -20,6 +20,9 @@ if (typeof globalThis.use === 'undefined') {
20
20
 
21
21
  const getenv = await use('getenv');
22
22
 
23
+ // Use semver package for version comparison (see issue #1146)
24
+ import semver from 'semver';
25
+
23
26
  // Import lino for parsing Links Notation format
24
27
  const { lino } = await import('./lino.lib.mjs');
25
28
 
@@ -89,8 +92,80 @@ export const claudeCode = {
89
92
  maxOutputTokens: parseIntWithDefault('CLAUDE_CODE_MAX_OUTPUT_TOKENS', parseIntWithDefault('HIVE_MIND_CLAUDE_CODE_MAX_OUTPUT_TOKENS', 64000)),
90
93
  };
91
94
 
95
+ // Default max thinking budget for Claude Code (see issue #1146)
96
+ // This is the default value used by Claude Code when extended thinking is enabled
97
+ // Can be overridden via --max-thinking-budget option
98
+ export const DEFAULT_MAX_THINKING_BUDGET = 31999;
99
+
100
+ /**
101
+ * Get thinking level token values calculated from max budget
102
+ * Values are evenly distributed: off=0, low=max/4, medium=max/2, high=max*3/4, max=max
103
+ * @param {number} maxBudget - Maximum thinking budget (default: 31999)
104
+ * @returns {Object} Mapping of thinking levels to token values
105
+ */
106
+ export const getThinkingLevelToTokens = (maxBudget = DEFAULT_MAX_THINKING_BUDGET) => ({
107
+ off: 0,
108
+ low: Math.floor(maxBudget / 4), // ~8000 for default 31999
109
+ medium: Math.floor(maxBudget / 2), // ~16000 for default 31999
110
+ high: Math.floor((maxBudget * 3) / 4), // ~24000 for default 31999
111
+ max: maxBudget, // 31999 by default
112
+ });
113
+
114
+ // Default thinking level to tokens mapping (using default max budget)
115
+ export const thinkingLevelToTokens = getThinkingLevelToTokens(DEFAULT_MAX_THINKING_BUDGET);
116
+
117
+ /**
118
+ * Get tokens to thinking level mapping function with configurable max budget
119
+ * Uses midpoint ranges to determine the level
120
+ * @param {number} maxBudget - Maximum thinking budget (default: 31999)
121
+ * @returns {Function} Function that converts tokens to thinking level
122
+ */
123
+ export const getTokensToThinkingLevel = (maxBudget = DEFAULT_MAX_THINKING_BUDGET) => {
124
+ const levels = getThinkingLevelToTokens(maxBudget);
125
+ // Calculate midpoints between levels for range determination
126
+ const lowMediumMidpoint = Math.floor((levels.low + levels.medium) / 2);
127
+ const mediumHighMidpoint = Math.floor((levels.medium + levels.high) / 2);
128
+ const highMaxMidpoint = Math.floor((levels.high + levels.max) / 2);
129
+
130
+ return tokens => {
131
+ if (tokens === 0) return 'off';
132
+ if (tokens <= lowMediumMidpoint) return 'low';
133
+ if (tokens <= mediumHighMidpoint) return 'medium';
134
+ if (tokens <= highMaxMidpoint) return 'high';
135
+ return 'max';
136
+ };
137
+ };
138
+
139
+ // Default tokens to thinking level function (using default max budget)
140
+ export const tokensToThinkingLevel = getTokensToThinkingLevel(DEFAULT_MAX_THINKING_BUDGET);
141
+
142
+ // Check if a version supports thinking budget (>= minimum version)
143
+ // Uses semver npm package for reliable version comparison (see issue #1146)
144
+ export const supportsThinkingBudget = (version, minVersion = '2.1.12') => {
145
+ // Clean the version string (remove any leading 'v' and extra text)
146
+ const cleanVersion = semver.clean(version) || semver.coerce(version)?.version;
147
+ const cleanMinVersion = semver.clean(minVersion) || semver.coerce(minVersion)?.version;
148
+
149
+ if (!cleanVersion || !cleanMinVersion) {
150
+ // If versions can't be parsed, assume old version (doesn't support budget)
151
+ return false;
152
+ }
153
+
154
+ return semver.gte(cleanVersion, cleanMinVersion);
155
+ };
156
+
92
157
  // Helper function to get Claude CLI environment with CLAUDE_CODE_MAX_OUTPUT_TOKENS set
93
- export const getClaudeEnv = () => ({ ...process.env, CLAUDE_CODE_MAX_OUTPUT_TOKENS: String(claudeCode.maxOutputTokens) });
158
+ // Optionally sets MAX_THINKING_TOKENS when thinkingBudget is provided (see issue #1146)
159
+ export const getClaudeEnv = (options = {}) => {
160
+ const env = { ...process.env, CLAUDE_CODE_MAX_OUTPUT_TOKENS: String(claudeCode.maxOutputTokens) };
161
+ // Set MAX_THINKING_TOKENS if thinkingBudget is provided
162
+ // This controls Claude Code's extended thinking feature (Claude Code >= 2.1.12)
163
+ // Default is 31999, set to 0 to disable thinking, max is 63999 for 64K output models
164
+ if (options.thinkingBudget !== undefined) {
165
+ env.MAX_THINKING_TOKENS = String(options.thinkingBudget);
166
+ }
167
+ return env;
168
+ };
94
169
 
95
170
  // Cache TTL configurations (in milliseconds)
96
171
  // The Usage API (Claude limits) has stricter rate limiting than regular APIs
@@ -1319,6 +1319,7 @@ export async function handlePRNotFoundError({ prNumber, owner, repo, argv, shoul
1319
1319
  if (argv.verbose) commandParts.push('--verbose');
1320
1320
  if (argv.model && argv.model !== 'sonnet') commandParts.push('--model', argv.model);
1321
1321
  if (argv.think) commandParts.push('--think', argv.think);
1322
+ if (argv.thinkingBudget !== undefined) commandParts.push('--thinking-budget', argv.thinkingBudget);
1322
1323
  await log(` ${commandParts.join(' ')}`, { level: 'error' });
1323
1324
  await log('', { level: 'error' });
1324
1325
  }
@@ -209,10 +209,20 @@ export const createYargsConfig = yargsInstance => {
209
209
  })
210
210
  .option('think', {
211
211
  type: 'string',
212
- description: 'Thinking level: low (Think.), medium (Think hard.), high (Think harder.), max (Ultrathink.)',
213
- choices: ['low', 'medium', 'high', 'max'],
212
+ description: 'Thinking level for Claude. Translated to --thinking-budget for Claude Code >= 2.1.12 (off=0, low=~8000, medium=~16000, high=~24000, max=31999). For older versions, uses thinking keywords.',
213
+ choices: ['off', 'low', 'medium', 'high', 'max'],
214
214
  default: undefined,
215
215
  })
216
+ .option('thinking-budget', {
217
+ type: 'number',
218
+ description: 'Thinking token budget for Claude Code (0-63999). Controls MAX_THINKING_TOKENS. Default: 31999 (Claude default). Set to 0 to disable thinking.',
219
+ default: undefined,
220
+ })
221
+ .option('max-thinking-budget', {
222
+ type: 'number',
223
+ description: 'Maximum thinking budget for calculating --think level mappings (default: 31999 for Claude Code). Values: off=0, low=max/4, medium=max/2, high=max*3/4, max=max.',
224
+ default: 31999,
225
+ })
216
226
  .option('prompt-plan-sub-agent', {
217
227
  type: 'boolean',
218
228
  description: 'Encourage AI to use Plan sub-agent for initial planning (only works with --tool claude)',
package/src/hive.mjs CHANGED
@@ -759,6 +759,8 @@ if (isDirectExecution) {
759
759
  args.push(argv.autoContinue ? '--auto-continue' : '--no-auto-continue');
760
760
  if (argv.autoResumeOnLimitReset) args.push('--auto-resume-on-limit-reset');
761
761
  if (argv.think) args.push('--think', argv.think);
762
+ if (argv.thinkingBudget !== undefined) args.push('--thinking-budget', argv.thinkingBudget);
763
+ if (argv.maxThinkingBudget !== undefined && argv.maxThinkingBudget !== 31999) args.push('--max-thinking-budget', argv.maxThinkingBudget);
762
764
  if (argv.promptPlanSubAgent) args.push('--prompt-plan-sub-agent');
763
765
  if (!argv.sentry) args.push('--no-sentry');
764
766
  if (argv.watch) args.push('--watch');
@@ -160,6 +160,9 @@ const KNOWN_OPTION_NAMES = [
160
160
  'dry-run',
161
161
  'tool',
162
162
  'think',
163
+ 'thinking-budget',
164
+ 'thinking-budget-claude-minimum-version',
165
+ 'max-thinking-budget',
163
166
  'watch',
164
167
  'sentry',
165
168
  'attach-logs',
@@ -216,10 +216,25 @@ export const createYargsConfig = yargsInstance => {
216
216
  })
217
217
  .option('think', {
218
218
  type: 'string',
219
- description: 'Thinking level: low (Think.), medium (Think hard.), high (Think harder.), max (Ultrathink.)',
220
- choices: ['low', 'medium', 'high', 'max'],
219
+ description: 'Thinking level for Claude. Translated to --thinking-budget for Claude Code >= 2.1.12 (off=0, low=~8000, medium=~16000, high=~24000, max=31999). For older versions, uses thinking keywords.',
220
+ choices: ['off', 'low', 'medium', 'high', 'max'],
221
221
  default: undefined,
222
222
  })
223
+ .option('thinking-budget', {
224
+ type: 'number',
225
+ description: 'Thinking token budget for Claude Code (0-63999). Controls MAX_THINKING_TOKENS. Default: 31999 (Claude default). Set to 0 to disable thinking. For older Claude Code versions, translated back to --think level.',
226
+ default: undefined,
227
+ })
228
+ .option('thinking-budget-claude-minimum-version', {
229
+ type: 'string',
230
+ description: 'Minimum Claude Code version that supports --thinking-budget (MAX_THINKING_TOKENS env var). Versions below this use thinking keywords instead.',
231
+ default: '2.1.12',
232
+ })
233
+ .option('max-thinking-budget', {
234
+ type: 'number',
235
+ description: 'Maximum thinking budget for calculating --think level mappings (default: 31999 for Claude Code). Values: off=0, low=max/4, medium=max/2, high=max*3/4, max=max.',
236
+ default: 31999,
237
+ })
223
238
  .option('prompt-plan-sub-agent', {
224
239
  type: 'boolean',
225
240
  description: 'Encourage AI to use Plan sub-agent for initial planning (only works with --tool claude)',
@@ -7,7 +7,9 @@
7
7
  * Features:
8
8
  * - Accepts all pending repository invitations
9
9
  * - Accepts all pending organization invitations
10
- * - Provides detailed feedback on accepted invitations
10
+ * - Groups output by Repositories and Organizations
11
+ * - Provides clickable links to repositories and organizations
12
+ * - Real-time progress updates during processing
11
13
  * - Error handling with detailed error messages
12
14
  *
13
15
  * @see https://docs.github.com/en/rest/collaborators/invitations
@@ -28,6 +30,83 @@ function escapeMarkdown(text) {
28
30
  return String(text).replace(/[_*[\]()~`>#+\-=|{}.!]/g, '\\$&');
29
31
  }
30
32
 
33
+ /**
34
+ * Build progress message from current state
35
+ * @param {Object} state - Current state object
36
+ * @param {string[]} state.acceptedRepos - List of accepted repo names
37
+ * @param {string[]} state.acceptedOrgs - List of accepted org names
38
+ * @param {string[]} state.errors - List of errors
39
+ * @param {number} state.totalRepos - Total number of repo invitations
40
+ * @param {number} state.totalOrgs - Total number of org invitations
41
+ * @param {number} state.processedRepos - Number of processed repo invitations
42
+ * @param {number} state.processedOrgs - Number of processed org invitations
43
+ * @param {boolean} state.isComplete - Whether processing is complete
44
+ * @returns {string} Formatted message
45
+ */
46
+ function buildProgressMessage(state) {
47
+ const { acceptedRepos, acceptedOrgs, errors, totalRepos, totalOrgs, processedRepos, processedOrgs, isComplete } = state;
48
+
49
+ // Calculate totals
50
+ const totalInvitations = totalRepos + totalOrgs;
51
+ const processedTotal = processedRepos + processedOrgs;
52
+ const acceptedTotal = acceptedRepos.length + acceptedOrgs.length;
53
+
54
+ // Build header with progress indicator
55
+ let message = isComplete ? '✅ *GitHub Invitations Processed*\n\n' : `🔄 *Processing GitHub Invitations* \\(${processedTotal}/${totalInvitations}\\)\n\n`;
56
+
57
+ // Show Repositories section if any
58
+ if (acceptedRepos.length > 0 || (!isComplete && totalRepos > 0)) {
59
+ message += '*Repositories:*\n';
60
+ for (const repoName of acceptedRepos) {
61
+ // Create clickable link: [owner/repo](https://github.com/owner/repo)
62
+ const escapedName = escapeMarkdown(repoName);
63
+ const escapedLink = escapeMarkdown(`https://github.com/${repoName}`);
64
+ message += ` • 📦 [${escapedName}](${escapedLink})\n`;
65
+ }
66
+ // Show pending indicator if still processing repos
67
+ if (!isComplete && processedRepos < totalRepos) {
68
+ const remaining = totalRepos - processedRepos;
69
+ message += ` • _\\.\\.\\. ${remaining} more pending_\n`;
70
+ }
71
+ message += '\n';
72
+ }
73
+
74
+ // Show Organizations section if any
75
+ if (acceptedOrgs.length > 0 || (!isComplete && totalOrgs > 0)) {
76
+ message += '*Organizations:*\n';
77
+ for (const orgName of acceptedOrgs) {
78
+ // Create clickable link: [org](https://github.com/org)
79
+ const escapedName = escapeMarkdown(orgName);
80
+ const escapedLink = escapeMarkdown(`https://github.com/${orgName}`);
81
+ message += ` • 🏢 [${escapedName}](${escapedLink})\n`;
82
+ }
83
+ // Show pending indicator if still processing orgs
84
+ if (!isComplete && processedOrgs < totalOrgs) {
85
+ const remaining = totalOrgs - processedOrgs;
86
+ message += ` • _\\.\\.\\. ${remaining} more pending_\n`;
87
+ }
88
+ message += '\n';
89
+ }
90
+
91
+ // Show errors if any
92
+ if (errors.length > 0) {
93
+ message += '*Errors:*\n' + errors.map(e => ` • ${escapeMarkdown(e)}`).join('\n') + '\n\n';
94
+ }
95
+
96
+ // Show summary
97
+ if (isComplete) {
98
+ if (acceptedTotal === 0 && errors.length === 0) {
99
+ message += 'No pending invitations found\\.';
100
+ } else if (acceptedTotal > 0 && errors.length === 0) {
101
+ message += `\n🎉 Successfully accepted ${acceptedTotal} invitation\\(s\\)\\!`;
102
+ } else if (acceptedTotal > 0 && errors.length > 0) {
103
+ message += `\n⚠️ Accepted ${acceptedTotal} invitation\\(s\\), ${errors.length} error\\(s\\)\\.`;
104
+ }
105
+ }
106
+
107
+ return message;
108
+ }
109
+
31
110
  /**
32
111
  * Registers the /accept_invites command handler with the bot
33
112
  * @param {Object} bot - The Telegraf bot instance
@@ -61,68 +140,97 @@ export function registerAcceptInvitesCommand(bot, options) {
61
140
  reply_to_message_id: ctx.message.message_id,
62
141
  });
63
142
 
64
- const fetchingMessage = await ctx.reply('🔄 Fetching pending GitHub invitations...', { reply_to_message_id: ctx.message.message_id });
65
- const accepted = [];
66
- const errors = [];
143
+ const fetchingMessage = await ctx.reply('🔄 Fetching pending GitHub invitations\\.\\.\\.', {
144
+ reply_to_message_id: ctx.message.message_id,
145
+ parse_mode: 'MarkdownV2',
146
+ });
147
+
148
+ // State for tracking progress
149
+ const state = {
150
+ acceptedRepos: [],
151
+ acceptedOrgs: [],
152
+ errors: [],
153
+ totalRepos: 0,
154
+ totalOrgs: 0,
155
+ processedRepos: 0,
156
+ processedOrgs: 0,
157
+ isComplete: false,
158
+ };
159
+
160
+ // Helper to update the message safely
161
+ const updateMessage = async () => {
162
+ try {
163
+ const message = buildProgressMessage(state);
164
+ await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, message, { parse_mode: 'MarkdownV2' });
165
+ } catch (err) {
166
+ // Ignore "message not modified" errors
167
+ if (!err.message?.includes('message is not modified')) {
168
+ VERBOSE && console.log(`[VERBOSE] /accept-invites: Error updating message: ${err.message}`);
169
+ }
170
+ }
171
+ };
67
172
 
68
173
  try {
69
174
  // Fetch repository invitations
70
175
  const { stdout: repoInvJson } = await exec('gh api /user/repository_invitations 2>/dev/null || echo "[]"');
71
176
  const repoInvitations = JSON.parse(repoInvJson.trim() || '[]');
177
+ state.totalRepos = repoInvitations.length;
72
178
  VERBOSE && console.log(`[VERBOSE] Found ${repoInvitations.length} pending repo invitations`);
73
179
 
74
- // Accept each repo invitation
180
+ // Fetch organization invitations
181
+ const { stdout: orgMemJson } = await exec('gh api /user/memberships/orgs 2>/dev/null || echo "[]"');
182
+ const orgMemberships = JSON.parse(orgMemJson.trim() || '[]');
183
+ const pendingOrgs = orgMemberships.filter(m => m.state === 'pending');
184
+ state.totalOrgs = pendingOrgs.length;
185
+ VERBOSE && console.log(`[VERBOSE] Found ${pendingOrgs.length} pending org invitations`);
186
+
187
+ // Check if there are any invitations
188
+ if (state.totalRepos === 0 && state.totalOrgs === 0) {
189
+ state.isComplete = true;
190
+ await updateMessage();
191
+ return;
192
+ }
193
+
194
+ // Update to show we found invitations
195
+ await updateMessage();
196
+
197
+ // Accept each repo invitation with progress updates
75
198
  for (const inv of repoInvitations) {
76
199
  const repoName = inv.repository?.full_name || 'unknown';
77
200
  try {
78
201
  await exec(`gh api -X PATCH /user/repository_invitations/${inv.id}`);
79
- accepted.push(`📦 Repository: ${repoName}`);
202
+ state.acceptedRepos.push(repoName);
80
203
  VERBOSE && console.log(`[VERBOSE] Accepted repo invitation: ${repoName}`);
81
204
  } catch (e) {
82
- errors.push(`📦 ${repoName}: ${e.message}`);
205
+ state.errors.push(`📦 ${repoName}: ${e.message}`);
83
206
  VERBOSE && console.log(`[VERBOSE] Failed to accept repo invitation ${repoName}: ${e.message}`);
84
207
  }
208
+ state.processedRepos++;
209
+ await updateMessage();
85
210
  }
86
211
 
87
- // Fetch organization invitations
88
- const { stdout: orgMemJson } = await exec('gh api /user/memberships/orgs 2>/dev/null || echo "[]"');
89
- const orgMemberships = JSON.parse(orgMemJson.trim() || '[]');
90
- const pendingOrgs = orgMemberships.filter(m => m.state === 'pending');
91
- VERBOSE && console.log(`[VERBOSE] Found ${pendingOrgs.length} pending org invitations`);
92
-
93
- // Accept each org invitation
212
+ // Accept each org invitation with progress updates
94
213
  for (const membership of pendingOrgs) {
95
214
  const orgName = membership.organization?.login || 'unknown';
96
215
  try {
97
216
  await exec(`gh api -X PATCH /user/memberships/orgs/${orgName} -f state=active`);
98
- accepted.push(`🏢 Organization: ${orgName}`);
217
+ state.acceptedOrgs.push(orgName);
99
218
  VERBOSE && console.log(`[VERBOSE] Accepted org invitation: ${orgName}`);
100
219
  } catch (e) {
101
- errors.push(`🏢 ${orgName}: ${e.message}`);
220
+ state.errors.push(`🏢 ${orgName}: ${e.message}`);
102
221
  VERBOSE && console.log(`[VERBOSE] Failed to accept org invitation ${orgName}: ${e.message}`);
103
222
  }
223
+ state.processedOrgs++;
224
+ await updateMessage();
104
225
  }
105
226
 
106
- // Build response message
107
- let message = '✅ *GitHub Invitations Processed*\n\n';
108
- if (accepted.length === 0 && errors.length === 0) {
109
- message += 'No pending invitations found.';
110
- } else {
111
- if (accepted.length > 0) {
112
- message += '*Accepted:*\n' + accepted.map(a => ` • ${escapeMarkdown(a)}`).join('\n') + '\n\n';
113
- }
114
- if (errors.length > 0) {
115
- message += '*Errors:*\n' + errors.map(e => ` • ${escapeMarkdown(e)}`).join('\n');
116
- }
117
- if (accepted.length > 0 && errors.length === 0) {
118
- message += `\n🎉 Successfully accepted ${accepted.length} invitation(s)!`;
119
- }
120
- }
121
-
122
- await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, message, { parse_mode: 'Markdown' });
227
+ // Final update
228
+ state.isComplete = true;
229
+ await updateMessage();
123
230
  } catch (error) {
124
231
  console.error('Error in /accept-invites:', error);
125
- await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, `❌ Error fetching invitations: ${escapeMarkdown(error.message)}\n\nMake sure \`gh\` CLI is installed and authenticated.`, { parse_mode: 'Markdown' });
232
+ const escapedError = escapeMarkdown(error.message);
233
+ await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, `❌ Error fetching invitations: ${escapedError}\n\nMake sure \`gh\` CLI is installed and authenticated\\.`, { parse_mode: 'MarkdownV2' });
126
234
  }
127
235
  });
128
236
  }
@@ -770,7 +770,7 @@ bot.command('help', async ctx => {
770
770
  message += '🔧 *Common Options:*\n';
771
771
  message += '• `--model <model>` or `-m` - Specify AI model (sonnet, opus, haiku, haiku-3-5, haiku-3)\n';
772
772
  message += '• `--base-branch <branch>` or `-b` - Target branch for PR (default: repo default branch)\n';
773
- message += '• `--think <level>` - Thinking level (low/medium/high/max)\n';
773
+ message += '• `--think <level>` - Thinking level (off/low/medium/high/max) | `--thinking-budget <num>` - Token budget (0-63999)\n';
774
774
  message += '• `--verbose` or `-v` - Verbose output | `--attach-logs` - Attach logs to PR\n';
775
775
  message += '\n💡 *Tip:* Many more options available. See full documentation for complete list.\n';
776
776