@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 +12 -0
- package/README.hi.md +23 -1
- package/README.md +22 -1
- package/README.ru.md +23 -1
- package/README.zh.md +22 -1
- package/package.json +1 -1
- package/src/github-merge-targets.lib.mjs +205 -0
- package/src/limits-i18n.lib.mjs +8 -0
- package/src/limits-subscription.lib.mjs +40 -1
- package/src/limits.lib.mjs +109 -150
- package/src/locales/en.lino +16 -0
- package/src/locales/hi.lino +16 -0
- package/src/locales/ru.lino +16 -0
- package/src/locales/zh.lino +16 -0
- package/src/telegram-merge-command.lib.mjs +93 -15
- package/src/telegram-merge-queue.lib.mjs +108 -24
- package/src/telegram-merge-wait.lib.mjs +75 -0
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
|
|
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
|
|
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
|
|
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
|
|
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
|
@@ -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
|
+
};
|
package/src/limits-i18n.lib.mjs
CHANGED
|
@@ -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
|
*
|