@foxden-app/foxclaw 0.5.35 → 0.5.37

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
@@ -2,6 +2,26 @@
2
2
 
3
3
  All notable FoxClaw changes are listed here. Each release note is bilingual so GitHub Releases and the npm package are useful to both Chinese and English readers.
4
4
 
5
+ ## 0.5.37 - 2026-06-17
6
+
7
+ ### 中文
8
+ - 精简 `/auth` RichMessage 表格的 `Quota A` / `Quota B` 单元格:只显示剩余额度百分比,例如 `26%`,窗口含义交给列和后续套餐/状态信息理解。
9
+ - 保持原始文本 fallback 和按钮文本不变,避免影响旧命令、微信复制路径和 auth 切换回调。
10
+
11
+ ### English
12
+ - Simplified the `/auth` RichMessage `Quota A` / `Quota B` cells to show only the remaining percentage, such as `26%`, leaving the window context to the columns and following plan/status fields.
13
+ - Kept the plain-text fallback and button labels unchanged so legacy commands, Weixin copy-paste flows, and auth switching callbacks remain stable.
14
+
15
+ ## 0.5.36 - 2026-06-17
16
+
17
+ ### 中文
18
+ - `/auth` 的 Telegram RichMessage 候选区从旧的竖线分隔文本整理为横向表格,拆出额度窗口、auth 名称、当前标记、套餐、健康状态、最近刷新、过期时间和风险提示列。
19
+ - ChatGPT auth 候选会从 access token `exp` 读取过期时间,并以 UTC `YYYY-MM-DD HH:mmZ` 展示;拿不到过期字段时保持 `-`,原始文本 fallback 仍可展开查看。
20
+
21
+ ### English
22
+ - Refined the Telegram RichMessage `/auth` candidate area from pipe-delimited text into a horizontally scrollable table with quota windows, auth name, current marker, plan, health, last refresh, expiry, and risk columns.
23
+ - ChatGPT auth candidates now read the access token `exp` value and display expiry as UTC `YYYY-MM-DD HH:mmZ`; candidates without an expiry field show `-`, with the expandable plain-text fallback preserved.
24
+
5
25
  ## 0.5.35 - 2026-06-17
6
26
 
7
27
  ### 中文
@@ -3,7 +3,8 @@ import fs from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { normalizeLocale, t } from '../i18n.js';
6
- import { chatGptAuthMetadataMatchesCandidateName, parseChatGptAuthMetadata, readChatGptAuthMetadata, } from '../auth/mirror.js';
6
+ import { chatGptAuthMetadataMatchesCandidateName, parseChatGptAuthMetadata, readChatGptAuthRecord, readChatGptAuthMetadata, } from '../auth/mirror.js';
7
+ import { readAccessTokenExpiresAtMs } from '../auth/cross_node_sync.js';
7
8
  import { parseCommand } from './commands.js';
8
9
  import { buildAccessSettingsKeyboard, buildModelSettingsKeyboard, buildSetupPanelKeyboard, buildThreadListKeyboard, buildThreadsKeyboard, clampEffortToModel, formatAccessPresetLabel, formatActiveTurnMessageModeLabel, formatAccessSettingsMessage, formatApprovalPolicyLabel, formatCollaborationModeLabel, formatModelSettingsMessage, formatSandboxModeLabel, formatServiceTierStatusLabel, formatSetupPanelMessage, formatThreadContextSummary, formatThreadsMessage, formatWeixinAccessCopyPaste, formatWeixinModelCopyPaste, formatWeixinThreadsCopyPaste, formatWeixinWhereNavCopyPaste, formatWhereMessage, normalizeRequestedEffort, resolveCurrentModel, resolveActiveTurnMessageMode, resolveRequestedModel, } from './presentation.js';
9
10
  import { clampServiceTierToModel, resolveFastTierForModel } from './service_tier.js';
@@ -6234,19 +6235,20 @@ export class BridgeSessionCore {
6234
6235
  }
6235
6236
  async readCodexAuthCandidateQuotaIdentities(candidates) {
6236
6237
  const entries = await Promise.all(candidates.map(async (candidate) => {
6237
- const metadata = await readChatGptAuthMetadata(candidate.path);
6238
- const metadataMatchesName = metadata
6239
- ? chatGptAuthMetadataMatchesCandidateName(candidate.name, metadata)
6238
+ const record = await readChatGptAuthRecord(candidate.path);
6239
+ const metadataMatchesName = record
6240
+ ? chatGptAuthMetadataMatchesCandidateName(candidate.name, record)
6240
6241
  : false;
6241
- candidate.credentialKind = metadata && metadataMatchesName
6242
+ candidate.credentialKind = record && metadataMatchesName
6242
6243
  ? 'chatgpt'
6243
6244
  : await isCodexApiKeyAuthCandidate(candidate.path)
6244
6245
  ? 'api-key'
6245
6246
  : 'invalid';
6246
- candidate.credentialLastRefreshMs = metadata && metadataMatchesName ? metadata.lastRefreshMs : null;
6247
- return [candidate.name, metadata && metadataMatchesName ? {
6248
- accountId: metadata.accountId,
6249
- quotaIdentityId: metadata.quotaIdentityId,
6247
+ candidate.credentialLastRefreshMs = record && metadataMatchesName ? record.lastRefreshMs : null;
6248
+ candidate.credentialExpiresAtMs = record && metadataMatchesName ? readAccessTokenExpiresAtMs(record.raw) : null;
6249
+ return [candidate.name, record && metadataMatchesName ? {
6250
+ accountId: record.accountId,
6251
+ quotaIdentityId: record.quotaIdentityId,
6250
6252
  } : null];
6251
6253
  }));
6252
6254
  const quotaIdentities = new Map();
@@ -8479,6 +8481,19 @@ function splitRichInternalSections(text) {
8479
8481
  return sections;
8480
8482
  }
8481
8483
  function formatRichInternalSection(lines) {
8484
+ const candidateRows = lines
8485
+ .map(parseRichAuthCandidateRow)
8486
+ .filter((row) => row !== null);
8487
+ if (candidateRows.length > 0) {
8488
+ const nonCandidateLines = lines.filter(line => parseRichAuthCandidateRow(line) === null);
8489
+ return [
8490
+ nonCandidateLines.length > 0 ? formatRichInternalSectionWithoutCandidates(nonCandidateLines) : '',
8491
+ formatRichAuthCandidateTable(candidateRows),
8492
+ ].filter(Boolean).join('\n');
8493
+ }
8494
+ return formatRichInternalSectionWithoutCandidates(lines);
8495
+ }
8496
+ function formatRichInternalSectionWithoutCandidates(lines) {
8482
8497
  const rows = lines
8483
8498
  .map(parseRichInternalKeyValue)
8484
8499
  .filter((row) => row !== null);
@@ -8495,6 +8510,107 @@ function formatRichInternalSection(lines) {
8495
8510
  '</ul>',
8496
8511
  ].join('\n');
8497
8512
  }
8513
+ function parseRichAuthCandidateRow(line) {
8514
+ const match = line.match(/^\s*(\d+)\.\s+(.+)$/);
8515
+ if (!match) {
8516
+ return null;
8517
+ }
8518
+ let body = match[2].trim();
8519
+ let status = '';
8520
+ const statusMatch = body.match(/^(.*?)\s+(\[[^\]]+\])$/);
8521
+ if (statusMatch) {
8522
+ body = statusMatch[1].trim();
8523
+ status = statusMatch[2].replace(/^\[|\]$/g, '');
8524
+ }
8525
+ const current = body.endsWith(' *');
8526
+ if (current) {
8527
+ body = body.slice(0, -2).trimEnd();
8528
+ }
8529
+ const parts = body.split('|');
8530
+ const name = parts.pop()?.trim() ?? '';
8531
+ if (!name) {
8532
+ return null;
8533
+ }
8534
+ const quotas = parts.map(formatRichAuthQuotaCell);
8535
+ const statusParts = splitRichAuthStatus(status);
8536
+ return {
8537
+ index: match[1],
8538
+ quotaA: quotas[0] ?? '-',
8539
+ quotaB: quotas[1] ?? '-',
8540
+ name,
8541
+ current,
8542
+ ...statusParts,
8543
+ };
8544
+ }
8545
+ function formatRichAuthCandidateTable(rows) {
8546
+ return [
8547
+ '<table bordered striped>',
8548
+ '<tr><th>#</th><th>Quota A</th><th>Quota B</th><th>Auth</th><th>Current</th><th>Plan</th><th>Health</th><th>Last refresh</th><th>Expiry</th><th>Risk</th></tr>',
8549
+ ...rows.map(row => [
8550
+ '<tr>',
8551
+ `<td>${escapeTelegramHtml(row.index)}</td>`,
8552
+ `<td>${escapeTelegramHtml(row.quotaA)}</td>`,
8553
+ `<td>${escapeTelegramHtml(row.quotaB)}</td>`,
8554
+ `<td>${escapeTelegramHtml(row.name)}</td>`,
8555
+ `<td>${row.current ? 'yes' : '-'}</td>`,
8556
+ `<td>${escapeTelegramHtml(row.plan)}</td>`,
8557
+ `<td>${escapeTelegramHtml(row.health)}</td>`,
8558
+ `<td>${escapeTelegramHtml(row.refresh)}</td>`,
8559
+ `<td>${escapeTelegramHtml(row.expiry)}</td>`,
8560
+ `<td>${escapeTelegramHtml(row.risk)}</td>`,
8561
+ '</tr>',
8562
+ ].join('')),
8563
+ '</table>',
8564
+ ].join('\n');
8565
+ }
8566
+ function splitRichAuthStatus(status) {
8567
+ const parts = status.split(' · ').map(part => part.trim()).filter(Boolean);
8568
+ const refreshIndex = parts.findIndex(part => /^(refreshed|刷新于)\b/i.test(part));
8569
+ const refresh = refreshIndex >= 0 ? parts.splice(refreshIndex, 1)[0] : '-';
8570
+ const expiryIndex = parts.findIndex(part => /^(expires|过期于)\b/i.test(part));
8571
+ const expiry = expiryIndex >= 0 ? parts.splice(expiryIndex, 1)[0] : '-';
8572
+ const plan = parts.length > 1 ? parts.shift() : '-';
8573
+ const health = parts.shift() ?? '-';
8574
+ return {
8575
+ plan,
8576
+ health,
8577
+ refresh,
8578
+ expiry,
8579
+ risk: formatRichAuthRisk(health),
8580
+ };
8581
+ }
8582
+ function formatRichAuthRisk(health) {
8583
+ if (/not recently refreshed|长期未刷新/i.test(health)) {
8584
+ return `stale >${CODEX_AUTH_STALE_CREDENTIAL_DAYS}d`;
8585
+ }
8586
+ if (/needs login repair|需要登录修复/i.test(health)) {
8587
+ return 'repair';
8588
+ }
8589
+ if (/quota exhausted|额度耗尽/i.test(health)) {
8590
+ return 'quota exhausted';
8591
+ }
8592
+ if (/invalid|无效/i.test(health)) {
8593
+ return 'invalid';
8594
+ }
8595
+ if (/\blow\b|偏低|低/i.test(health)) {
8596
+ return 'low quota';
8597
+ }
8598
+ if (/unknown|未知/i.test(health)) {
8599
+ return 'unknown';
8600
+ }
8601
+ return '-';
8602
+ }
8603
+ function formatRichAuthQuotaCell(value) {
8604
+ const trimmed = value.trim();
8605
+ if (!trimmed || trimmed === '--' || trimmed === '—') {
8606
+ return '-';
8607
+ }
8608
+ const [windowLabel, percent] = trimmed.split(':');
8609
+ if (!windowLabel || percent === undefined) {
8610
+ return trimmed;
8611
+ }
8612
+ return `${percent}%`;
8613
+ }
8498
8614
  function parseRichInternalKeyValue(line) {
8499
8615
  const separator = line.includes(':') ? ':' : ':';
8500
8616
  const index = line.indexOf(separator);
@@ -9269,6 +9385,7 @@ async function listCodexAuthState(disabledNames = new Set(), candidateStates = n
9269
9385
  mtimeMs: stat.mtimeMs,
9270
9386
  credentialKind: 'invalid',
9271
9387
  credentialLastRefreshMs: null,
9388
+ credentialExpiresAtMs: null,
9272
9389
  quota: null,
9273
9390
  });
9274
9391
  }
@@ -9613,6 +9730,11 @@ function formatCodexAuthCandidateStatus(locale, candidate) {
9613
9730
  value: formatCompactAge(locale, Date.now() - candidate.credentialLastRefreshMs),
9614
9731
  }));
9615
9732
  }
9733
+ if (candidate.credentialExpiresAtMs !== null) {
9734
+ details.push(t(locale, 'auth_candidate_expires_at', {
9735
+ value: formatUtcDateTime(candidate.credentialExpiresAtMs),
9736
+ }));
9737
+ }
9616
9738
  return `[${details.join(' · ')}]`;
9617
9739
  }
9618
9740
  function formatCodexAuthFilter(locale, filter) {
@@ -10459,6 +10581,12 @@ function formatCompactAge(locale, ageMs) {
10459
10581
  const roundedMinutes = Math.floor(minutes);
10460
10582
  return locale === 'zh' ? `${roundedMinutes}分钟前` : `${roundedMinutes}m ago`;
10461
10583
  }
10584
+ function formatUtcDateTime(timestampMs) {
10585
+ if (!Number.isFinite(timestampMs)) {
10586
+ return '-';
10587
+ }
10588
+ return `${new Date(timestampMs).toISOString().slice(0, 16).replace('T', ' ')}Z`;
10589
+ }
10462
10590
  function formatUsagePercent(value) {
10463
10591
  if (!Number.isFinite(value)) {
10464
10592
  return '?';
package/dist/i18n.d.ts CHANGED
@@ -173,6 +173,7 @@ declare const MESSAGES: {
173
173
  readonly 'auth_candidate_health_api-key': "API key";
174
174
  readonly auth_candidate_health_invalid: "invalid auth file";
175
175
  readonly auth_candidate_last_refresh: "refreshed {value}";
176
+ readonly auth_candidate_expires_at: "expires {value}";
176
177
  readonly auth_candidate_enabled: "Enabled auth candidate for auto-rotation: {value}";
177
178
  readonly auth_candidate_disabled: "Disabled auth candidate for auto-rotation: {value}";
178
179
  readonly auth_candidate_enabled_short: "Auth enabled";
@@ -871,6 +872,7 @@ declare const MESSAGES: {
871
872
  readonly 'auth_candidate_health_api-key': "API key";
872
873
  readonly auth_candidate_health_invalid: "auth 文件无效";
873
874
  readonly auth_candidate_last_refresh: "刷新于 {value}";
875
+ readonly auth_candidate_expires_at: "过期于 {value}";
874
876
  readonly auth_candidate_enabled: "已启用 auth 候选自动轮换:{value}";
875
877
  readonly auth_candidate_disabled: "已禁用 auth 候选自动轮换:{value}";
876
878
  readonly auth_candidate_enabled_short: "auth 已启用";
package/dist/i18n.js CHANGED
@@ -171,6 +171,7 @@ const MESSAGES = {
171
171
  'auth_candidate_health_api-key': 'API key',
172
172
  auth_candidate_health_invalid: 'invalid auth file',
173
173
  auth_candidate_last_refresh: 'refreshed {value}',
174
+ auth_candidate_expires_at: 'expires {value}',
174
175
  auth_candidate_enabled: 'Enabled auth candidate for auto-rotation: {value}',
175
176
  auth_candidate_disabled: 'Disabled auth candidate for auto-rotation: {value}',
176
177
  auth_candidate_enabled_short: 'Auth enabled',
@@ -869,6 +870,7 @@ const MESSAGES = {
869
870
  'auth_candidate_health_api-key': 'API key',
870
871
  auth_candidate_health_invalid: 'auth 文件无效',
871
872
  auth_candidate_last_refresh: '刷新于 {value}',
873
+ auth_candidate_expires_at: '过期于 {value}',
872
874
  auth_candidate_enabled: '已启用 auth 候选自动轮换:{value}',
873
875
  auth_candidate_disabled: '已禁用 auth 候选自动轮换:{value}',
874
876
  auth_candidate_enabled_short: 'auth 已启用',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.5.35",
3
+ "version": "0.5.37",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",