@foxden-app/foxclaw 0.4.6 → 0.4.7

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,20 @@
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.4.7 - 2026-06-02
6
+
7
+ ### 中文
8
+ - `/auth` 面板新增“刷新全部”按钮,并支持 `/auth refresh all`。
9
+ - 刷新全部仅在所有 Telegram runtime、微信 runtime、审批、待输入、登录流程和 auth 镜像写入都空闲时执行。
10
+ - 执行时逐个切换 ChatGPT 候选,调用 Codex `account/read refreshToken=true` 主动刷新,使用 usage 接口验证后再镜像同步,并在结束后恢复原当前 auth、刷新面板摘要。
11
+ - 更新 FoxClaw/npm 发布 skill:用户要求收尾或发布时,校验通过后自动 commit、push、publish,只在真实外部阻塞时再等待用户。
12
+
13
+ ### English
14
+ - Added a Refresh all button to the `/auth` panel and the `/auth refresh all` command.
15
+ - Refresh all runs only when every Telegram runtime, the Weixin runtime, approvals, inputs, login flows, and auth mirror writes are idle.
16
+ - It visits each ChatGPT candidate, calls Codex `account/read refreshToken=true`, validates through the usage endpoint before mirroring, restores the original current auth, and refreshes the panel summary.
17
+ - Updated the FoxClaw/npm publish skills so release wrap-up proceeds through commit, push, and publish after validation, pausing only for real external blockers.
18
+
5
19
  ## 0.4.6 - 2026-06-01
6
20
 
7
21
  ### 中文
package/README.md CHANGED
@@ -258,7 +258,7 @@ FoxClaw 会把 `codex app-server` 作为 detached 子进程启动,记录其 pi
258
258
  - `/status`、`/account`、`/quota`、`/update`
259
259
  - `/quota_nudge <credits|usage_limit> confirm`
260
260
  - `/login_device`、`/login_cancel [id]`、`/logout confirm`
261
- - `/auth [list|use <n>|enable <n>|disable <n>|reload|add <name>]`
261
+ - `/auth [list|use <n>|enable <n>|disable <n>|reload|refresh all|add <name>]`
262
262
  - `/threads [query]`、`/threads archived`、`/open <n>`
263
263
  - `/goal [objective|pause|resume|done|budget <tokens|off>|clear confirm]`
264
264
  - `/history [limit]`、`/files <query>`、`/remote`
package/README_EN.md CHANGED
@@ -258,7 +258,7 @@ No static Codex app-server port is required in normal installs.
258
258
  - `/status`, `/account`, `/quota`, `/update`
259
259
  - `/quota_nudge <credits|usage_limit> confirm`
260
260
  - `/login_device`, `/login_cancel [id]`, `/logout confirm`
261
- - `/auth [list|use <n>|enable <n>|disable <n>|reload|add <name>]`
261
+ - `/auth [list|use <n>|enable <n>|disable <n>|reload|refresh all|add <name>]`
262
262
  - `/threads [query]`, `/threads archived`, `/open <n>`
263
263
  - `/goal [objective|pause|resume|done|budget <tokens|off>|clear confirm]`
264
264
  - `/history [limit]`, `/files <query>`, `/remote`
@@ -125,7 +125,7 @@ export declare class CodexAppClient extends EventEmitter {
125
125
  reviewThreadId: string;
126
126
  }>;
127
127
  listModels(): Promise<ModelInfo[]>;
128
- readAccount(): Promise<CodexAccountInfo | null>;
128
+ readAccount(refreshToken?: boolean): Promise<CodexAccountInfo | null>;
129
129
  startDeviceLogin(): Promise<CodexLoginDeviceCode>;
130
130
  cancelLogin(loginId: string): Promise<void>;
131
131
  logoutAccount(): Promise<void>;
@@ -277,8 +277,8 @@ export class CodexAppClient extends EventEmitter {
277
277
  } while (cursor);
278
278
  return models;
279
279
  }
280
- async readAccount() {
281
- const result = await this.request('account/read', { refreshToken: false });
280
+ async readAccount(refreshToken = false) {
281
+ const result = await this.request('account/read', { refreshToken });
282
282
  const account = result?.account;
283
283
  if (!account || typeof account !== 'object') {
284
284
  return null;
@@ -45,6 +45,7 @@ export declare class BridgeSessionCore {
45
45
  private pendingApprovalMessages;
46
46
  private pendingAuthRotation;
47
47
  private authRotationInProgress;
48
+ private authRefreshAllInProgress;
48
49
  private authRotationFailedTargets;
49
50
  private localUsageCache;
50
51
  private localUsageCacheLoaded;
@@ -231,12 +232,14 @@ export declare class BridgeSessionCore {
231
232
  private handleFastCommand;
232
233
  private setCollaborationMode;
233
234
  private handleAuthReloadCommand;
235
+ private canRunGlobalAuthRefresh;
234
236
  private handleSelfUpdateCommand;
235
237
  private scheduleSelfUpdateStatusPoll;
236
238
  private clearSelfUpdateStatusPoll;
237
239
  private pollSelfUpdateStatus;
238
240
  private formatSelfUpdateResult;
239
241
  private handleAuthCommand;
242
+ private handleAuthRefreshAllCommand;
240
243
  private handleAuthUseCommand;
241
244
  private handleAuthToggleCommand;
242
245
  private findLatestAuthChoiceList;
@@ -288,6 +291,7 @@ export declare class BridgeSessionCore {
288
291
  private switchCodexAuthAndRestart;
289
292
  private recoverCodexAuthCandidate;
290
293
  private syncCodexAuthCandidate;
294
+ private refreshAllCodexAuthCandidates;
291
295
  private buildNativeCollaborationMode;
292
296
  private buildCodexUsageStatusLines;
293
297
  private buildCodexLocalUsageStatusLines;
@@ -299,6 +303,7 @@ export declare class BridgeSessionCore {
299
303
  private codexLocalUsageSnapshotPath;
300
304
  private refreshCurrentCodexAuthQuota;
301
305
  private applySharedCodexAuthQuotaSnapshots;
306
+ private recordCodexAuthQuotaSnapshot;
302
307
  private readCodexAuthCandidateAccountIds;
303
308
  private codexAuthQuotaSnapshotMatchesAccount;
304
309
  private readCodexAuthQuotaSnapshots;
@@ -113,6 +113,7 @@ export class BridgeSessionCore {
113
113
  pendingApprovalMessages = new Map();
114
114
  pendingAuthRotation = null;
115
115
  authRotationInProgress = false;
116
+ authRefreshAllInProgress = false;
116
117
  authRotationFailedTargets = new Set();
117
118
  localUsageCache = null;
118
119
  localUsageCacheLoaded = false;
@@ -232,6 +233,7 @@ export class BridgeSessionCore {
232
233
  this.recentCommandUsageByScope.clear();
233
234
  this.commandUsageSequence = 0;
234
235
  this.pendingAuthRotation = null;
236
+ this.authRefreshAllInProgress = false;
235
237
  this.clearObservedThreadWatchers();
236
238
  this.releaseActiveTurnsForBridgeShutdown();
237
239
  this.bot.stop();
@@ -1008,7 +1010,7 @@ export class BridgeSessionCore {
1008
1010
  await this.handleAuthToggleCallback(event, authToggleMatch[1], Number.parseInt(authToggleMatch[2], 10), locale);
1009
1011
  return;
1010
1012
  }
1011
- const authActionMatch = /^auth:([a-f0-9]+):(login_device|reload)$/.exec(event.data);
1013
+ const authActionMatch = /^auth:([a-f0-9]+):(login_device|reload|refresh_all)$/.exec(event.data);
1012
1014
  if (authActionMatch) {
1013
1015
  await this.handleAuthPanelActionCallback(event, authActionMatch[1], authActionMatch[2], locale);
1014
1016
  return;
@@ -2604,7 +2606,8 @@ export class BridgeSessionCore {
2604
2606
  && this.pendingUserInputs.size === 0
2605
2607
  && this.pendingMcpElicitations.size === 0
2606
2608
  && this.pendingLoginsByScope.size === 0
2607
- && !this.authRotationInProgress;
2609
+ && !this.authRotationInProgress
2610
+ && !this.authRefreshAllInProgress;
2608
2611
  }
2609
2612
  hasLocalBlockingActivity() {
2610
2613
  return !this.isIdleForServiceUpdate();
@@ -3779,6 +3782,12 @@ export class BridgeSessionCore {
3779
3782
  lines.push(...await this.buildCodexUsageStatusLines(locale));
3780
3783
  await this.sendMessage(scopeId, lines.join('\n'));
3781
3784
  }
3785
+ canRunGlobalAuthRefresh() {
3786
+ return this.isIdleForServiceUpdate()
3787
+ && this.store.countPendingApprovals() === 0
3788
+ && this.store.countPendingUserInputs() === 0
3789
+ && (!this.coordinator?.canSelfUpdate || this.coordinator.canSelfUpdate());
3790
+ }
3782
3791
  async handleSelfUpdateCommand(scopeId, locale) {
3783
3792
  if (!this.selfUpdater) {
3784
3793
  await this.sendMessage(scopeId, t(locale, 'update_unavailable'));
@@ -3860,6 +3869,18 @@ export class BridgeSessionCore {
3860
3869
  await this.handleAuthReloadCommand(scopeId, locale);
3861
3870
  return;
3862
3871
  }
3872
+ if (action === 'refresh') {
3873
+ if (args[1]?.toLowerCase() === 'all') {
3874
+ await this.handleAuthRefreshAllCommand(scopeId, locale);
3875
+ return;
3876
+ }
3877
+ await this.sendMessage(scopeId, t(locale, 'usage_auth'));
3878
+ return;
3879
+ }
3880
+ if (action === 'refresh_all') {
3881
+ await this.handleAuthRefreshAllCommand(scopeId, locale);
3882
+ return;
3883
+ }
3863
3884
  if (action === 'use') {
3864
3885
  await this.handleAuthUseCommand(scopeId, locale, args.slice(1));
3865
3886
  return;
@@ -3889,6 +3910,26 @@ export class BridgeSessionCore {
3889
3910
  const messageId = await this.sendMessage(scopeId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(scopeId) !== null), authChoiceKeyboard(locale, record));
3890
3911
  record.messageId = messageId;
3891
3912
  }
3913
+ async handleAuthRefreshAllCommand(scopeId, locale) {
3914
+ if (!this.canRunGlobalAuthRefresh()) {
3915
+ await this.sendMessage(scopeId, t(locale, 'auth_refresh_all_blocked_active'));
3916
+ return;
3917
+ }
3918
+ await this.sendMessage(scopeId, t(locale, 'auth_refresh_all_starting'));
3919
+ const result = await this.refreshAllCodexAuthCandidates();
3920
+ const state = await this.listCodexAuthState();
3921
+ await this.applySharedCodexAuthQuotaSnapshots(state);
3922
+ const record = {
3923
+ localId: crypto.randomBytes(8).toString('hex'),
3924
+ chatId: scopeId,
3925
+ messageId: null,
3926
+ candidates: state.candidates,
3927
+ createdAt: Date.now(),
3928
+ };
3929
+ this.pendingAuthChoiceLists.set(record.localId, record);
3930
+ const messageId = await this.sendMessage(scopeId, `${formatAuthRefreshAllResult(locale, result)}\n\n${renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(scopeId) !== null)}`, authChoiceKeyboard(locale, record));
3931
+ record.messageId = messageId;
3932
+ }
3892
3933
  async handleAuthUseCommand(scopeId, locale, args) {
3893
3934
  if (this.hasLocalBlockingActivity()) {
3894
3935
  await this.sendMessage(scopeId, t(locale, 'auth_reload_blocked_active'));
@@ -4361,6 +4402,25 @@ export class BridgeSessionCore {
4361
4402
  await this.handleLoginDeviceCommand(event.scopeId, locale);
4362
4403
  return;
4363
4404
  }
4405
+ if (action === 'refresh_all') {
4406
+ if (!this.canRunGlobalAuthRefresh()) {
4407
+ await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_refresh_all_blocked_active'));
4408
+ return;
4409
+ }
4410
+ await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_refresh_all_starting'));
4411
+ if (record.messageId !== null) {
4412
+ await this.editMessage(event.scopeId, record.messageId, t(locale, 'auth_refresh_all_starting'), []);
4413
+ }
4414
+ const result = await this.refreshAllCodexAuthCandidates();
4415
+ const state = await this.listCodexAuthState();
4416
+ await this.applySharedCodexAuthQuotaSnapshots(state);
4417
+ record.candidates = state.candidates;
4418
+ record.createdAt = Date.now();
4419
+ if (record.messageId !== null) {
4420
+ await this.editMessage(event.scopeId, record.messageId, `${formatAuthRefreshAllResult(locale, result)}\n\n${renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null)}`, authChoiceKeyboard(locale, record));
4421
+ }
4422
+ return;
4423
+ }
4364
4424
  await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_reload_restarting'));
4365
4425
  await this.handleAuthReloadCommand(event.scopeId, locale);
4366
4426
  }
@@ -4603,6 +4663,71 @@ export class BridgeSessionCore {
4603
4663
  });
4604
4664
  }
4605
4665
  }
4666
+ async refreshAllCodexAuthCandidates() {
4667
+ if (this.authRefreshAllInProgress) {
4668
+ throw new UserFacingError('Auth refresh all is already running.');
4669
+ }
4670
+ const initialState = await this.listCodexAuthState();
4671
+ const result = { refreshed: [], skipped: [], failed: [] };
4672
+ if (initialState.candidates.length === 0) {
4673
+ return result;
4674
+ }
4675
+ const authStat = await fs.lstat(initialState.authPath).catch(() => null);
4676
+ const originalRegularAuth = authStat?.isFile()
4677
+ ? await fs.readFile(initialState.authPath, 'utf8').catch(() => null)
4678
+ : null;
4679
+ let changedAuthTarget = false;
4680
+ this.authRefreshAllInProgress = true;
4681
+ try {
4682
+ for (const candidate of initialState.candidates) {
4683
+ const before = await readChatGptAuthMetadata(candidate.path);
4684
+ if (!before) {
4685
+ result.skipped.push(candidate.name);
4686
+ continue;
4687
+ }
4688
+ try {
4689
+ await pointCodexAuthAtTarget(initialState.authDir, initialState.authPath, candidate.path);
4690
+ changedAuthTarget = true;
4691
+ this.pendingTurnErrors.clear();
4692
+ this.attachedThreads.clear();
4693
+ await this.app.restart();
4694
+ await this.app.readAccount(true);
4695
+ const rateLimits = await this.app.readAccountRateLimits();
4696
+ const snapshot = selectCodexRateLimitSnapshot(rateLimits);
4697
+ if (!snapshot) {
4698
+ throw new Error('Codex did not return ChatGPT rate limits after refresh');
4699
+ }
4700
+ const after = await readChatGptAuthMetadata(candidate.path);
4701
+ if (!after || after.accountId !== before.accountId) {
4702
+ throw new Error('refreshed auth account id did not match the original candidate');
4703
+ }
4704
+ if (after.lastRefreshMs <= before.lastRefreshMs) {
4705
+ throw new Error('Codex did not advance last_refresh');
4706
+ }
4707
+ await this.recordCodexAuthQuotaSnapshot(candidate.name, after.accountId, snapshot);
4708
+ await this.syncCodexAuthCandidate(candidate.name);
4709
+ result.refreshed.push(candidate.name);
4710
+ }
4711
+ catch (error) {
4712
+ result.failed.push({ name: candidate.name, error: formatUserError(error) });
4713
+ }
4714
+ }
4715
+ }
4716
+ finally {
4717
+ try {
4718
+ if (changedAuthTarget) {
4719
+ await restoreCodexAuthTarget(initialState.authDir, initialState.authPath, initialState.currentTargetPath, originalRegularAuth);
4720
+ this.pendingTurnErrors.clear();
4721
+ this.attachedThreads.clear();
4722
+ await this.app.restart();
4723
+ }
4724
+ }
4725
+ finally {
4726
+ this.authRefreshAllInProgress = false;
4727
+ }
4728
+ }
4729
+ return result;
4730
+ }
4606
4731
  async buildNativeCollaborationMode(settings, cwd, modeOverride) {
4607
4732
  const mode = resolveCollaborationMode(modeOverride === undefined ? settings?.collaborationMode ?? null : modeOverride);
4608
4733
  try {
@@ -4807,11 +4932,7 @@ export class BridgeSessionCore {
4807
4932
  }
4808
4933
  const quota = authQuotaSnapshotFromRateLimit(snapshot, metadata?.accountId ?? null);
4809
4934
  candidate.quota = quota;
4810
- this.authQuotaSnapshots[candidate.name] = quota;
4811
- if (metadata?.accountId) {
4812
- this.store.setCodexAuthQuotaSnapshot(this.authRuntimeId(), candidate.name, metadata.accountId, quota);
4813
- }
4814
- await this.writeCodexAuthQuotaSnapshots();
4935
+ await this.recordCodexAuthQuotaSnapshot(candidate.name, metadata?.accountId ?? null, snapshot);
4815
4936
  await this.applySharedCodexAuthQuotaSnapshots(state);
4816
4937
  await this.syncCodexAuthCandidate(candidate.name);
4817
4938
  }
@@ -4840,6 +4961,14 @@ export class BridgeSessionCore {
4840
4961
  candidate.quota = mergeCodexAuthQuotaSnapshots(candidate.quota, snapshotsByAccount.get(accountId) ?? null);
4841
4962
  }
4842
4963
  }
4964
+ async recordCodexAuthQuotaSnapshot(candidateName, accountId, snapshot) {
4965
+ const quota = authQuotaSnapshotFromRateLimit(snapshot, accountId);
4966
+ this.authQuotaSnapshots[candidateName] = quota;
4967
+ if (accountId) {
4968
+ this.store.setCodexAuthQuotaSnapshot(this.authRuntimeId(), candidateName, accountId, quota);
4969
+ }
4970
+ await this.writeCodexAuthQuotaSnapshots();
4971
+ }
4843
4972
  async readCodexAuthCandidateAccountIds(candidates) {
4844
4973
  const entries = await Promise.all(candidates.map(async (candidate) => {
4845
4974
  const metadata = await readChatGptAuthMetadata(candidate.path);
@@ -7455,6 +7584,30 @@ async function pointCodexAuthAtTarget(authDir, authPath, targetPath) {
7455
7584
  throw error;
7456
7585
  }
7457
7586
  }
7587
+ async function restoreCodexAuthTarget(authDir, authPath, targetPath, regularAuthContents) {
7588
+ if (regularAuthContents !== null) {
7589
+ await fs.mkdir(authDir, { recursive: true });
7590
+ const temporary = path.join(authDir, `.auth.json.${process.pid}.${Date.now()}.restore`);
7591
+ try {
7592
+ await fs.writeFile(temporary, regularAuthContents, { mode: 0o600 });
7593
+ await fs.rename(temporary, authPath);
7594
+ }
7595
+ catch (error) {
7596
+ await fs.unlink(temporary).catch(() => undefined);
7597
+ throw error;
7598
+ }
7599
+ return;
7600
+ }
7601
+ if (targetPath) {
7602
+ await pointCodexAuthAtTarget(authDir, authPath, targetPath);
7603
+ return;
7604
+ }
7605
+ await fs.unlink(authPath).catch((error) => {
7606
+ if (!isFileMissingError(error)) {
7607
+ throw error;
7608
+ }
7609
+ });
7610
+ }
7458
7611
  async function switchCodexAuth(targetPath, explicitAuthDir = null) {
7459
7612
  const state = await listCodexAuthState(new Set(), explicitAuthDir);
7460
7613
  const candidate = state.candidates.find(entry => entry.path === targetPath);
@@ -7513,9 +7666,33 @@ function authChoiceKeyboard(locale, record) {
7513
7666
  { text: t(locale, 'button_permissions'), callback_data: 'nav:permissions' },
7514
7667
  { text: t(locale, 'button_login_device'), callback_data: `auth:${record.localId}:login_device` },
7515
7668
  ]);
7516
- rows.push([{ text: t(locale, 'button_auth_reload'), callback_data: `auth:${record.localId}:reload` }]);
7669
+ rows.push([
7670
+ { text: t(locale, 'button_auth_reload'), callback_data: `auth:${record.localId}:reload` },
7671
+ { text: t(locale, 'button_auth_refresh_all'), callback_data: `auth:${record.localId}:refresh_all` },
7672
+ ]);
7517
7673
  return rows;
7518
7674
  }
7675
+ function formatAuthRefreshAllResult(locale, result) {
7676
+ const lines = [t(locale, 'auth_refresh_all_done', {
7677
+ refreshed: String(result.refreshed.length),
7678
+ skipped: String(result.skipped.length),
7679
+ failed: String(result.failed.length),
7680
+ })];
7681
+ if (result.refreshed.length > 0) {
7682
+ lines.push(t(locale, 'auth_refresh_all_refreshed', { value: result.refreshed.join(', ') }));
7683
+ }
7684
+ if (result.skipped.length > 0) {
7685
+ lines.push(t(locale, 'auth_refresh_all_skipped', { value: result.skipped.join(', ') }));
7686
+ }
7687
+ if (result.failed.length > 0) {
7688
+ const details = result.failed
7689
+ .slice(0, 5)
7690
+ .map((failure) => `${failure.name}: ${failure.error}`)
7691
+ .join('; ');
7692
+ lines.push(t(locale, 'auth_refresh_all_failed', { value: details }));
7693
+ }
7694
+ return lines.join('\n');
7695
+ }
7519
7696
  function normalizeHelpUsageKey(name) {
7520
7697
  const normalized = name.toLowerCase();
7521
7698
  switch (normalized) {
package/dist/i18n.d.ts CHANGED
@@ -137,7 +137,7 @@ declare const MESSAGES: {
137
137
  readonly auth_reload_restarting: "Restarting Codex app-server to reload auth...";
138
138
  readonly auth_reload_done: "Codex app-server restarted. Current auth has been reloaded.";
139
139
  readonly auth_reload_blocked_active: "Cannot reload Codex auth while a turn, approval, or question is active. Wait or use /interrupt first.";
140
- readonly usage_auth: "Usage: /auth [list|use <n>|enable <n>|disable <n>|reload|add <name>]";
140
+ readonly usage_auth: "Usage: /auth [list|use <n>|enable <n>|disable <n>|reload|refresh all|add <name>]";
141
141
  readonly usage_auth_add: "Usage: /auth add <name>. Use letters, numbers, dot, dash, or underscore.";
142
142
  readonly auth_list_title: "Codex auth files:";
143
143
  readonly auth_bot: "Bot runtime: {value}";
@@ -158,6 +158,12 @@ declare const MESSAGES: {
158
158
  readonly auth_switching: "Switching Codex auth: {from} -> {to}...";
159
159
  readonly auth_switch_done: "Codex auth switched: {from} -> {to}.";
160
160
  readonly auth_recovered_newer_candidate: "Recovered a newer same-account credential for {value} from another Codex home before reload.";
161
+ readonly auth_refresh_all_starting: "Refreshing all ChatGPT auth candidates. Every runtime must stay idle...";
162
+ readonly auth_refresh_all_blocked_active: "Cannot refresh all auth candidates while any runtime, approval, input, login, or auth mirror write is active. Wait or use /interrupt first.";
163
+ readonly auth_refresh_all_done: "Auth refresh all complete: {refreshed} refreshed, {skipped} skipped, {failed} failed.";
164
+ readonly auth_refresh_all_refreshed: "Refreshed: {value}";
165
+ readonly auth_refresh_all_skipped: "Skipped non-ChatGPT/invalid candidates: {value}";
166
+ readonly auth_refresh_all_failed: "Failed: {value}";
161
167
  readonly auth_auto_switching: "Codex auth problem detected ({error}). Switching: {from} -> {to}...";
162
168
  readonly auth_auto_done: "Auto-switched Codex auth: {from} -> {to}.";
163
169
  readonly auth_auto_retrying: "Retrying the same request with the new auth...";
@@ -173,6 +179,7 @@ declare const MESSAGES: {
173
179
  readonly auth_add_missing_file: "Login completed, but the new auth file was not created: {value}";
174
180
  readonly button_login_device: "🔑 Login";
175
181
  readonly button_auth_reload: "🔄 Reload auth";
182
+ readonly button_auth_refresh_all: "🔁 Refresh all";
176
183
  readonly button_auth_enable: "✅";
177
184
  readonly button_auth_disable: "⏸️";
178
185
  readonly another_turn_running: "Another turn is already running. Use /interrupt, /takeover, /queue, or wait.";
@@ -713,7 +720,7 @@ declare const MESSAGES: {
713
720
  readonly auth_reload_restarting: "正在重启 Codex app-server 以重新读取 auth...";
714
721
  readonly auth_reload_done: "Codex app-server 已重启,当前 auth 已重新读取。";
715
722
  readonly auth_reload_blocked_active: "当前有回复、审批或问题在进行中,不能重载 Codex auth。请先等待,或使用 /interrupt。";
716
- readonly usage_auth: "用法:/auth [list|use <编号>|enable <编号>|disable <编号>|reload|add <名称>]";
723
+ readonly usage_auth: "用法:/auth [list|use <编号>|enable <编号>|disable <编号>|reload|refresh all|add <名称>]";
717
724
  readonly usage_auth_add: "用法:/auth add <名称>。名称只能包含字母、数字、点、短横线或下划线。";
718
725
  readonly auth_list_title: "Codex auth 文件:";
719
726
  readonly auth_bot: "Bot runtime:{value}";
@@ -734,6 +741,12 @@ declare const MESSAGES: {
734
741
  readonly auth_switching: "正在切换 Codex auth:{from} -> {to}...";
735
742
  readonly auth_switch_done: "Codex auth 已切换:{from} -> {to}。";
736
743
  readonly auth_recovered_newer_candidate: "重载前已从其他 Codex home 恢复 {value} 的同账号较新凭据。";
744
+ readonly auth_refresh_all_starting: "正在刷新全部 ChatGPT auth 候选。执行期间所有 runtime 必须保持空闲...";
745
+ readonly auth_refresh_all_blocked_active: "当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能刷新全部 auth。请先等待,或使用 /interrupt。";
746
+ readonly auth_refresh_all_done: "全部 auth 刷新完成:已刷新 {refreshed},已跳过 {skipped},失败 {failed}。";
747
+ readonly auth_refresh_all_refreshed: "已刷新:{value}";
748
+ readonly auth_refresh_all_skipped: "已跳过非 ChatGPT/无效候选:{value}";
749
+ readonly auth_refresh_all_failed: "失败:{value}";
737
750
  readonly auth_auto_switching: "检测到 Codex auth 问题({error}),正在切换:{from} -> {to}...";
738
751
  readonly auth_auto_done: "已自动切换 Codex auth:{from} -> {to}。";
739
752
  readonly auth_auto_retrying: "正在用新的 auth 重试同一条请求...";
@@ -749,6 +762,7 @@ declare const MESSAGES: {
749
762
  readonly auth_add_missing_file: "登录已完成,但没有创建新的 auth 文件:{value}";
750
763
  readonly button_login_device: "🔑 设备登录";
751
764
  readonly button_auth_reload: "🔄 重载 auth";
765
+ readonly button_auth_refresh_all: "🔁 刷新全部";
752
766
  readonly button_auth_enable: "✅";
753
767
  readonly button_auth_disable: "⏸️";
754
768
  readonly another_turn_running: "已经有一个回复在进行中。请先等待,或使用 /interrupt、/takeover、/queue。";
package/dist/i18n.js CHANGED
@@ -135,7 +135,7 @@ const MESSAGES = {
135
135
  auth_reload_restarting: 'Restarting Codex app-server to reload auth...',
136
136
  auth_reload_done: 'Codex app-server restarted. Current auth has been reloaded.',
137
137
  auth_reload_blocked_active: 'Cannot reload Codex auth while a turn, approval, or question is active. Wait or use /interrupt first.',
138
- usage_auth: 'Usage: /auth [list|use <n>|enable <n>|disable <n>|reload|add <name>]',
138
+ usage_auth: 'Usage: /auth [list|use <n>|enable <n>|disable <n>|reload|refresh all|add <name>]',
139
139
  usage_auth_add: 'Usage: /auth add <name>. Use letters, numbers, dot, dash, or underscore.',
140
140
  auth_list_title: 'Codex auth files:',
141
141
  auth_bot: 'Bot runtime: {value}',
@@ -156,6 +156,12 @@ const MESSAGES = {
156
156
  auth_switching: 'Switching Codex auth: {from} -> {to}...',
157
157
  auth_switch_done: 'Codex auth switched: {from} -> {to}.',
158
158
  auth_recovered_newer_candidate: 'Recovered a newer same-account credential for {value} from another Codex home before reload.',
159
+ auth_refresh_all_starting: 'Refreshing all ChatGPT auth candidates. Every runtime must stay idle...',
160
+ auth_refresh_all_blocked_active: 'Cannot refresh all auth candidates while any runtime, approval, input, login, or auth mirror write is active. Wait or use /interrupt first.',
161
+ auth_refresh_all_done: 'Auth refresh all complete: {refreshed} refreshed, {skipped} skipped, {failed} failed.',
162
+ auth_refresh_all_refreshed: 'Refreshed: {value}',
163
+ auth_refresh_all_skipped: 'Skipped non-ChatGPT/invalid candidates: {value}',
164
+ auth_refresh_all_failed: 'Failed: {value}',
159
165
  auth_auto_switching: 'Codex auth problem detected ({error}). Switching: {from} -> {to}...',
160
166
  auth_auto_done: 'Auto-switched Codex auth: {from} -> {to}.',
161
167
  auth_auto_retrying: 'Retrying the same request with the new auth...',
@@ -171,6 +177,7 @@ const MESSAGES = {
171
177
  auth_add_missing_file: 'Login completed, but the new auth file was not created: {value}',
172
178
  button_login_device: '🔑 Login',
173
179
  button_auth_reload: '🔄 Reload auth',
180
+ button_auth_refresh_all: '🔁 Refresh all',
174
181
  button_auth_enable: '✅',
175
182
  button_auth_disable: '⏸️',
176
183
  another_turn_running: 'Another turn is already running. Use /interrupt, /takeover, /queue, or wait.',
@@ -711,7 +718,7 @@ const MESSAGES = {
711
718
  auth_reload_restarting: '正在重启 Codex app-server 以重新读取 auth...',
712
719
  auth_reload_done: 'Codex app-server 已重启,当前 auth 已重新读取。',
713
720
  auth_reload_blocked_active: '当前有回复、审批或问题在进行中,不能重载 Codex auth。请先等待,或使用 /interrupt。',
714
- usage_auth: '用法:/auth [list|use <编号>|enable <编号>|disable <编号>|reload|add <名称>]',
721
+ usage_auth: '用法:/auth [list|use <编号>|enable <编号>|disable <编号>|reload|refresh all|add <名称>]',
715
722
  usage_auth_add: '用法:/auth add <名称>。名称只能包含字母、数字、点、短横线或下划线。',
716
723
  auth_list_title: 'Codex auth 文件:',
717
724
  auth_bot: 'Bot runtime:{value}',
@@ -732,6 +739,12 @@ const MESSAGES = {
732
739
  auth_switching: '正在切换 Codex auth:{from} -> {to}...',
733
740
  auth_switch_done: 'Codex auth 已切换:{from} -> {to}。',
734
741
  auth_recovered_newer_candidate: '重载前已从其他 Codex home 恢复 {value} 的同账号较新凭据。',
742
+ auth_refresh_all_starting: '正在刷新全部 ChatGPT auth 候选。执行期间所有 runtime 必须保持空闲...',
743
+ auth_refresh_all_blocked_active: '当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能刷新全部 auth。请先等待,或使用 /interrupt。',
744
+ auth_refresh_all_done: '全部 auth 刷新完成:已刷新 {refreshed},已跳过 {skipped},失败 {failed}。',
745
+ auth_refresh_all_refreshed: '已刷新:{value}',
746
+ auth_refresh_all_skipped: '已跳过非 ChatGPT/无效候选:{value}',
747
+ auth_refresh_all_failed: '失败:{value}',
735
748
  auth_auto_switching: '检测到 Codex auth 问题({error}),正在切换:{from} -> {to}...',
736
749
  auth_auto_done: '已自动切换 Codex auth:{from} -> {to}。',
737
750
  auth_auto_retrying: '正在用新的 auth 重试同一条请求...',
@@ -747,6 +760,7 @@ const MESSAGES = {
747
760
  auth_add_missing_file: '登录已完成,但没有创建新的 auth 文件:{value}',
748
761
  button_login_device: '🔑 设备登录',
749
762
  button_auth_reload: '🔄 重载 auth',
763
+ button_auth_refresh_all: '🔁 刷新全部',
750
764
  button_auth_enable: '✅',
751
765
  button_auth_disable: '⏸️',
752
766
  another_turn_running: '已经有一个回复在进行中。请先等待,或使用 /interrupt、/takeover、/queue。',
@@ -389,7 +389,7 @@ If the login is cancelled or fails, FoxClaw tries to restore the previous auth t
389
389
 
390
390
  ### 6.3 The `/auth` Panel
391
391
 
392
- `/auth` lists candidate accounts, the current account, and the auth directory. It also provides buttons for switching, disabling, login, and reload. In multi-bot mode the panel names the `@botname` runtime being managed, because private chats, groups, and topics on one bot share that bot's current auth. The `5h|7d` numbers before each filename are the last recorded remaining percentages for the two quota windows; the current auth is refreshed when the panel opens, while other candidates are not switched merely to query quota. When multiple bot runtimes have recently used the same ChatGPT account, FoxClaw combines their cached quota snapshots by verified account ID, so one bot's `/auth` panel can show quota information learned by another bot without mixing different accounts.
392
+ `/auth` lists candidate accounts, the current account, and the auth directory. It also provides buttons for switching, disabling, login, reload, and Refresh all. In multi-bot mode the panel names the `@botname` runtime being managed, because private chats, groups, and topics on one bot share that bot's current auth. The `5h|7d` numbers before each filename are the last recorded remaining percentages for the two quota windows; the current auth is refreshed when the panel opens, while other candidates are not switched merely to query quota. When multiple bot runtimes have recently used the same ChatGPT account, FoxClaw combines their cached quota snapshots by verified account ID, so one bot's `/auth` panel can show quota information learned by another bot without mixing different accounts.
393
393
 
394
394
  Approximation:
395
395
 
@@ -405,10 +405,10 @@ Quota remaining: 5h|7d|auth
405
405
  [✅ 20|25|auth.json_personal] [✅]
406
406
  [🔐 --|--|auth.json_team] [✅]
407
407
  [🛡️ Access] [🔑 Login]
408
- [🔄 Reload auth]
408
+ [🔄 Reload auth] [🔁 Refresh all]
409
409
  ```
410
410
 
411
- The right-side `✅` / `⏸️` button shows the current state. Tapping it toggles enabled/disabled, and the refreshed list shows the new state. Tapping a candidate switches auth, restarts that runtime, and refreshes the same panel with its buttons intact so you can switch again immediately. `--|--` means no quota snapshot has been observed for that candidate yet.
411
+ The right-side `✅` / `⏸️` button shows the current state. Tapping it toggles enabled/disabled, and the refreshed list shows the new state. Tapping a candidate switches auth, restarts that runtime, and refreshes the same panel with its buttons intact so you can switch again immediately. `Refresh all` is a maintenance action: it is allowed only when every Telegram runtime, the Weixin runtime, approvals, inputs, logins, and auth mirroring are idle. It then visits every ChatGPT candidate in the panel, asks Codex to force-refresh tokens with `account/read refreshToken=true`, verifies the result through the usage endpoint, mirrors successful candidates, restores the original current auth, and refreshes the panel with a summary. `--|--` means no quota snapshot has been observed for that candidate yet.
412
412
 
413
413
  Equivalent commands:
414
414
 
@@ -417,6 +417,7 @@ Equivalent commands:
417
417
  - `/auth enable <n>`: let candidate n participate in auto-rotation.
418
418
  - `/auth disable <n>`: skip candidate n during auto-rotation.
419
419
  - `/auth reload` or `/auth_reload`: restart app-server and reload the current `auth.json`.
420
+ - `/auth refresh all`: same as the panel's Refresh all button.
420
421
 
421
422
  If the requesting bot runtime has active turns, pending approvals, pending user inputs, or MCP elicitations, FoxClaw refuses manual auth switching to avoid changing accounts mid-request; another idle bot is unaffected.
422
423
 
@@ -389,7 +389,7 @@ cp -L ~/.codex/auth.json ~/.codex/auth.json_personal
389
389
 
390
390
  ### 6.3 `/auth` 面板
391
391
 
392
- `/auth` 会列出候选账号、当前账号和 auth 目录,并提供按钮切换、禁用、登录和重载。多 bot 模式中,面板顶部还会显示当前正在管理的 `@botname`,因为该 bot 内的私聊、群聊和话题共享同一个当前 auth。每个候选名前的 `5h|7d` 数字表示上次记录到的两个额度窗口剩余百分比;当前 auth 会在打开面板时刷新,其他候选不会为了查询额度被自动切换。如果多个 bot runtime 最近使用过同一个 ChatGPT 账号,FoxClaw 会按已验证的账号 ID 合并它们缓存到的额度快照,因此一个 bot 的 `/auth` 面板可以显示另一个 bot 掌握到的额度信息,同时不会把不同账号混在一起。
392
+ `/auth` 会列出候选账号、当前账号和 auth 目录,并提供按钮切换、禁用、登录、重载和刷新全部。多 bot 模式中,面板顶部还会显示当前正在管理的 `@botname`,因为该 bot 内的私聊、群聊和话题共享同一个当前 auth。每个候选名前的 `5h|7d` 数字表示上次记录到的两个额度窗口剩余百分比;当前 auth 会在打开面板时刷新,其他候选不会为了查询额度被自动切换。如果多个 bot runtime 最近使用过同一个 ChatGPT 账号,FoxClaw 会按已验证的账号 ID 合并它们缓存到的额度快照,因此一个 bot 的 `/auth` 面板可以显示另一个 bot 掌握到的额度信息,同时不会把不同账号混在一起。
393
393
 
394
394
  示意:
395
395
 
@@ -405,10 +405,10 @@ Candidates: 2
405
405
  [✅ 20|25|auth.json_personal] [✅]
406
406
  [🔐 --|--|auth.json_team] [✅]
407
407
  [🛡️ Access] [🔑 设备登录]
408
- [🔄 Reload auth]
408
+ [🔄 Reload auth] [🔁 刷新全部]
409
409
  ```
410
410
 
411
- 右侧 `✅` / `⏸️` 表示当前状态。点一下会切换启用/禁用,列表刷新后图标会随状态变化。点击候选会切换 auth、重启对应 runtime,并在原消息上刷新面板且保留按钮,因此可以立即连续切换。`--|--` 表示该候选还没有额度历史快照。
411
+ 右侧 `✅` / `⏸️` 表示当前状态。点一下会切换启用/禁用,列表刷新后图标会随状态变化。点击候选会切换 auth、重启对应 runtime,并在原消息上刷新面板且保留按钮,因此可以立即连续切换。`刷新全部` 是维护操作:只有所有 Telegram runtime、微信 runtime、审批、待输入、登录流程和 auth 镜像写入都空闲时才允许执行。它会逐个访问面板里的 ChatGPT 候选,让 Codex 通过 `account/read refreshToken=true` 强制刷新 token,再用 usage 接口验证,成功后镜像到其他 bot home,最后恢复原本的当前 auth 并刷新面板摘要。`--|--` 表示该候选还没有额度历史快照。
412
412
 
413
413
  命令等价用法:
414
414
 
@@ -417,6 +417,7 @@ Candidates: 2
417
417
  - `/auth enable <n>`:让第 n 个候选参与自动轮转。
418
418
  - `/auth disable <n>`:禁用第 n 个候选,自动轮转会跳过它。
419
419
  - `/auth reload` 或 `/auth_reload`:重启 app-server,重新加载当前 `auth.json`。
420
+ - `/auth refresh all`:等同于面板上的“刷新全部”按钮。
420
421
 
421
422
  切换 auth 时,如果当前 bot runtime 还有活跃 turn、待审批、待用户输入或 MCP elicitation,FoxClaw 会先拒绝切换,避免中途换号破坏正在进行的请求;另一个空闲 bot 不受影响。
422
423
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -67,6 +67,7 @@ Telegram behavior:
67
67
  - when `TG_BOT_TOKENS` contains multiple bots, one FoxClaw service starts independent Codex app-servers and auth selections per bot
68
68
  - isolated Telegram runtimes use independent `CODEX_HOME` directories and file-backed credentials; they do not share Codex sessions
69
69
  - refreshed ChatGPT candidates are mirrored only after online usage-endpoint validation; before auth switch or reload, a runtime restores any newer same-account credential found in another Codex home
70
+ - `/auth` includes a Refresh all panel button; it is a global-idle maintenance action that force-refreshes every ChatGPT candidate through Codex `account/read refreshToken=true`, validates usage, mirrors successful candidates, and restores the original current auth
70
71
  - if `TG_BOT_TOKEN` is also set to one exact token from `TG_BOT_TOKENS`, that bot uses the default/shared-terminal runtime instead of an isolated Telegram home
71
72
  - if Weixin is enabled alongside multiple Telegram bots, it remains on the default Codex runtime instead of borrowing a Telegram bot runtime
72
73
  - in a group with multiple configured bots, address a bot by mention, reply, or suffixed command such as `/status@botname`
@@ -173,6 +174,8 @@ Do not describe the setup as "done" until this smoke test has either passed or b
173
174
 
174
175
  Use this checklist when the user asks for standard closing actions, release wrap-up, local install updates, npm publish, or says things like "收尾动作", "中文 commit msg", "中英双语 commit msg", "push", "npm publish", or "本地安装更新".
175
176
 
177
+ When the user says development is complete, asks to "收尾", or asks to push/publish, treat that as authorization to finish the whole release path without asking for another confirmation. After validation passes, commit the scoped changes, push the branch, publish through the configured release flow, and verify the published result. Stop and ask only when a real external decision is required, such as npm/GitHub web auth, missing credentials, failed checks, merge conflicts, an already-published target version that requires choosing a new version, or unrelated local changes that would be staged.
178
+
176
179
  1. Inspect scope before staging:
177
180
  - `git status -sb`
178
181
  - `git diff --stat`
@@ -7,6 +7,8 @@ description: Publish npm packages safely, especially packages that require npm 2
7
7
 
8
8
  Use this skill to publish an npm package from a repo. Prefer CI trusted publishing when the repository has a publish workflow. Prefer the normal no-2FA publish path first only when CI publishing is not available. Fall back to npm web-auth only when npm explicitly prompts for it.
9
9
 
10
+ If the user asks to release, publish, "收尾", "push publish", or otherwise finish a completed package change, proceed through verification, commit, push, publish, and post-publish verification without asking for a second confirmation. Pause only for real blockers: missing credentials, failed checks, unrelated changes that would be staged, merge conflicts, an already-published target version that needs a version choice, or npm/GitHub web-auth that requires the user to click/confirm an external page.
11
+
10
12
  ## Release Checklist
11
13
 
12
14
  1. Sync first when the user asks for it:
@@ -1,4 +1,4 @@
1
1
  interface:
2
2
  display_name: "NPM Publish"
3
- short_description: "Publish npm packages with 2FA web auth"
4
- default_prompt: "Publish this npm package using the recorded web-auth flow."
3
+ short_description: "Verify, push, and publish npm packages"
4
+ default_prompt: "Publish this npm package: verify it, commit and push scoped release changes, run the configured publish flow, and only pause for real auth or failure blockers."