@link-assistant/hive-mind 2.1.3 → 2.1.5

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,17 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 2.1.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 26b07f2: Extend Telegram `/merge` to accept repository, issue, and pull request targets, including reply-based GitHub link extraction, and wait for unfinished PRs to become mergeable before merging.
8
+
9
+ ## 2.1.4
10
+
11
+ ### Patch Changes
12
+
13
+ - acc3ea1: Simplify the `/limits` output with subscription headings and hide unused Codex limits and credits.
14
+
3
15
  ## 2.1.3
4
16
 
5
17
  ### Patch Changes
package/README.hi.md CHANGED
@@ -511,6 +511,26 @@ Examples:
511
511
  /hive https://github.com/microsoft --all-issues --concurrency 3
512
512
  ```
513
513
 
514
+ #### `/merge` - तैयार Pull Requests merge करें
515
+
516
+ ```
517
+ /merge <repository-url|issue-url|pull-request-url> [--auto-resolve]
518
+
519
+ Examples:
520
+ /merge https://github.com/owner/repo
521
+ /merge https://github.com/owner/repo/issues/123
522
+ /merge https://github.com/owner/repo/pull/456
523
+ ```
524
+
525
+ Repository targets `ready` label वाले PRs को क्रम से process करते हैं। Issue और
526
+ pull request targets केवल linked या selected PR process करते हैं। आप किसी ऐसे
527
+ message पर `/merge` से reply भी कर सकते हैं जिसमें एक GitHub repository, issue,
528
+ या pull request link हो, जैसे कोई पुराना `/codex ...issues/123` command.
529
+
530
+ अगर target PR अभी finished नहीं है, तो `/merge` merge करने से पहले उसके
531
+ mergeable होने तक wait करता है। Merge-conflict skips अभी भी `--auto-resolve` के
532
+ साथ काम करते हैं।
533
+
514
534
  #### `/limits` - उपयोग सीमाएँ दिखाएँ
515
535
 
516
536
  ```
@@ -521,7 +541,9 @@ Shows:
521
541
  - RAM usage (used vs total)
522
542
  - Disk space usage
523
543
  - GitHub API rate limits
524
- - Claude usage limits (session and weekly)
544
+ - Claude and ChatGPT subscription usage windows
545
+ - Non-zero Codex weekly limits and credits
546
+ - Solve queue status
525
547
  ```
526
548
 
527
549
  #### `/terminal_watch` - Live Session Log
package/README.md CHANGED
@@ -532,6 +532,25 @@ Examples:
532
532
  /hive https://github.com/microsoft --all-issues --concurrency 3
533
533
  ```
534
534
 
535
+ #### `/merge` - Merge Ready Pull Requests
536
+
537
+ ```
538
+ /merge <repository-url|issue-url|pull-request-url> [--auto-resolve]
539
+
540
+ Examples:
541
+ /merge https://github.com/owner/repo
542
+ /merge https://github.com/owner/repo/issues/123
543
+ /merge https://github.com/owner/repo/pull/456
544
+ ```
545
+
546
+ Repository targets process PRs with the `ready` label sequentially. Issue and
547
+ pull request targets process only the linked or selected PR. You can also reply
548
+ with `/merge` to a message containing one GitHub repository, issue, or pull
549
+ request link, such as a previous `/codex ...issues/123` command.
550
+
551
+ If a target PR is not finished yet, `/merge` waits for it to become mergeable
552
+ before merging. Merge-conflict skips still work with `--auto-resolve`.
553
+
535
554
  #### `/limits` - Show Usage Limits
536
555
 
537
556
  ```
@@ -542,7 +561,9 @@ Shows:
542
561
  - RAM usage (used vs total)
543
562
  - Disk space usage
544
563
  - GitHub API rate limits
545
- - Claude usage limits (session and weekly)
564
+ - Claude and ChatGPT subscription usage windows
565
+ - Non-zero Codex weekly limits and credits
566
+ - Solve queue status
546
567
  ```
547
568
 
548
569
  #### `/terminal_watch` - Live Session Log
package/README.ru.md CHANGED
@@ -512,6 +512,26 @@ Examples:
512
512
  /hive https://github.com/microsoft --all-issues --concurrency 3
513
513
  ```
514
514
 
515
+ #### `/merge` — Слияние готовых Pull Request
516
+
517
+ ```
518
+ /merge <repository-url|issue-url|pull-request-url> [--auto-resolve]
519
+
520
+ Examples:
521
+ /merge https://github.com/owner/repo
522
+ /merge https://github.com/owner/repo/issues/123
523
+ /merge https://github.com/owner/repo/pull/456
524
+ ```
525
+
526
+ Цель-репозиторий обрабатывает PR с меткой `ready` последовательно. Цели issue и
527
+ pull request обрабатывают только связанный или выбранный PR. Также можно ответить
528
+ командой `/merge` на сообщение с одной ссылкой на GitHub repository, issue или
529
+ pull request, например на предыдущую команду `/codex ...issues/123`.
530
+
531
+ Если целевой PR ещё не завершён, `/merge` ждёт, пока он станет mergeable, и затем
532
+ выполняет слияние. Пропуск конфликтов слияния по-прежнему работает с
533
+ `--auto-resolve`.
534
+
515
535
  #### `/limits` — Показать лимиты использования
516
536
 
517
537
  ```
@@ -522,7 +542,9 @@ Shows:
522
542
  - RAM usage (used vs total)
523
543
  - Disk space usage
524
544
  - GitHub API rate limits
525
- - Claude usage limits (session and weekly)
545
+ - Claude and ChatGPT subscription usage windows
546
+ - Non-zero Codex weekly limits and credits
547
+ - Solve queue status
526
548
  ```
527
549
 
528
550
  #### `/terminal_watch` — Live Session Log
package/README.zh.md CHANGED
@@ -508,6 +508,25 @@ Examples:
508
508
  /hive https://github.com/microsoft --all-issues --concurrency 3
509
509
  ```
510
510
 
511
+ #### `/merge` - 合并就绪的 Pull Request
512
+
513
+ ```
514
+ /merge <repository-url|issue-url|pull-request-url> [--auto-resolve]
515
+
516
+ Examples:
517
+ /merge https://github.com/owner/repo
518
+ /merge https://github.com/owner/repo/issues/123
519
+ /merge https://github.com/owner/repo/pull/456
520
+ ```
521
+
522
+ 仓库目标会按顺序处理带有 `ready` 标签的 PR。Issue 和 Pull Request
523
+ 目标只处理关联或指定的 PR。你也可以回复一条包含单个 GitHub
524
+ 仓库、Issue 或 Pull Request 链接的消息并发送 `/merge`,例如之前的
525
+ `/codex ...issues/123` 命令。
526
+
527
+ 如果目标 PR 尚未完成,`/merge` 会等待它变为可合并后再合并。合并冲突跳过仍可配合
528
+ `--auto-resolve` 使用。
529
+
511
530
  #### `/limits` - 显示用量限制
512
531
 
513
532
  ```
@@ -518,7 +537,9 @@ Shows:
518
537
  - RAM usage (used vs total)
519
538
  - Disk space usage
520
539
  - GitHub API rate limits
521
- - Claude usage limits (session and weekly)
540
+ - Claude and ChatGPT subscription usage windows
541
+ - Non-zero Codex weekly limits and credits
542
+ - Solve queue status
522
543
  ```
523
544
 
524
545
  #### `/terminal_watch` - Live Session Log
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "2.1.3",
3
+ "version": "2.1.5",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * GitHub merge target helpers for `/merge`.
4
+ *
5
+ * Repository-wide merge queues still use github-merge.lib.mjs. This module
6
+ * handles the narrower targets introduced in issue #2013: a single PR URL, an
7
+ * issue URL with linked PRs, or a GitHub URL found in a replied message.
8
+ *
9
+ * @see https://github.com/link-assistant/hive-mind/issues/2013
10
+ */
11
+
12
+ import { promisify } from 'node:util';
13
+ import { exec as execCallback } from 'node:child_process';
14
+
15
+ import { githubLimits } from './config.lib.mjs';
16
+ import { parseGitHubUrl } from './github.lib.mjs';
17
+ import { ghWithRateLimitRetry } from './github-rate-limit.lib.mjs';
18
+ import { getLinkedPRsFromTimeline } from './github-merge-ready-sync.lib.mjs';
19
+
20
+ const execRaw = promisify(execCallback);
21
+ const exec = (cmd, opts = {}) =>
22
+ ghWithRateLimitRetry(() => execRaw(cmd, { maxBuffer: githubLimits.bufferMaxSize, ...opts }), {
23
+ label: `gh exec (${cmd.split(/\s+/).slice(0, 3).join(' ')})`,
24
+ });
25
+
26
+ const SUPPORTED_REPOSITORY_TYPES = new Set(['repo', 'issues_list', 'pulls_list']);
27
+ const SUPPORTED_TARGET_TYPES = new Set([...SUPPORTED_REPOSITORY_TYPES, 'issue', 'pull']);
28
+
29
+ function buildIssueUrl(owner, repo, issueNumber) {
30
+ return `https://github.com/${owner}/${repo}/issues/${issueNumber}`;
31
+ }
32
+
33
+ function buildPullUrl(owner, repo, prNumber) {
34
+ return `https://github.com/${owner}/${repo}/pull/${prNumber}`;
35
+ }
36
+
37
+ /**
38
+ * Parse a `/merge` target URL.
39
+ *
40
+ * @param {string} url
41
+ * @returns {{valid: boolean, mode?: 'repository'|'issue'|'pull', owner: string|null, repo: string|null, issueNumber?: number, prNumber?: number, url?: string, error: string|null}}
42
+ */
43
+ export function parseMergeTargetUrl(url) {
44
+ const parsed = parseGitHubUrl(url);
45
+
46
+ if (!parsed.valid) {
47
+ return { valid: false, owner: null, repo: null, error: parsed.error };
48
+ }
49
+
50
+ if (SUPPORTED_REPOSITORY_TYPES.has(parsed.type)) {
51
+ return {
52
+ valid: true,
53
+ mode: 'repository',
54
+ owner: parsed.owner,
55
+ repo: parsed.repo,
56
+ url: `https://github.com/${parsed.owner}/${parsed.repo}`,
57
+ error: null,
58
+ };
59
+ }
60
+
61
+ if (parsed.type === 'issue') {
62
+ return {
63
+ valid: true,
64
+ mode: 'issue',
65
+ owner: parsed.owner,
66
+ repo: parsed.repo,
67
+ issueNumber: parsed.number,
68
+ url: buildIssueUrl(parsed.owner, parsed.repo, parsed.number),
69
+ error: null,
70
+ };
71
+ }
72
+
73
+ if (parsed.type === 'pull') {
74
+ return {
75
+ valid: true,
76
+ mode: 'pull',
77
+ owner: parsed.owner,
78
+ repo: parsed.repo,
79
+ prNumber: parsed.number,
80
+ url: buildPullUrl(parsed.owner, parsed.repo, parsed.number),
81
+ error: null,
82
+ };
83
+ }
84
+
85
+ if (parsed.type === 'user' || parsed.type === 'organization') {
86
+ return {
87
+ valid: false,
88
+ owner: parsed.owner,
89
+ repo: null,
90
+ error: 'URL points to a user/organization. Please provide a repository, issue, or pull request URL.',
91
+ };
92
+ }
93
+
94
+ return {
95
+ valid: false,
96
+ owner: parsed.owner || null,
97
+ repo: parsed.repo || null,
98
+ error: `URL type '${parsed.type}' is not supported for /merge. Please provide a repository, issue, or pull request URL.`,
99
+ };
100
+ }
101
+
102
+ function trimCandidateUrl(candidate) {
103
+ return String(candidate || '')
104
+ .trim()
105
+ .replace(/^[<([{'"]+/, '')
106
+ .replace(/[>)\]}'".,;:!?]+$/, '');
107
+ }
108
+
109
+ /**
110
+ * Extract a single `/merge` target URL from arbitrary text, such as a replied
111
+ * `/codex https://github.com/owner/repo/issues/123 --think max` message.
112
+ *
113
+ * @param {string} text
114
+ * @returns {{valid: boolean, url: string|null, target: Object|null, error: string|null}}
115
+ */
116
+ export function extractMergeTargetUrlFromText(text) {
117
+ if (!text || typeof text !== 'string') {
118
+ return { valid: false, url: null, target: null, error: 'No text to inspect for GitHub links' };
119
+ }
120
+
121
+ const pattern = /(?:https?:\/\/)?github\.com\/[^\s<>)\]]+|[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/(?:issues|pull)\/\d+/g;
122
+ const matches = text.match(pattern) || [];
123
+ const targetsByUrl = new Map();
124
+
125
+ for (const match of matches) {
126
+ const candidate = trimCandidateUrl(match);
127
+ const target = parseMergeTargetUrl(candidate);
128
+ if (target.valid && SUPPORTED_TARGET_TYPES.has(target.mode === 'repository' ? 'repo' : target.mode)) {
129
+ targetsByUrl.set(target.url, target);
130
+ }
131
+ }
132
+
133
+ const targets = [...targetsByUrl.values()];
134
+ if (targets.length === 0) {
135
+ return { valid: false, url: null, target: null, error: 'No GitHub repository, issue, or pull request link found' };
136
+ }
137
+ if (targets.length > 1) {
138
+ return { valid: false, url: null, target: null, error: `Expected one GitHub merge target, found ${targets.length}` };
139
+ }
140
+
141
+ const [target] = targets;
142
+ return { valid: true, url: target.url, target, error: null };
143
+ }
144
+
145
+ /**
146
+ * Fetch a PR in the shape expected by MergeQueueItem.
147
+ *
148
+ * @param {string} owner
149
+ * @param {string} repo
150
+ * @param {number} prNumber
151
+ * @param {Object|null} issue
152
+ * @param {boolean} verbose
153
+ * @returns {Promise<{pr: Object, issue: Object|null, sortDate: Date}>}
154
+ */
155
+ export async function fetchMergeTargetPullRequest(owner, repo, prNumber, issue = null, verbose = false) {
156
+ const { stdout } = await exec(`gh pr view ${prNumber} --repo ${owner}/${repo} --json number,title,url,createdAt,headRefName,author,mergeable,mergeStateStatus,isDraft,state`);
157
+ const pr = JSON.parse(stdout.trim());
158
+ if (verbose) {
159
+ console.log(`[VERBOSE] /merge: Loaded target PR #${pr.number} from ${owner}/${repo}`);
160
+ }
161
+ return {
162
+ pr,
163
+ issue,
164
+ sortDate: new Date(pr.createdAt),
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Resolve a non-repository merge target into queue item data.
170
+ *
171
+ * @param {{mode: string, owner: string, repo: string, issueNumber?: number, prNumber?: number, url?: string}} target
172
+ * @param {boolean} verbose
173
+ * @returns {Promise<Array<{pr: Object, issue: Object|null, sortDate: Date}>|null>}
174
+ */
175
+ export async function resolveMergeTargetItems(target, verbose = false) {
176
+ if (!target || target.mode === 'repository') {
177
+ return null;
178
+ }
179
+
180
+ if (target.mode === 'pull') {
181
+ return [await fetchMergeTargetPullRequest(target.owner, target.repo, target.prNumber, null, verbose)];
182
+ }
183
+
184
+ if (target.mode === 'issue') {
185
+ const issue = {
186
+ number: target.issueNumber,
187
+ url: target.url || buildIssueUrl(target.owner, target.repo, target.issueNumber),
188
+ };
189
+ const linkedPRs = await getLinkedPRsFromTimeline(target.owner, target.repo, target.issueNumber, verbose);
190
+ const items = [];
191
+ for (const linkedPR of linkedPRs) {
192
+ items.push(await fetchMergeTargetPullRequest(target.owner, target.repo, linkedPR.number, issue, verbose));
193
+ }
194
+ return items.sort((a, b) => a.sortDate - b.sortDate);
195
+ }
196
+
197
+ throw new Error(`Unsupported merge target mode: ${target.mode}`);
198
+ }
199
+
200
+ export default {
201
+ parseMergeTargetUrl,
202
+ extractMergeTargetUrlFromText,
203
+ fetchMergeTargetPullRequest,
204
+ resolveMergeTargetItems,
205
+ };
@@ -9,12 +9,15 @@ const ENGLISH_LIMITS = {
9
9
  balance: 'balance',
10
10
  claude_5_hour_session: 'Claude 5 hour session',
11
11
  claude_limits: 'Claude limits',
12
+ claude_subscription_title: 'Claude{{plan}} subscription',
13
+ chatgpt_subscription_title: 'ChatGPT{{plan}} subscription',
12
14
  codex_5_hour_session: 'Codex 5 hour session',
13
15
  codex_credits: 'Codex credits',
14
16
  codex_limits: 'Codex limits',
15
17
  cpu: 'CPU',
16
18
  cpu_cores_used: 'CPU cores used',
17
19
  current_time: 'Current time',
20
+ current_week: 'Current week',
18
21
  current_week_all_models: 'Current week (all models)',
19
22
  current_week_sonnet_only: 'Current week (Sonnet only)',
20
23
  disabled_by_admin: '`--show-limits` is disabled by the bot administrator.',
@@ -25,6 +28,7 @@ const ENGLISH_LIMITS = {
25
28
  duration_second_short: 's',
26
29
  end: 'End',
27
30
  five_hour_session: '5h session',
31
+ five_hour_limit_session: '5 hour session',
28
32
  five_min_load_avg: '5m load avg',
29
33
  github_api: 'GitHub API',
30
34
  limits_at_end: 'Limits at end',
@@ -76,6 +80,10 @@ const ENGLISH_LIMITS = {
76
80
  start: 'Start',
77
81
  subscription_ends: 'Subscription ends {{time}}',
78
82
  subscription_ends_in: 'Subscription ends in {{duration}} ({{time}})',
83
+ subscription_detail_ends: 'ends {{time}}',
84
+ subscription_detail_ends_in: 'ends in {{duration}}; {{time}}',
85
+ subscription_detail_trial_ends: 'trial ends {{time}}',
86
+ subscription_detail_trial_ends_in: 'trial ends in {{duration}}; {{time}}',
79
87
  subscription_status: 'Subscription: {{status}}',
80
88
  trial_ends: 'Trial ends {{time}}',
81
89
  trial_ends_in: 'Trial ends in {{duration}} ({{time}})',
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { CACHE_TTL, DEFAULT_CODEX_AUTH_PATH, DEFAULT_CREDENTIALS_PATH, decodeJwtPayload, getLimitCache, readCodexAuth, readCredentials } from './limits.lib.mjs';
13
- import { formatLocalizedRelativeTime, formatLocalizedResetTime, formatSubscriptionEnds, formatSubscriptionStatus, formatTrialEnds, resolveLimitLocale } from './limits-i18n.lib.mjs';
13
+ import { formatLocalizedRelativeTime, formatLocalizedResetTime, formatSubscriptionEnds, formatSubscriptionStatus, formatTrialEnds, lt, resolveLimitLocale } from './limits-i18n.lib.mjs';
14
14
 
15
15
  const PROFILE_API_ENDPOINT = 'https://api.anthropic.com/api/oauth/profile';
16
16
 
@@ -32,6 +32,45 @@ export function formatSubscriptionLines(subscription, options = {}) {
32
32
  return line ? `${line}\n` : '';
33
33
  }
34
34
 
35
+ function humanizePlanType(planType, provider) {
36
+ if (!planType) return '';
37
+ const providerPrefix = provider === 'claude' ? /^claude[\s_-]*/i : /^(chatgpt|codex|openai)[\s_-]*/i;
38
+ const normalized = String(planType).trim().replace(providerPrefix, '').replace(/[_-]+/g, ' ').replace(/\s+/g, ' ').trim();
39
+ if (!normalized) return '';
40
+ return normalized
41
+ .split(' ')
42
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
43
+ .join(' ');
44
+ }
45
+
46
+ function formatSubscriptionDetail(subscription, options = {}) {
47
+ if (!subscription) return '';
48
+ const locale = resolveLimitLocale(options);
49
+ const buildDetail = (iso, inKey, atKey) => {
50
+ const resetTime = formatLocalizedResetTime(iso, true, { locale });
51
+ if (!resetTime) return '';
52
+ const duration = formatLocalizedRelativeTime(iso, { locale });
53
+ return duration ? lt(inKey, { duration, time: resetTime }, { locale }) : lt(atKey, { time: resetTime }, { locale });
54
+ };
55
+
56
+ if (subscription.endsAt) return buildDetail(subscription.endsAt, 'subscription_detail_ends_in', 'subscription_detail_ends');
57
+ if (subscription.trialEndsAt) return buildDetail(subscription.trialEndsAt, 'subscription_detail_trial_ends_in', 'subscription_detail_trial_ends');
58
+ return subscription.status ? String(subscription.status).trim() : '';
59
+ }
60
+
61
+ export function formatSubscriptionHeading(provider, subscription, options = {}) {
62
+ const locale = resolveLimitLocale(options);
63
+ const normalizedProvider = provider === 'claude' ? 'claude' : 'chatgpt';
64
+ const planType = subscription?.planType || options?.planType || null;
65
+ if (!subscription && !planType) return '';
66
+
67
+ const plan = humanizePlanType(planType, normalizedProvider);
68
+ const titleKey = normalizedProvider === 'claude' ? 'claude_subscription_title' : 'chatgpt_subscription_title';
69
+ const title = lt(titleKey, { plan: plan ? ` ${plan}` : '' }, { locale });
70
+ const detail = formatSubscriptionDetail(subscription, { locale });
71
+ return detail ? `${title} (${detail})` : title;
72
+ }
73
+
35
74
  /**
36
75
  * Get Claude subscription metadata.
37
76
  *