@link-assistant/hive-mind 1.69.12 → 1.69.14

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
+ ## 1.69.14
4
+
5
+ ### Patch Changes
6
+
7
+ - 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.
8
+
9
+ ## 1.69.13
10
+
11
+ ### Patch Changes
12
+
13
+ - 52dfa8e: Preserve pull request `.gitkeep` edits during final cleanup so intentional `.gitkeep` deletions are not re-added.
14
+
3
15
  ## 1.69.12
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.69.12",
3
+ "version": "1.69.14",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -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
+ }
@@ -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,
@@ -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"
@@ -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 "उपयोग"
@@ -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 "использовано"
@@ -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 "已用"
@@ -361,6 +361,20 @@ const detectClaudeMdCommitFromBranch = async (tempDir, branchName) => {
361
361
  }
362
362
  };
363
363
 
364
+ const wasFileTouchedAfterCommit = async (tempDir, commitHash, fileName) => {
365
+ const changedCommitsResult = await $({ cwd: tempDir, silent: true })`git log --format=%H ${commitHash}..HEAD -- ${fileName}`;
366
+ if (changedCommitsResult.code === 0) {
367
+ return Boolean(changedCommitsResult.stdout?.trim());
368
+ }
369
+
370
+ if (changedCommitsResult.code !== 0) {
371
+ await log(` Could not inspect ${fileName} changes after initial commit`, { verbose: true });
372
+ await log(` git log output: ${changedCommitsResult.stderr || changedCommitsResult.stdout || 'no output'}`, { verbose: true });
373
+ }
374
+
375
+ return true;
376
+ };
377
+
364
378
  // Revert the CLAUDE.md or .gitkeep commit to restore original state
365
379
  export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash = null) => {
366
380
  try {
@@ -406,6 +420,16 @@ export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash =
406
420
 
407
421
  const commitToRevert = claudeCommitHash;
408
422
 
423
+ // Issue #1791: .gitkeep is a normal repository file in some projects, and
424
+ // user work may intentionally edit or delete it. Once later PR commits touch
425
+ // .gitkeep, final cleanup must not restore the pre-session version.
426
+ if (fileName === '.gitkeep' && (await wasFileTouchedAfterCommit(tempDir, commitToRevert, fileName))) {
427
+ await log(` ${fileName} changed after the initial auto-commit; leaving PR changes untouched`, {
428
+ verbose: true,
429
+ });
430
+ return;
431
+ }
432
+
409
433
  // APPROACH 3: Check for modifications before reverting (proactive detection)
410
434
  // This is the main strategy - detect if the file was modified after initial commit
411
435
  await log(` Checking if ${fileName} was modified since initial commit...`, { verbose: true });
@@ -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 codexSection = formatCodexLimitsSection(limits.codex.success ? limits.codex : null, codexError, { locale: userLocale });
570
- 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 });
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 => {