@link-assistant/hive-mind 1.69.13 → 1.69.15
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/package.json +1 -1
- package/src/limits-i18n.lib.mjs +19 -0
- package/src/limits-subscription.lib.mjs +206 -0
- package/src/limits.lib.mjs +21 -7
- package/src/locales/en.lino +5 -0
- package/src/locales/hi.lino +5 -0
- package/src/locales/ru.lino +5 -0
- package/src/locales/zh.lino +5 -0
- package/src/solve.fork-detection.lib.mjs +81 -16
- package/src/telegram-bot.mjs +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.69.15
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- fbda6de: Fix `--auto-fork` failing on private repositories with read-only access when forking is allowed. `handleAutoForkOption` now probes the `allow_forking` repository attribute before bailing out: when it is `true`, fork mode is enabled (the same behaviour already used for public repos without write access); when it is explicitly `false`, the fatal exit explains that direct branch mode needs push/write access, fork mode is disabled, and the maintainer must either grant Write access or enable private forking; when it cannot be determined, we fall through with a verbose warning so `gh repo fork` can produce a precise downstream error. Resolves #1795.
|
|
8
|
+
|
|
9
|
+
## 1.69.14
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 32af9e1: Show subscription end date in `/limits` for Claude and Codex when the underlying providers expose it. Claude trials display the trial end date from the OAuth profile; Codex displays the renewal date decoded from the ChatGPT JWT (`chatgpt_subscription_active_until`). Lines are only rendered when real data is available.
|
|
14
|
+
|
|
3
15
|
## 1.69.13
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
package/src/limits-i18n.lib.mjs
CHANGED
|
@@ -74,6 +74,11 @@ const ENGLISH_LIMITS = {
|
|
|
74
74
|
seven_day_sonnet_only: '7d Sonnet only',
|
|
75
75
|
session: 'session',
|
|
76
76
|
start: 'Start',
|
|
77
|
+
subscription_ends: 'Subscription ends {{time}}',
|
|
78
|
+
subscription_ends_in: 'Subscription ends in {{duration}} ({{time}})',
|
|
79
|
+
subscription_status: 'Subscription: {{status}}',
|
|
80
|
+
trial_ends: 'Trial ends {{time}}',
|
|
81
|
+
trial_ends_in: 'Trial ends in {{duration}} ({{time}})',
|
|
77
82
|
unavailable: 'unavailable',
|
|
78
83
|
unlimited: 'unlimited',
|
|
79
84
|
used: 'used',
|
|
@@ -110,6 +115,20 @@ export function formatLimitResetsAt(resetTime, options = {}) {
|
|
|
110
115
|
return lt('resets_at', { time: resetTime }, options);
|
|
111
116
|
}
|
|
112
117
|
|
|
118
|
+
export function formatSubscriptionEnds(duration, resetTime, options = {}) {
|
|
119
|
+
if (duration) return lt('subscription_ends_in', { duration, time: resetTime }, options);
|
|
120
|
+
return lt('subscription_ends', { time: resetTime }, options);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function formatTrialEnds(duration, resetTime, options = {}) {
|
|
124
|
+
if (duration) return lt('trial_ends_in', { duration, time: resetTime }, options);
|
|
125
|
+
return lt('trial_ends', { time: resetTime }, options);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function formatSubscriptionStatus(status, options = {}) {
|
|
129
|
+
return lt('subscription_status', { status }, options);
|
|
130
|
+
}
|
|
131
|
+
|
|
113
132
|
export function formatLocalizedResetTime(isoDate, includeTimezone = true, options = {}) {
|
|
114
133
|
if (typeof includeTimezone === 'object') return formatLocalizedResetTime(isoDate, true, includeTimezone);
|
|
115
134
|
const locale = resolveLimitLocale(options);
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription metadata helpers for the /limits command.
|
|
3
|
+
*
|
|
4
|
+
* Surfaces the subscription end / trial end / status fields the underlying
|
|
5
|
+
* providers expose:
|
|
6
|
+
* - Claude: GET https://api.anthropic.com/api/oauth/profile
|
|
7
|
+
* - Codex: decoded id_token from ~/.codex/auth.json
|
|
8
|
+
*
|
|
9
|
+
* See `docs/case-studies/issue-1793/`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
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';
|
|
14
|
+
|
|
15
|
+
const PROFILE_API_ENDPOINT = 'https://api.anthropic.com/api/oauth/profile';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Render the localized "Subscription ends …" / "Trial ends …" / "Subscription: …"
|
|
19
|
+
* line for a tool. Returns '' when no displayable data is present.
|
|
20
|
+
*/
|
|
21
|
+
export function formatSubscriptionLines(subscription, options = {}) {
|
|
22
|
+
if (!subscription) return '';
|
|
23
|
+
const locale = resolveLimitLocale(options);
|
|
24
|
+
const buildLine = (iso, formatter) => {
|
|
25
|
+
const resetTime = formatLocalizedResetTime(iso, true, { locale });
|
|
26
|
+
return resetTime ? formatter(formatLocalizedRelativeTime(iso, { locale }), resetTime, { locale }) : null;
|
|
27
|
+
};
|
|
28
|
+
let line = null;
|
|
29
|
+
if (subscription.endsAt) line = buildLine(subscription.endsAt, formatSubscriptionEnds);
|
|
30
|
+
else if (subscription.trialEndsAt) line = buildLine(subscription.trialEndsAt, formatTrialEnds);
|
|
31
|
+
else if (subscription.status) line = formatSubscriptionStatus(subscription.status, { locale });
|
|
32
|
+
return line ? `${line}\n` : '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get Claude subscription metadata.
|
|
37
|
+
*
|
|
38
|
+
* Reads `~/.claude/.credentials.json` and queries Anthropic's
|
|
39
|
+
* `/api/oauth/profile` endpoint to surface what is currently published:
|
|
40
|
+
* - subscription_status ("active" / "inactive")
|
|
41
|
+
* - subscription_created_at (ISO timestamp)
|
|
42
|
+
* - claude_code_trial_ends_at (ISO timestamp; trials only)
|
|
43
|
+
* - subscriptionType from local creds (informational plan label)
|
|
44
|
+
*
|
|
45
|
+
* Anthropic does NOT currently expose a `subscription_ends_at` field for
|
|
46
|
+
* paid plans, so we never fabricate one. The renderer only emits a line
|
|
47
|
+
* when a value is available.
|
|
48
|
+
*
|
|
49
|
+
* @param {Object} opts
|
|
50
|
+
* @param {boolean} opts.verbose - Verbose logging
|
|
51
|
+
* @param {string} opts.credentialsPath - Override Claude credentials path
|
|
52
|
+
* @returns {Object} `{ success, subscription? , error? }`
|
|
53
|
+
*/
|
|
54
|
+
export async function getClaudeSubscriptionInfo({ verbose = false, credentialsPath = DEFAULT_CREDENTIALS_PATH } = {}) {
|
|
55
|
+
try {
|
|
56
|
+
const credentials = await readCredentials(credentialsPath, verbose);
|
|
57
|
+
if (!credentials) {
|
|
58
|
+
return {
|
|
59
|
+
success: false,
|
|
60
|
+
error: 'Could not read Claude credentials. Make sure Claude is properly installed and authenticated.',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const accessToken = credentials?.claudeAiOauth?.accessToken;
|
|
65
|
+
const planType = credentials?.claudeAiOauth?.subscriptionType || null;
|
|
66
|
+
|
|
67
|
+
if (!accessToken) {
|
|
68
|
+
return {
|
|
69
|
+
success: false,
|
|
70
|
+
error: 'No access token found in Claude credentials.',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const requestHeaders = {
|
|
75
|
+
Accept: 'application/json',
|
|
76
|
+
'Content-Type': 'application/json',
|
|
77
|
+
'User-Agent': 'claude-code/2.0.55',
|
|
78
|
+
Authorization: `Bearer ${accessToken}`,
|
|
79
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (verbose) {
|
|
83
|
+
console.log(`[VERBOSE] /limits subscription: GET ${PROFILE_API_ENDPOINT}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const response = await fetch(PROFILE_API_ENDPOINT, { method: 'GET', headers: requestHeaders });
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
if (verbose) {
|
|
89
|
+
console.error(`[VERBOSE] /limits subscription HTTP ${response.status} ${response.statusText}`);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
error: `Failed to fetch Claude profile: ${response.status} ${response.statusText}`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const data = await response.json();
|
|
98
|
+
if (verbose) {
|
|
99
|
+
console.log('[VERBOSE] /limits subscription body:', JSON.stringify(data, null, 2));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const organization = data?.organization || {};
|
|
103
|
+
return {
|
|
104
|
+
success: true,
|
|
105
|
+
subscription: {
|
|
106
|
+
planType,
|
|
107
|
+
status: organization.subscription_status || null,
|
|
108
|
+
createdAt: organization.subscription_created_at || null,
|
|
109
|
+
trialEndsAt: organization.claude_code_trial_ends_at || null,
|
|
110
|
+
endsAt: organization.subscription_ends_at || null,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (verbose) console.error('[VERBOSE] /limits subscription error:', error);
|
|
115
|
+
return { success: false, error: `Failed to get Claude subscription info: ${error.message}` };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get Codex subscription metadata.
|
|
121
|
+
*
|
|
122
|
+
* Decodes the OIDC `id_token` persisted in `~/.codex/auth.json`. The token's
|
|
123
|
+
* `https://api.openai.com/auth` claim carries the active ChatGPT subscription
|
|
124
|
+
* window — including `chatgpt_subscription_active_until`, which is exactly
|
|
125
|
+
* the renewal/end date the issue asks for. No HTTP call is needed.
|
|
126
|
+
*
|
|
127
|
+
* @param {Object} opts
|
|
128
|
+
* @param {boolean} opts.verbose
|
|
129
|
+
* @param {string} opts.authPath
|
|
130
|
+
* @returns {Object} `{ success, subscription? , error? }`
|
|
131
|
+
*/
|
|
132
|
+
export async function getCodexSubscriptionInfo({ verbose = false, authPath = DEFAULT_CODEX_AUTH_PATH } = {}) {
|
|
133
|
+
try {
|
|
134
|
+
const auth = await readCodexAuth(authPath, verbose);
|
|
135
|
+
if (!auth) {
|
|
136
|
+
return {
|
|
137
|
+
success: false,
|
|
138
|
+
error: 'Could not read Codex authentication.',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (auth.auth_mode && auth.auth_mode !== 'chatgpt') {
|
|
143
|
+
return {
|
|
144
|
+
success: false,
|
|
145
|
+
error: 'Codex subscription info is only available for ChatGPT-authenticated Codex.',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const idToken = auth?.tokens?.id_token || null;
|
|
150
|
+
const accessToken = auth?.tokens?.access_token || null;
|
|
151
|
+
const payload = decodeJwtPayload(idToken) || decodeJwtPayload(accessToken);
|
|
152
|
+
const claims = payload?.['https://api.openai.com/auth'] || null;
|
|
153
|
+
|
|
154
|
+
if (!claims) {
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
error: 'Could not decode Codex subscription claims from id_token.',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
success: true,
|
|
163
|
+
subscription: {
|
|
164
|
+
planType: claims.chatgpt_plan_type || null,
|
|
165
|
+
activeStart: claims.chatgpt_subscription_active_start || null,
|
|
166
|
+
endsAt: claims.chatgpt_subscription_active_until || null,
|
|
167
|
+
lastChecked: claims.chatgpt_subscription_last_checked || null,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (verbose) console.error('[VERBOSE] /limits Codex subscription error:', error);
|
|
172
|
+
return { success: false, error: `Failed to get Codex subscription info: ${error.message}` };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Cached Claude subscription metadata. Uses the same 20-minute
|
|
178
|
+
* TTL as `/limits` so we don't add traffic to the Anthropic OAuth API.
|
|
179
|
+
*/
|
|
180
|
+
export async function getCachedClaudeSubscription(verbose = false) {
|
|
181
|
+
const cache = getLimitCache();
|
|
182
|
+
const cached = cache.get('claude-subscription', CACHE_TTL.USAGE_API);
|
|
183
|
+
if (cached) {
|
|
184
|
+
if (verbose) console.log('[VERBOSE] /limits-cache: Using cached Claude subscription');
|
|
185
|
+
return cached;
|
|
186
|
+
}
|
|
187
|
+
const result = await getClaudeSubscriptionInfo({ verbose });
|
|
188
|
+
if (result.success) cache.set('claude-subscription', result, CACHE_TTL.USAGE_API);
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Cached Codex subscription metadata. The JWT decode is local, but we still
|
|
194
|
+
* cache for parity with the rest of the /limits pipeline.
|
|
195
|
+
*/
|
|
196
|
+
export async function getCachedCodexSubscription(verbose = false) {
|
|
197
|
+
const cache = getLimitCache();
|
|
198
|
+
const cached = cache.get('codex-subscription', CACHE_TTL.USAGE_API);
|
|
199
|
+
if (cached) {
|
|
200
|
+
if (verbose) console.log('[VERBOSE] /limits-cache: Using cached Codex subscription');
|
|
201
|
+
return cached;
|
|
202
|
+
}
|
|
203
|
+
const result = await getCodexSubscriptionInfo({ verbose });
|
|
204
|
+
if (result.success) cache.set('codex-subscription', result, CACHE_TTL.USAGE_API);
|
|
205
|
+
return result;
|
|
206
|
+
}
|
package/src/limits.lib.mjs
CHANGED
|
@@ -14,6 +14,8 @@ import utc from 'dayjs/plugin/utc.js';
|
|
|
14
14
|
|
|
15
15
|
import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry, execGhWithRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller. execGhWithRetry adds transient-network retry (#1756).
|
|
16
16
|
import { formatLimitResetsAt, formatLimitResetsIn, formatLocalizedCurrentTime, formatLocalizedRelativeTime, formatLocalizedResetTime, localizeCompactDuration, lt, resolveLimitLocale } from './limits-i18n.lib.mjs';
|
|
17
|
+
import { formatSubscriptionLines, getCachedClaudeSubscription, getCachedCodexSubscription, getClaudeSubscriptionInfo, getCodexSubscriptionInfo } from './limits-subscription.lib.mjs';
|
|
18
|
+
export { getCachedClaudeSubscription, getCachedCodexSubscription, getClaudeSubscriptionInfo, getCodexSubscriptionInfo };
|
|
17
19
|
// Initialize dayjs plugins
|
|
18
20
|
dayjs.extend(utc);
|
|
19
21
|
|
|
@@ -31,8 +33,8 @@ const execAsync = promisify(exec);
|
|
|
31
33
|
/**
|
|
32
34
|
* Default path to Claude credentials file
|
|
33
35
|
*/
|
|
34
|
-
const DEFAULT_CREDENTIALS_PATH = join(homedir(), '.claude', '.credentials.json');
|
|
35
|
-
const DEFAULT_CODEX_AUTH_PATH = join(homedir(), '.codex', 'auth.json');
|
|
36
|
+
export const DEFAULT_CREDENTIALS_PATH = join(homedir(), '.claude', '.credentials.json');
|
|
37
|
+
export const DEFAULT_CODEX_AUTH_PATH = join(homedir(), '.codex', 'auth.json');
|
|
36
38
|
const DEFAULT_CODEX_CONFIG_PATH = join(homedir(), '.codex', 'config.toml');
|
|
37
39
|
|
|
38
40
|
/**
|
|
@@ -41,7 +43,7 @@ const DEFAULT_CODEX_CONFIG_PATH = join(homedir(), '.codex', 'config.toml');
|
|
|
41
43
|
const USAGE_API_ENDPOINT = 'https://api.anthropic.com/api/oauth/usage';
|
|
42
44
|
const CODEX_USAGE_API_DEFAULT_BASE_URL = 'https://chatgpt.com/backend-api';
|
|
43
45
|
|
|
44
|
-
function decodeJwtPayload(token) {
|
|
46
|
+
export function decodeJwtPayload(token) {
|
|
45
47
|
if (!token || typeof token !== 'string') return null;
|
|
46
48
|
|
|
47
49
|
try {
|
|
@@ -73,7 +75,7 @@ function mapCodexWindow(window) {
|
|
|
73
75
|
};
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
async function readCodexAuth(authPath = DEFAULT_CODEX_AUTH_PATH, verbose = false) {
|
|
78
|
+
export async function readCodexAuth(authPath = DEFAULT_CODEX_AUTH_PATH, verbose = false) {
|
|
77
79
|
try {
|
|
78
80
|
const content = await readFile(authPath, 'utf-8');
|
|
79
81
|
const auth = JSON.parse(content);
|
|
@@ -121,7 +123,7 @@ async function getCodexUsageBaseUrl(configPath = DEFAULT_CODEX_CONFIG_PATH, verb
|
|
|
121
123
|
* @param {boolean} verbose - Whether to log verbose output
|
|
122
124
|
* @returns {Object|null} Credentials object or null if not found
|
|
123
125
|
*/
|
|
124
|
-
async function readCredentials(credentialsPath = DEFAULT_CREDENTIALS_PATH, verbose = false) {
|
|
126
|
+
export async function readCredentials(credentialsPath = DEFAULT_CREDENTIALS_PATH, verbose = false) {
|
|
125
127
|
try {
|
|
126
128
|
const content = await readFile(credentialsPath, 'utf-8');
|
|
127
129
|
const credentials = JSON.parse(content);
|
|
@@ -1000,6 +1002,7 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
|
|
|
1000
1002
|
extraSections = [];
|
|
1001
1003
|
}
|
|
1002
1004
|
const locale = resolveLimitLocale(options);
|
|
1005
|
+
const subscription = options?.subscription || null;
|
|
1003
1006
|
const sections = [];
|
|
1004
1007
|
|
|
1005
1008
|
sections.push(`${lt('current_time', {}, { locale })}: ${formatCurrentTime({ locale })}\n`);
|
|
@@ -1157,6 +1160,9 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
|
|
|
1157
1160
|
sonnetSection += `${lt('na', {}, { locale })}\n`;
|
|
1158
1161
|
}
|
|
1159
1162
|
sections.push(sonnetSection);
|
|
1163
|
+
|
|
1164
|
+
const subscriptionLines = formatSubscriptionLines(subscription, { locale });
|
|
1165
|
+
if (subscriptionLines) sections.push(subscriptionLines);
|
|
1160
1166
|
}
|
|
1161
1167
|
|
|
1162
1168
|
// Append any caller-provided extra sections (e.g. queue status) inside the code block
|
|
@@ -1187,6 +1193,7 @@ export function formatCodexLimitsSection(codexLimits, codexError = null, options
|
|
|
1187
1193
|
const additionalRateLimits = codexLimits?.additionalRateLimits || [];
|
|
1188
1194
|
const credits = codexLimits?.credits || null;
|
|
1189
1195
|
const planType = codexLimits?.planType || null;
|
|
1196
|
+
const subscription = options?.subscription || null;
|
|
1190
1197
|
|
|
1191
1198
|
let section = `${lt('codex_limits', {}, { locale })}\n`;
|
|
1192
1199
|
if (planType) {
|
|
@@ -1249,6 +1256,9 @@ export function formatCodexLimitsSection(codexLimits, codexError = null, options
|
|
|
1249
1256
|
section += `\n${lt('codex_credits', {}, { locale })}\n${creditSummary}\n`;
|
|
1250
1257
|
}
|
|
1251
1258
|
|
|
1259
|
+
const subscriptionLines = formatSubscriptionLines(subscription, { locale });
|
|
1260
|
+
if (subscriptionLines) section += subscriptionLines;
|
|
1261
|
+
|
|
1252
1262
|
return section;
|
|
1253
1263
|
}
|
|
1254
1264
|
|
|
@@ -1435,14 +1445,16 @@ export async function getCachedDiskInfo(verbose = false) {
|
|
|
1435
1445
|
}
|
|
1436
1446
|
|
|
1437
1447
|
export async function getAllCachedLimits(verbose = false) {
|
|
1438
|
-
const [claude, codex, github, memory, cpu, disk] = await Promise.all([getCachedClaudeLimits(verbose), getCachedCodexLimits(verbose), getCachedGitHubLimits(verbose), getCachedMemoryInfo(verbose), getCachedCpuInfo(verbose), getCachedDiskInfo(verbose)]);
|
|
1439
|
-
return { claude, codex, github, memory, cpu, disk };
|
|
1448
|
+
const [claude, codex, github, memory, cpu, disk, claudeSubscription, codexSubscription] = await Promise.all([getCachedClaudeLimits(verbose), getCachedCodexLimits(verbose), getCachedGitHubLimits(verbose), getCachedMemoryInfo(verbose), getCachedCpuInfo(verbose), getCachedDiskInfo(verbose), getCachedClaudeSubscription(verbose), getCachedCodexSubscription(verbose)]);
|
|
1449
|
+
return { claude, codex, github, memory, cpu, disk, claudeSubscription, codexSubscription };
|
|
1440
1450
|
}
|
|
1441
1451
|
|
|
1442
1452
|
export default {
|
|
1443
1453
|
// Raw functions (no caching)
|
|
1444
1454
|
getClaudeUsageLimits,
|
|
1445
1455
|
getCodexUsageLimits,
|
|
1456
|
+
getClaudeSubscriptionInfo,
|
|
1457
|
+
getCodexSubscriptionInfo,
|
|
1446
1458
|
getCpuLoadInfo,
|
|
1447
1459
|
getMemoryInfo,
|
|
1448
1460
|
getDiskSpaceInfo,
|
|
@@ -1461,6 +1473,8 @@ export default {
|
|
|
1461
1473
|
// Cached functions
|
|
1462
1474
|
getCachedClaudeLimits,
|
|
1463
1475
|
getCachedCodexLimits,
|
|
1476
|
+
getCachedClaudeSubscription,
|
|
1477
|
+
getCachedCodexSubscription,
|
|
1464
1478
|
getCachedGitHubLimits,
|
|
1465
1479
|
getCachedMemoryInfo,
|
|
1466
1480
|
getCachedCpuInfo,
|
package/src/locales/en.lino
CHANGED
|
@@ -135,6 +135,11 @@ en
|
|
|
135
135
|
limits.seven_day_sonnet_only "7d Sonnet only"
|
|
136
136
|
limits.session "session"
|
|
137
137
|
limits.start "Start"
|
|
138
|
+
limits.subscription_ends "Subscription ends {{time}}"
|
|
139
|
+
limits.subscription_ends_in "Subscription ends in {{duration}} ({{time}})"
|
|
140
|
+
limits.subscription_status "Subscription: {{status}}"
|
|
141
|
+
limits.trial_ends "Trial ends {{time}}"
|
|
142
|
+
limits.trial_ends_in "Trial ends in {{duration}} ({{time}})"
|
|
138
143
|
limits.unavailable "unavailable"
|
|
139
144
|
limits.unlimited "unlimited"
|
|
140
145
|
limits.used "used"
|
package/src/locales/hi.lino
CHANGED
|
@@ -135,6 +135,11 @@ hi
|
|
|
135
135
|
limits.seven_day_sonnet_only "7 दिन केवल Sonnet"
|
|
136
136
|
limits.session "सत्र"
|
|
137
137
|
limits.start "शुरुआत"
|
|
138
|
+
limits.subscription_ends "सदस्यता समाप्त होगी {{time}}"
|
|
139
|
+
limits.subscription_ends_in "सदस्यता {{duration}} में समाप्त होगी ({{time}})"
|
|
140
|
+
limits.subscription_status "सदस्यता: {{status}}"
|
|
141
|
+
limits.trial_ends "ट्रायल समाप्त होगा {{time}}"
|
|
142
|
+
limits.trial_ends_in "ट्रायल {{duration}} में समाप्त होगा ({{time}})"
|
|
138
143
|
limits.unavailable "अनुपलब्ध"
|
|
139
144
|
limits.unlimited "असीमित"
|
|
140
145
|
limits.used "उपयोग"
|
package/src/locales/ru.lino
CHANGED
|
@@ -135,6 +135,11 @@ ru
|
|
|
135
135
|
limits.seven_day_sonnet_only "7 дней, только Sonnet"
|
|
136
136
|
limits.session "сеанс"
|
|
137
137
|
limits.start "Начало"
|
|
138
|
+
limits.subscription_ends "Подписка заканчивается {{time}}"
|
|
139
|
+
limits.subscription_ends_in "Подписка заканчивается через {{duration}} ({{time}})"
|
|
140
|
+
limits.subscription_status "Подписка: {{status}}"
|
|
141
|
+
limits.trial_ends "Пробный период заканчивается {{time}}"
|
|
142
|
+
limits.trial_ends_in "Пробный период заканчивается через {{duration}} ({{time}})"
|
|
138
143
|
limits.unavailable "недоступно"
|
|
139
144
|
limits.unlimited "без ограничений"
|
|
140
145
|
limits.used "использовано"
|
package/src/locales/zh.lino
CHANGED
|
@@ -135,6 +135,11 @@ zh
|
|
|
135
135
|
limits.seven_day_sonnet_only "7 天仅 Sonnet"
|
|
136
136
|
limits.session "会话"
|
|
137
137
|
limits.start "开始"
|
|
138
|
+
limits.subscription_ends "订阅结束于 {{time}}"
|
|
139
|
+
limits.subscription_ends_in "订阅将在 {{duration}} 后结束 ({{time}})"
|
|
140
|
+
limits.subscription_status "订阅: {{status}}"
|
|
141
|
+
limits.trial_ends "试用结束于 {{time}}"
|
|
142
|
+
limits.trial_ends_in "试用将在 {{duration}} 后结束 ({{time}})"
|
|
138
143
|
limits.unavailable "不可用"
|
|
139
144
|
limits.unlimited "无限"
|
|
140
145
|
limits.used "已用"
|
|
@@ -24,9 +24,41 @@ const { log, ghCmdRetry } = lib;
|
|
|
24
24
|
const githubLib = await import('./github.lib.mjs');
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* Probe whether the repository allows forking via the GitHub API
|
|
28
|
+
* (`allow_forking` is true for repos that can be forked by users with read
|
|
29
|
+
* access — including private repositories). Returns `null` when the
|
|
30
|
+
* attribute could not be determined so callers can decide a safe default.
|
|
31
|
+
*
|
|
32
|
+
* Kept here (instead of github.lib.mjs) because it's only used by the
|
|
33
|
+
* auto-fork branch and the existing `detectRepositoryVisibility` already
|
|
34
|
+
* makes a separate API call; this avoids reshuffling shared helpers.
|
|
35
|
+
*
|
|
36
|
+
* Issue #1795: a private repository with read-only access can still be
|
|
37
|
+
* forked when `allow_forking` is true, so failing early was overly
|
|
38
|
+
* conservative.
|
|
39
|
+
*/
|
|
40
|
+
async function detectAllowForking(owner, repo) {
|
|
41
|
+
const result = await ghCmdRetry(() => $`gh api repos/${owner}/${repo} --jq .allow_forking`, { label: `allow_forking ${owner}/${repo}` });
|
|
42
|
+
if (result.code !== 0) return null;
|
|
43
|
+
const raw = result.stdout.toString().trim();
|
|
44
|
+
if (raw === 'true') return true;
|
|
45
|
+
if (raw === 'false') return false;
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function describeRepoPermissionLevel(permissions) {
|
|
50
|
+
if (permissions.admin === true) return 'Admin';
|
|
51
|
+
if (permissions.maintain === true) return 'Maintain';
|
|
52
|
+
if (permissions.push === true) return 'Write';
|
|
53
|
+
if (permissions.triage === true) return 'Triage';
|
|
54
|
+
if (permissions.pull === true) return 'Read';
|
|
55
|
+
return 'No confirmed repository access';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Handle the --auto-fork option: when the user lacks write access, attempt
|
|
60
|
+
* to enable fork mode (including for private repositories where
|
|
61
|
+
* `allow_forking` is true). Only fail when forking is also unavailable.
|
|
30
62
|
*
|
|
31
63
|
* Mutates argv.fork in place when fork mode is enabled.
|
|
32
64
|
*
|
|
@@ -51,19 +83,52 @@ export async function handleAutoForkOption({ owner, repo, argv, safeExit }) {
|
|
|
51
83
|
const { isPublic } = await detectRepositoryVisibility(owner, repo);
|
|
52
84
|
|
|
53
85
|
if (!isPublic) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
await
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
86
|
+
// Issue #1795: read access to a private repo is enough to fork it
|
|
87
|
+
// when the upstream allows forking. Probe `allow_forking` before
|
|
88
|
+
// bailing out so users with limited (read-only) access can still
|
|
89
|
+
// proceed via their own fork.
|
|
90
|
+
const allowForking = await detectAllowForking(owner, repo);
|
|
91
|
+
if (allowForking === false) {
|
|
92
|
+
const permissionLevel = describeRepoPermissionLevel(permissions);
|
|
93
|
+
|
|
94
|
+
await log('');
|
|
95
|
+
await log("❌ --auto-fork failed: Repository is private, you don't have write access, and forking is disabled", { level: 'error' });
|
|
96
|
+
await log('');
|
|
97
|
+
await log(' 🔍 What happened:', { level: 'error' });
|
|
98
|
+
await log(` Repository ${owner}/${repo} is private`, { level: 'error' });
|
|
99
|
+
await log(` Your detected GitHub repository access level is ${permissionLevel}`, { level: 'error' });
|
|
100
|
+
await log(` API permissions: ${JSON.stringify(permissions)}`, { level: 'error' });
|
|
101
|
+
await log(' Direct branch mode requires push/write access, but permissions.push is false', { level: 'error' });
|
|
102
|
+
await log(" Fork mode is also unavailable because the repository owner disabled private forking ('allow_forking' is false)", {
|
|
103
|
+
level: 'error',
|
|
104
|
+
});
|
|
105
|
+
await log('');
|
|
106
|
+
await log(' 💡 Solution:', { level: 'error' });
|
|
107
|
+
await log(' • To let Hive Mind work directly in this repository:', { level: 'error' });
|
|
108
|
+
await log(` Ask an owner/admin to open https://github.com/${owner}/${repo}/settings/access`, { level: 'error' });
|
|
109
|
+
await log(' Then add this GitHub account or its team with the Write role (Maintain/Admin also works)', { level: 'error' });
|
|
110
|
+
await log(' • To let Hive Mind work through a fork instead:', { level: 'error' });
|
|
111
|
+
await log(` Ask an owner/admin to open https://github.com/${owner}/${repo}/settings`, { level: 'error' });
|
|
112
|
+
await log(' Then enable Settings -> General -> Features -> Allow forking', { level: 'error' });
|
|
113
|
+
await log(' For organization-owned private repositories, the organization must also allow private repository forks', {
|
|
114
|
+
level: 'error',
|
|
115
|
+
});
|
|
116
|
+
await log('');
|
|
117
|
+
await safeExit(1, 'Auto-fork failed - private repository without access and forking is disabled');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (allowForking === true) {
|
|
122
|
+
await log('✅ Auto-fork: Read-only access to private repository, enabling fork mode (allow_forking=true)');
|
|
123
|
+
} else {
|
|
124
|
+
await log("✅ Auto-fork: Read-only access to private repository, attempting fork mode (allow_forking couldn't be confirmed)");
|
|
125
|
+
await log(" ⚠️ Could not determine 'allow_forking' for the private repository; letting gh repo fork report the exact result", {
|
|
126
|
+
verbose: true,
|
|
127
|
+
level: 'warning',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
argv.fork = true;
|
|
67
132
|
return;
|
|
68
133
|
}
|
|
69
134
|
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -566,8 +566,10 @@ bot.command('limits', async ctx => {
|
|
|
566
566
|
const codexError = limits.codex.success ? null : limits.codex.error;
|
|
567
567
|
const solveQueue = getSolveQueue({ verbose: VERBOSE });
|
|
568
568
|
const queueStatus = await solveQueue.formatStatus({ locale: userLocale });
|
|
569
|
-
const
|
|
570
|
-
const
|
|
569
|
+
const claudeSubscription = limits.claudeSubscription?.success ? limits.claudeSubscription.subscription : null;
|
|
570
|
+
const codexSubscription = limits.codexSubscription?.success ? limits.codexSubscription.subscription : null;
|
|
571
|
+
const codexSection = formatCodexLimitsSection(limits.codex.success ? limits.codex : null, codexError, { locale: userLocale, subscription: codexSubscription });
|
|
572
|
+
const message = t('telegram.usage_limits_title', {}, { locale: userLocale }) + '\n\n' + formatUsageMessage(limits.claude.success ? limits.claude.usage : null, limits.disk.success ? limits.disk.diskSpace : null, limits.github.success ? limits.github.githubRateLimit : null, limits.cpu.success ? limits.cpu.cpuLoad : null, limits.memory.success ? limits.memory.memory : null, claudeError, [codexSection, queueStatus], { locale: userLocale, subscription: claudeSubscription });
|
|
571
573
|
await safeEditMessageText(ctx.telegram, fetchingMessage.chat.id, fetchingMessage.message_id, undefined, message, { parse_mode: 'Markdown', fallbackLocale: userLocale, verbose: VERBOSE });
|
|
572
574
|
});
|
|
573
575
|
bot.command('version', async ctx => {
|