@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 +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.results.lib.mjs +24 -0
- 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.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
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 "已用"
|
|
@@ -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 });
|
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 => {
|