@foxden-app/foxclaw 0.5.10 → 0.5.11

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,18 @@
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.11 - 2026-06-08
6
+
7
+ ### 中文
8
+ - `/auth` 面板的 `Bot runtime` 现在显示 Telegram bot id,例如 `@WuguiAI2_Bot (bot8949529424)`,便于和 `~/.foxclaw/codex/telegram/<botid>/home` 对应。
9
+ - `/auth` 面板新增“安全同步”按钮,并支持 `/auth sync safe`,可在全局空闲时安全打平本机多 bot auth,并把已校验的候选推送到跨节点 peer。
10
+ - 本机 auth mirror 新增全量安全同步路径:只传播通过既有在线校验的刷新候选,同时补齐 canonical 中已知、同账号且更新的 runtime 副本。
11
+
12
+ ### English
13
+ - The `/auth` panel now shows the Telegram bot id in `Bot runtime`, for example `@WuguiAI2_Bot (bot8949529424)`, making it easy to match the runtime with `~/.foxclaw/codex/telegram/<botid>/home`.
14
+ - Added a Safe sync button to the `/auth` panel, plus `/auth sync safe`, to flatten same-node multi-bot auth while globally idle and push validated candidates to cross-node peers.
15
+ - Added a full safe-sync path for the local auth mirror: it only propagates candidates that pass the existing online validation and fills runtime copies from newer same-account canonical candidates.
16
+
5
17
  ## 0.5.10 - 2026-06-08
6
18
 
7
19
  ### 中文
package/README.md CHANGED
@@ -39,6 +39,7 @@ FoxClaw(狸爪)的目标很直接:让你用手机控制本机的 Codex,
39
39
  - 已经装好,想系统了解 `/help`、`/setup`、`/threads`、`/watch`、`/auth` 和账号轮转?看 [用户手册](./docs/zh/user-manual.md)。
40
40
  - 想把同一组合法 ChatGPT auth 候选同步到多台机器?看 [跨节点 auth 同步配置指南](./docs/zh/cross-node-auth-sync.md)。
41
41
  - 想了解每个版本改了什么?看 [更新日志](./CHANGELOG.md)。
42
+ - 维护者准备发版?看 [发布 runbook](./docs/zh/release.md)。
42
43
  - Git、Node、`.env` 都玩得转?直接往下看快速设置。
43
44
  - 卡住了?看 [故障排查](./docs/zh/troubleshooting.md)。
44
45
 
package/README_EN.md CHANGED
@@ -39,6 +39,7 @@ FoxClaw is more than message forwarding. It provides Telegram panels for Codex w
39
39
  - Already installed and want the full command guide for `/help`, `/setup`, `/threads`, `/watch`, `/auth`, and auth rotation? Read the [User Manual](./docs/user-manual.md).
40
40
  - Want to sync the same legally owned ChatGPT auth candidate pool across multiple machines? Read the [Cross-Node Auth Sync Setup Guide](./docs/cross-node-auth-sync.md).
41
41
  - Want to see what changed in each release? Read the [Changelog](./CHANGELOG.md).
42
+ - Maintaining a release? Use the [Release Runbook](./docs/release.md).
42
43
  - Already comfortable with Git, Node, and `.env` files? Use the quick setup below.
43
44
  - Something failed? Check [Troubleshooting](./docs/troubleshooting.md).
44
45
 
@@ -52,6 +52,10 @@ export interface AuthMirrorSyncedEvent {
52
52
  status: AuthMirrorStatus;
53
53
  record: AuthMirrorCandidateRecord;
54
54
  }
55
+ export interface AuthMirrorSyncAllResult {
56
+ synced: number;
57
+ skipped: number;
58
+ }
55
59
  export interface AuthMirrorHooks {
56
60
  onSynced?: (event: AuthMirrorSyncedEvent) => Promise<void> | void;
57
61
  }
@@ -77,8 +81,10 @@ export declare class AuthCandidateMirror {
77
81
  readRuntimeCandidate(runtimeId: string, candidateName: string): Promise<AuthMirrorCandidateRecord | null>;
78
82
  listNewestCandidates(): Promise<AuthMirrorCandidateRecord[]>;
79
83
  syncRuntimeCandidate(runtimeId: string, candidateName: string): Promise<boolean>;
84
+ syncAllRuntimeCandidates(): Promise<AuthMirrorSyncAllResult>;
80
85
  recoverRuntimeCandidate(runtimeId: string, candidateName: string): Promise<AuthMirrorRecovery | null>;
81
86
  private scan;
87
+ private distributeCanonicalCandidates;
82
88
  importExternalCandidate(candidateName: string, raw: string, source: {
83
89
  nodeId: string;
84
90
  label?: string | null;
@@ -126,6 +126,28 @@ export class AuthCandidateMirror {
126
126
  return false;
127
127
  return this.withActivity(() => this.propagateValidatedCandidate(runtime, candidateName));
128
128
  }
129
+ async syncAllRuntimeCandidates() {
130
+ return this.withActivity(async () => {
131
+ let synced = 0;
132
+ let skipped = 0;
133
+ for (const runtime of this.runtimes) {
134
+ const names = await listAuthCandidateNames(runtime.authDir);
135
+ for (const name of names) {
136
+ if (await this.propagateValidatedCandidate(runtime, name)) {
137
+ synced += 1;
138
+ }
139
+ else {
140
+ skipped += 1;
141
+ }
142
+ }
143
+ }
144
+ const distributed = await this.distributeCanonicalCandidates();
145
+ return {
146
+ synced: synced + distributed.synced,
147
+ skipped: skipped + distributed.skipped,
148
+ };
149
+ });
150
+ }
129
151
  async recoverRuntimeCandidate(runtimeId, candidateName) {
130
152
  if (!isAuthCandidateName(candidateName))
131
153
  return null;
@@ -169,6 +191,33 @@ export class AuthCandidateMirror {
169
191
  }
170
192
  });
171
193
  }
194
+ async distributeCanonicalCandidates() {
195
+ let synced = 0;
196
+ let skipped = 0;
197
+ for (const name of await listAuthCandidateNames(this.canonicalDir)) {
198
+ const canonical = await readChatGptAuthRecord(path.join(this.canonicalDir, name));
199
+ if (!canonical) {
200
+ skipped += this.runtimes.length;
201
+ continue;
202
+ }
203
+ for (const runtime of this.runtimes) {
204
+ const destinationPath = path.join(runtime.authDir, name);
205
+ const destination = await readChatGptAuthRecord(destinationPath);
206
+ if (destination && destination.accountId !== canonical.accountId) {
207
+ skipped += 1;
208
+ this.logger.warn('auth.mirror.distribution_conflict', { runtimeId: runtime.id, name });
209
+ continue;
210
+ }
211
+ if (destination && destination.lastRefreshMs >= canonical.lastRefreshMs) {
212
+ skipped += 1;
213
+ continue;
214
+ }
215
+ await atomicWrite(destinationPath, canonical.raw);
216
+ synced += 1;
217
+ }
218
+ }
219
+ return { synced, skipped };
220
+ }
172
221
  async importExternalCandidate(candidateName, raw, source) {
173
222
  if (!isAuthCandidateName(candidateName)) {
174
223
  return { ok: false, imported: false, reason: 'invalid candidate name' };
@@ -19,6 +19,12 @@ export interface CoreCoordinator {
19
19
  }>;
20
20
  releaseAuthRefreshLease?: (leaseId: string | null) => Promise<void>;
21
21
  getAuthSyncStatus?: () => RuntimeStatus['authSync'];
22
+ authSyncSafeAll?: () => Promise<{
23
+ localSynced: number;
24
+ localSkipped: number;
25
+ sent: number;
26
+ skipped: number;
27
+ }>;
22
28
  authSyncPushAll?: () => Promise<{
23
29
  sent: number;
24
30
  skipped: number;
@@ -283,6 +289,7 @@ export declare class BridgeSessionCore {
283
289
  private notifyProactiveAuthRefresh;
284
290
  private handleAuthCommand;
285
291
  private handleAuthSyncCommand;
292
+ private runAuthSafeSyncAll;
286
293
  private handleAuthRefreshAllCommand;
287
294
  private handleAuthUseCommand;
288
295
  private handleAuthToggleCommand;
@@ -1065,7 +1065,7 @@ export class BridgeSessionCore {
1065
1065
  await this.handleAuthListViewCallback(event, authClearSearchMatch[1], 'clear_search', locale);
1066
1066
  return;
1067
1067
  }
1068
- const authActionMatch = /^auth:([a-f0-9]+):(login_device|reload|refresh_all_confirm|refresh_all_cancel|refresh_all)$/.exec(event.data);
1068
+ const authActionMatch = /^auth:([a-f0-9]+):(login_device|reload|safe_sync|refresh_all_confirm|refresh_all_cancel|refresh_all)$/.exec(event.data);
1069
1069
  if (authActionMatch) {
1070
1070
  await this.handleAuthPanelActionCallback(event, authActionMatch[1], authActionMatch[2], locale);
1071
1071
  return;
@@ -2956,7 +2956,7 @@ export class BridgeSessionCore {
2956
2956
  authDisplayBotLabel() {
2957
2957
  if (!this.config.tgScopeBotId)
2958
2958
  return null;
2959
- return this.botUsername ? `@${this.botUsername}` : this.config.tgScopeBotId;
2959
+ return this.botUsername ? `@${this.botUsername} (${this.config.tgScopeBotId})` : this.config.tgScopeBotId;
2960
2960
  }
2961
2961
  ownsScope(scopeId) {
2962
2962
  if (scopeId.startsWith(BRIDGE_SCOPE_WEIXIN_PREFIX)) {
@@ -4467,17 +4467,19 @@ export class BridgeSessionCore {
4467
4467
  await this.sendMessage(scopeId, message);
4468
4468
  return;
4469
4469
  }
4470
- if (action === 'push' && args[1]?.toLowerCase() === 'all') {
4470
+ if (action === 'safe' || (action === 'push' && args[1]?.toLowerCase() === 'all')) {
4471
4471
  if (!this.canRunGlobalAuthRefresh()) {
4472
4472
  await this.sendMessage(scopeId, t(locale, 'auth_sync_push_blocked_active'));
4473
4473
  return;
4474
4474
  }
4475
- const result = await this.coordinator?.authSyncPushAll?.();
4475
+ const result = await this.runAuthSafeSyncAll();
4476
4476
  if (!result) {
4477
4477
  await this.sendMessage(scopeId, t(locale, 'auth_sync_disabled'));
4478
4478
  return;
4479
4479
  }
4480
- await this.sendMessage(scopeId, t(locale, 'auth_sync_push_done', {
4480
+ await this.sendMessage(scopeId, t(locale, 'auth_sync_safe_done', {
4481
+ localSynced: result.localSynced,
4482
+ localSkipped: result.localSkipped,
4481
4483
  sent: result.sent,
4482
4484
  skipped: result.skipped,
4483
4485
  }));
@@ -4485,6 +4487,16 @@ export class BridgeSessionCore {
4485
4487
  }
4486
4488
  await this.sendMessage(scopeId, t(locale, 'usage_auth_sync'));
4487
4489
  }
4490
+ async runAuthSafeSyncAll() {
4491
+ const safeResult = await this.coordinator?.authSyncSafeAll?.();
4492
+ if (safeResult) {
4493
+ return safeResult;
4494
+ }
4495
+ const pushResult = await this.coordinator?.authSyncPushAll?.();
4496
+ return pushResult
4497
+ ? { localSynced: 0, localSkipped: 0, sent: pushResult.sent, skipped: pushResult.skipped }
4498
+ : null;
4499
+ }
4488
4500
  async handleAuthRefreshAllCommand(scopeId, locale, confirmed = false) {
4489
4501
  if (!this.canRunGlobalAuthRefresh()) {
4490
4502
  await this.sendMessage(scopeId, t(locale, 'auth_refresh_all_blocked_active'));
@@ -4993,6 +5005,37 @@ export class BridgeSessionCore {
4993
5005
  await this.handleLoginDeviceCommand(event.scopeId, locale);
4994
5006
  return;
4995
5007
  }
5008
+ if (action === 'safe_sync') {
5009
+ if (!this.canRunGlobalAuthRefresh()) {
5010
+ await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_sync_push_blocked_active'));
5011
+ return;
5012
+ }
5013
+ await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_sync_safe_starting'));
5014
+ if (record.messageId !== null) {
5015
+ await this.editMessage(event.scopeId, record.messageId, t(locale, 'auth_sync_safe_starting'), []);
5016
+ }
5017
+ const result = await this.runAuthSafeSyncAll();
5018
+ if (!result) {
5019
+ if (record.messageId !== null) {
5020
+ await this.editMessage(event.scopeId, record.messageId, t(locale, 'auth_sync_disabled'), authChoiceKeyboard(locale, record));
5021
+ }
5022
+ return;
5023
+ }
5024
+ const state = await this.listCodexAuthState();
5025
+ await this.applySharedCodexAuthQuotaSnapshots(state);
5026
+ record.candidates = state.candidates;
5027
+ record.createdAt = Date.now();
5028
+ clampCodexAuthListOffset(record);
5029
+ if (record.messageId !== null) {
5030
+ await this.editMessage(event.scopeId, record.messageId, `${t(locale, 'auth_sync_safe_done', {
5031
+ localSynced: result.localSynced,
5032
+ localSkipped: result.localSkipped,
5033
+ sent: result.sent,
5034
+ skipped: result.skipped,
5035
+ })}\n\n${renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record)}`, authChoiceKeyboard(locale, record));
5036
+ }
5037
+ return;
5038
+ }
4996
5039
  if (action === 'refresh_all') {
4997
5040
  if (!this.canRunGlobalAuthRefresh()) {
4998
5041
  await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_refresh_all_blocked_active'));
@@ -8409,7 +8452,7 @@ function renderAuthListMessage(locale, state, botLabel = null, includeWeixinCopy
8409
8452
  if (state.candidates.length === 0) {
8410
8453
  lines.push(t(locale, 'auth_no_candidates'));
8411
8454
  if (includeWeixinCopyPaste) {
8412
- lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_auth_title'), '/login_device', '/auth reload', '/permissions');
8455
+ lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_auth_title'), '/login_device', '/auth sync safe', '/auth reload', '/permissions');
8413
8456
  }
8414
8457
  return lines.join('\n');
8415
8458
  }
@@ -8442,7 +8485,7 @@ function renderAuthListMessage(locale, state, botLabel = null, includeWeixinCopy
8442
8485
  if (includeWeixinCopyPaste) {
8443
8486
  lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_auth_title'), ...page.visible.map(({ index }) => `/auth use ${index + 1}`), ...page.visible.map(({ candidate, index }) => candidate.disabled
8444
8487
  ? `/auth enable ${index + 1}`
8445
- : `/auth disable ${index + 1}`), '/auth filter all', '/auth filter enabled', '/auth filter attention', '/auth list <keyword>', '/login_device', '/auth reload', '/permissions');
8488
+ : `/auth disable ${index + 1}`), '/auth filter all', '/auth filter enabled', '/auth filter attention', '/auth list <keyword>', '/login_device', '/auth sync safe', '/auth reload', '/permissions');
8446
8489
  }
8447
8490
  return lines.join('\n');
8448
8491
  }
@@ -8480,7 +8523,10 @@ function authChoiceKeyboard(locale, record) {
8480
8523
  { text: t(locale, 'button_permissions'), callback_data: 'nav:permissions' },
8481
8524
  { text: t(locale, 'button_login_device'), callback_data: `auth:${record.localId}:login_device` },
8482
8525
  ]);
8483
- rows.push([{ text: t(locale, 'button_auth_reload'), callback_data: `auth:${record.localId}:reload` }]);
8526
+ rows.push([
8527
+ { text: t(locale, 'button_auth_safe_sync'), callback_data: `auth:${record.localId}:safe_sync` },
8528
+ { text: t(locale, 'button_auth_reload'), callback_data: `auth:${record.localId}:reload` },
8529
+ ]);
8484
8530
  return rows;
8485
8531
  }
8486
8532
  function formatCodexAuthCandidateDisplayName(name) {
package/dist/i18n.d.ts CHANGED
@@ -143,8 +143,8 @@ declare const MESSAGES: {
143
143
  readonly auth_reload_restarting: "Restarting Codex app-server to reload auth...";
144
144
  readonly auth_reload_done: "Codex app-server restarted. Current auth has been reloaded.";
145
145
  readonly auth_reload_blocked_active: "Cannot reload Codex auth while a turn, approval, or question is active. Wait or use /interrupt first.";
146
- readonly usage_auth: "Usage: /auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|sync <status|test|push all>|add <name>]";
147
- readonly usage_auth_sync: "Usage: /auth sync <status|events [filter]|trace <requestId>|test|push all>";
146
+ readonly usage_auth: "Usage: /auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|sync <status|test|safe|push all>|add <name>]";
147
+ readonly usage_auth_sync: "Usage: /auth sync <status|events [filter]|trace <requestId>|test|safe|push all>";
148
148
  readonly usage_auth_add: "Usage: /auth add <name>. Use letters, numbers, dot, dash, or underscore.";
149
149
  readonly auth_list_title: "Codex auth files:";
150
150
  readonly auth_bot: "Bot runtime: {value}";
@@ -212,6 +212,8 @@ declare const MESSAGES: {
212
212
  readonly auth_sync_test_sent: "Auth sync test complete: sent {sent}, replies {replied}.";
213
213
  readonly auth_sync_test_missing: "Missing replies: {value}";
214
214
  readonly auth_sync_push_blocked_active: "Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.";
215
+ readonly auth_sync_safe_starting: "Safely syncing auth across local bot runtimes and cross-node peers...";
216
+ readonly auth_sync_safe_done: "Safe auth sync complete: local synced {localSynced}, local skipped {localSkipped}; cross-node sent {sent}, skipped {skipped}.";
215
217
  readonly auth_sync_push_done: "Auth sync push complete: sent {sent}, skipped {skipped}.";
216
218
  readonly auth_sync_status_title: "Cross-node auth sync:";
217
219
  readonly auth_sync_status_node: "Node: {value}";
@@ -235,6 +237,7 @@ declare const MESSAGES: {
235
237
  readonly auth_sync_trace_missing: "Usage: /auth sync trace <requestId>";
236
238
  readonly button_login_device: "🔑 Login";
237
239
  readonly button_auth_reload: "🔄 Reload auth";
240
+ readonly button_auth_safe_sync: "🧷 Safe sync";
238
241
  readonly button_auth_refresh_all_confirm: "⚠️ Accept risk & refresh";
239
242
  readonly button_auth_enable: "✅";
240
243
  readonly button_auth_disable: "⏸️";
@@ -796,8 +799,8 @@ declare const MESSAGES: {
796
799
  readonly auth_reload_restarting: "正在重启 Codex app-server 以重新读取 auth...";
797
800
  readonly auth_reload_done: "Codex app-server 已重启,当前 auth 已重新读取。";
798
801
  readonly auth_reload_blocked_active: "当前有回复、审批或问题在进行中,不能重载 Codex auth。请先等待,或使用 /interrupt。";
799
- readonly usage_auth: "用法:/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|sync <status|test|push all>|add <名称>]";
800
- readonly usage_auth_sync: "用法:/auth sync <status|events [过滤]|trace <requestId>|test|push all>";
802
+ readonly usage_auth: "用法:/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|sync <status|test|safe|push all>|add <名称>]";
803
+ readonly usage_auth_sync: "用法:/auth sync <status|events [过滤]|trace <requestId>|test|safe|push all>";
801
804
  readonly usage_auth_add: "用法:/auth add <名称>。名称只能包含字母、数字、点、短横线或下划线。";
802
805
  readonly auth_list_title: "Codex auth 文件:";
803
806
  readonly auth_bot: "Bot runtime:{value}";
@@ -865,6 +868,8 @@ declare const MESSAGES: {
865
868
  readonly auth_sync_test_sent: "auth sync 测试完成:已发送 {sent},收到回应 {replied}。";
866
869
  readonly auth_sync_test_missing: "未回应:{value}";
867
870
  readonly auth_sync_push_blocked_active: "当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。";
871
+ readonly auth_sync_safe_starting: "正在安全同步本机多 bot runtime 和跨节点 auth...";
872
+ readonly auth_sync_safe_done: "安全 auth 同步完成:本机同步 {localSynced},本机跳过 {localSkipped};跨节点发送 {sent},跳过 {skipped}。";
868
873
  readonly auth_sync_push_done: "auth 同步推送完成:已发送 {sent},已跳过 {skipped}。";
869
874
  readonly auth_sync_status_title: "跨节点 auth 同步:";
870
875
  readonly auth_sync_status_node: "节点:{value}";
@@ -888,6 +893,7 @@ declare const MESSAGES: {
888
893
  readonly auth_sync_trace_missing: "用法:/auth sync trace <requestId>";
889
894
  readonly button_login_device: "🔑 设备登录";
890
895
  readonly button_auth_reload: "🔄 重载 auth";
896
+ readonly button_auth_safe_sync: "🧷 安全同步";
891
897
  readonly button_auth_refresh_all_confirm: "⚠️ 接受风险并刷新";
892
898
  readonly button_auth_enable: "✅";
893
899
  readonly button_auth_disable: "⏸️";
package/dist/i18n.js CHANGED
@@ -141,8 +141,8 @@ const MESSAGES = {
141
141
  auth_reload_restarting: 'Restarting Codex app-server to reload auth...',
142
142
  auth_reload_done: 'Codex app-server restarted. Current auth has been reloaded.',
143
143
  auth_reload_blocked_active: 'Cannot reload Codex auth while a turn, approval, or question is active. Wait or use /interrupt first.',
144
- usage_auth: 'Usage: /auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|sync <status|test|push all>|add <name>]',
145
- usage_auth_sync: 'Usage: /auth sync <status|events [filter]|trace <requestId>|test|push all>',
144
+ usage_auth: 'Usage: /auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|sync <status|test|safe|push all>|add <name>]',
145
+ usage_auth_sync: 'Usage: /auth sync <status|events [filter]|trace <requestId>|test|safe|push all>',
146
146
  usage_auth_add: 'Usage: /auth add <name>. Use letters, numbers, dot, dash, or underscore.',
147
147
  auth_list_title: 'Codex auth files:',
148
148
  auth_bot: 'Bot runtime: {value}',
@@ -210,6 +210,8 @@ const MESSAGES = {
210
210
  auth_sync_test_sent: 'Auth sync test complete: sent {sent}, replies {replied}.',
211
211
  auth_sync_test_missing: 'Missing replies: {value}',
212
212
  auth_sync_push_blocked_active: 'Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.',
213
+ auth_sync_safe_starting: 'Safely syncing auth across local bot runtimes and cross-node peers...',
214
+ auth_sync_safe_done: 'Safe auth sync complete: local synced {localSynced}, local skipped {localSkipped}; cross-node sent {sent}, skipped {skipped}.',
213
215
  auth_sync_push_done: 'Auth sync push complete: sent {sent}, skipped {skipped}.',
214
216
  auth_sync_status_title: 'Cross-node auth sync:',
215
217
  auth_sync_status_node: 'Node: {value}',
@@ -233,6 +235,7 @@ const MESSAGES = {
233
235
  auth_sync_trace_missing: 'Usage: /auth sync trace <requestId>',
234
236
  button_login_device: '🔑 Login',
235
237
  button_auth_reload: '🔄 Reload auth',
238
+ button_auth_safe_sync: '🧷 Safe sync',
236
239
  button_auth_refresh_all_confirm: '⚠️ Accept risk & refresh',
237
240
  button_auth_enable: '✅',
238
241
  button_auth_disable: '⏸️',
@@ -794,8 +797,8 @@ const MESSAGES = {
794
797
  auth_reload_restarting: '正在重启 Codex app-server 以重新读取 auth...',
795
798
  auth_reload_done: 'Codex app-server 已重启,当前 auth 已重新读取。',
796
799
  auth_reload_blocked_active: '当前有回复、审批或问题在进行中,不能重载 Codex auth。请先等待,或使用 /interrupt。',
797
- usage_auth: '用法:/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|sync <status|test|push all>|add <名称>]',
798
- usage_auth_sync: '用法:/auth sync <status|events [过滤]|trace <requestId>|test|push all>',
800
+ usage_auth: '用法:/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|sync <status|test|safe|push all>|add <名称>]',
801
+ usage_auth_sync: '用法:/auth sync <status|events [过滤]|trace <requestId>|test|safe|push all>',
799
802
  usage_auth_add: '用法:/auth add <名称>。名称只能包含字母、数字、点、短横线或下划线。',
800
803
  auth_list_title: 'Codex auth 文件:',
801
804
  auth_bot: 'Bot runtime:{value}',
@@ -863,6 +866,8 @@ const MESSAGES = {
863
866
  auth_sync_test_sent: 'auth sync 测试完成:已发送 {sent},收到回应 {replied}。',
864
867
  auth_sync_test_missing: '未回应:{value}',
865
868
  auth_sync_push_blocked_active: '当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。',
869
+ auth_sync_safe_starting: '正在安全同步本机多 bot runtime 和跨节点 auth...',
870
+ auth_sync_safe_done: '安全 auth 同步完成:本机同步 {localSynced},本机跳过 {localSkipped};跨节点发送 {sent},跳过 {skipped}。',
866
871
  auth_sync_push_done: 'auth 同步推送完成:已发送 {sent},已跳过 {skipped}。',
867
872
  auth_sync_status_title: '跨节点 auth 同步:',
868
873
  auth_sync_status_node: '节点:{value}',
@@ -886,6 +891,7 @@ const MESSAGES = {
886
891
  auth_sync_trace_missing: '用法:/auth sync trace <requestId>',
887
892
  button_login_device: '🔑 设备登录',
888
893
  button_auth_reload: '🔄 重载 auth',
894
+ button_auth_safe_sync: '🧷 安全同步',
889
895
  button_auth_refresh_all_confirm: '⚠️ 接受风险并刷新',
890
896
  button_auth_enable: '✅',
891
897
  button_auth_disable: '⏸️',
package/dist/main.js CHANGED
@@ -342,6 +342,16 @@ async function runServeCli() {
342
342
  releaseAuthRefreshLease: (leaseId) => authSync?.releaseRefreshLease(leaseId)
343
343
  ?? localAuthRefreshLease.release(leaseId),
344
344
  getAuthSyncStatus: () => authSync?.getStatus() ?? null,
345
+ authSyncSafeAll: async () => {
346
+ const local = await mirror.syncAllRuntimeCandidates();
347
+ const remote = await authSync?.pushAll() ?? { sent: 0, skipped: 0 };
348
+ return {
349
+ localSynced: local.synced,
350
+ localSkipped: local.skipped,
351
+ sent: remote.sent,
352
+ skipped: remote.skipped,
353
+ };
354
+ },
345
355
  authSyncPushAll: () => authSync?.pushAll() ?? Promise.resolve({ sent: 0, skipped: 0 }),
346
356
  authSyncTest: () => authSync?.testPeers() ?? Promise.resolve({ sent: 0, replied: 0, missing: [] }),
347
357
  statusUpdated: () => writeAggregateStatus(),
@@ -500,6 +510,16 @@ async function runServeCli() {
500
510
  releaseAuthRefreshLease: (leaseId) => singleAuthSync?.releaseRefreshLease(leaseId)
501
511
  ?? singleLocalAuthRefreshLease.release(leaseId),
502
512
  getAuthSyncStatus: () => singleAuthSync?.getStatus() ?? null,
513
+ authSyncSafeAll: async () => {
514
+ const local = await singleMirror?.syncAllRuntimeCandidates() ?? { synced: 0, skipped: 0 };
515
+ const remote = await singleAuthSync?.pushAll() ?? { sent: 0, skipped: 0 };
516
+ return {
517
+ localSynced: local.synced,
518
+ localSkipped: local.skipped,
519
+ sent: remote.sent,
520
+ skipped: remote.skipped,
521
+ };
522
+ },
503
523
  authSyncPushAll: () => singleAuthSync?.pushAll() ?? Promise.resolve({ sent: 0, skipped: 0 }),
504
524
  authSyncTest: () => singleAuthSync?.testPeers() ?? Promise.resolve({ sent: 0, replied: 0, missing: [] }),
505
525
  statusUpdated: (status) => {
@@ -0,0 +1,102 @@
1
+ # FoxClaw Release Runbook
2
+
3
+ This file is for maintainers. User upgrades are driven by `/update` or `foxclaw update`, but those commands install `@foxden-app/foxclaw@latest` from the npm registry. Pushing a PR branch or a normal commit does not make installed nodes receive a new version.
4
+
5
+ ## Release Rules
6
+
7
+ - npm `latest` is the source of truth for `/update`.
8
+ - The GitHub `Publish` workflow is triggered by `v*` tags and also supports manual dispatch; normal releases should use tags.
9
+ - The tag name must match `v<package.json version>`. For example, `package.json` version `0.5.10` must be released with `v0.5.10`.
10
+ - Versions already present on npm are not published again. Reinstalling the same version may report `0.5.10 -> 0.5.10` in Telegram; that means registry latest did not move.
11
+ - `CHANGELOG.md` must contain the target version entry. `/update` reads that entry from the installed package and shows it as the upgrade notes.
12
+
13
+ ## Preflight
14
+
15
+ Check the worktree and remote state:
16
+
17
+ ```bash
18
+ git status --short --branch
19
+ npm pkg get name version
20
+ npm view @foxden-app/foxclaw version
21
+ git tag --list 'v*' --sort=-v:refname | head
22
+ ```
23
+
24
+ Confirm the target version and remote tag do not already exist:
25
+
26
+ ```bash
27
+ npm view @foxden-app/foxclaw@0.5.10 version 2>/dev/null || true
28
+ git ls-remote --tags origin refs/tags/v0.5.10
29
+ ```
30
+
31
+ Run the same local checks as the publish workflow:
32
+
33
+ ```bash
34
+ npm run lint
35
+ npm run typecheck
36
+ npm test
37
+ npm pack --dry-run
38
+ git diff --check
39
+ ```
40
+
41
+ `npm pack --dry-run` runs `prepack` and lists the package contents. Confirm `CHANGELOG.md` is included.
42
+
43
+ ## Prepare The Version
44
+
45
+ 1. Update `package.json` and `package-lock.json`.
46
+ 2. Add a new top entry to `CHANGELOG.md` with both `### 中文` and `### English` sections.
47
+ 3. Commit the release metadata:
48
+
49
+ ```bash
50
+ git add package.json package-lock.json CHANGELOG.md
51
+ git commit -m "发布 0.5.10:short release summary"
52
+ ```
53
+
54
+ ## Push And Publish
55
+
56
+ This repository uses lightweight tags:
57
+
58
+ ```bash
59
+ git tag v0.5.10
60
+ git push origin <branch>
61
+ git push origin v0.5.10
62
+ ```
63
+
64
+ Watch the publish workflow:
65
+
66
+ ```bash
67
+ gh run list --repo foxden-app/foxclaw --workflow Publish --limit 5
68
+ gh run watch <run-id> --repo foxden-app/foxclaw --exit-status
69
+ ```
70
+
71
+ After success, verify npm and GitHub Releases:
72
+
73
+ ```bash
74
+ npm view @foxden-app/foxclaw version
75
+ gh release view v0.5.10 --repo foxden-app/foxclaw --json tagName,name,url,publishedAt,isDraft,isPrerelease
76
+ ```
77
+
78
+ ## Verify `/update`
79
+
80
+ On a node still running the previous version, send `/update`. Expected:
81
+
82
+ ```text
83
+ FoxClaw upgraded and restarted: 0.5.9 -> 0.5.10.
84
+
85
+ What changed:
86
+ - ...
87
+ ```
88
+
89
+ If the node is already on the latest version, another `/update` may report:
90
+
91
+ ```text
92
+ FoxClaw upgraded and restarted: 0.5.10 -> 0.5.10.
93
+ ```
94
+
95
+ That means it reinstalled the current npm latest, not that a newer version was available.
96
+
97
+ ## Failure Handling
98
+
99
+ - If the workflow fails during version validation, check that the tag and `package.json` version match.
100
+ - If npm says the version already exists, do not reuse that version; publish the next patch after fixing the metadata.
101
+ - If npm publish succeeds but GitHub Release creation fails, rerun the workflow or repair the release notes manually.
102
+ - Do not delete and republish npm versions. Prefer publishing a fixed patch release; use npm dist-tags for emergency latest rollback only when necessary.
@@ -0,0 +1,102 @@
1
+ # FoxClaw 发布 runbook
2
+
3
+ 本文件给维护者使用。用户侧升级由 `/update` 或 `foxclaw update` 完成,但它们只会安装 npm registry 上的 `@foxden-app/foxclaw@latest`。推 PR、推普通分支或本地提交都不会让已安装节点收到新版。
4
+
5
+ ## 发布规则
6
+
7
+ - npm `latest` 是 `/update` 的事实来源。
8
+ - GitHub `Publish` workflow 由 `v*` tag 触发,也支持手动 dispatch;正常发布只使用 tag。
9
+ - tag 名必须等于 `v<package.json version>`,例如 `package.json` 是 `0.5.10` 时只能推 `v0.5.10`。
10
+ - npm 上已经存在的版本不会再次发布。重新安装同版本时,Telegram 可能显示 `0.5.10 -> 0.5.10`,这表示 registry latest 没有前进。
11
+ - `CHANGELOG.md` 必须包含目标版本条目。`/update` 完成回报会从已安装包读取这个条目展示“更新内容”。
12
+
13
+ ## 发布前检查
14
+
15
+ 确认工作区和远端状态:
16
+
17
+ ```bash
18
+ git status --short --branch
19
+ npm pkg get name version
20
+ npm view @foxden-app/foxclaw version
21
+ git tag --list 'v*' --sort=-v:refname | head
22
+ ```
23
+
24
+ 确认目标版本还没有发布,也没有远端 tag:
25
+
26
+ ```bash
27
+ npm view @foxden-app/foxclaw@0.5.10 version 2>/dev/null || true
28
+ git ls-remote --tags origin refs/tags/v0.5.10
29
+ ```
30
+
31
+ 运行与发布 workflow 对齐的本地校验:
32
+
33
+ ```bash
34
+ npm run lint
35
+ npm run typecheck
36
+ npm test
37
+ npm pack --dry-run
38
+ git diff --check
39
+ ```
40
+
41
+ `npm pack --dry-run` 会执行 `prepack` 构建,并列出 npm 包内容。确认输出里有 `CHANGELOG.md`。
42
+
43
+ ## 准备版本
44
+
45
+ 1. 更新 `package.json` 和 `package-lock.json` 的版本号。
46
+ 2. 在 `CHANGELOG.md` 顶部新增版本条目,包含 `### 中文` 和 `### English` 小节。
47
+ 3. 提交发布 commit,推荐格式:
48
+
49
+ ```bash
50
+ git add package.json package-lock.json CHANGELOG.md
51
+ git commit -m "发布 0.5.10:一句话说明"
52
+ ```
53
+
54
+ ## 推送并触发发布
55
+
56
+ 当前仓库使用 lightweight tag:
57
+
58
+ ```bash
59
+ git tag v0.5.10
60
+ git push origin <branch>
61
+ git push origin v0.5.10
62
+ ```
63
+
64
+ 观察发布 workflow:
65
+
66
+ ```bash
67
+ gh run list --repo foxden-app/foxclaw --workflow Publish --limit 5
68
+ gh run watch <run-id> --repo foxden-app/foxclaw --exit-status
69
+ ```
70
+
71
+ 成功后确认 npm 和 GitHub Release:
72
+
73
+ ```bash
74
+ npm view @foxden-app/foxclaw version
75
+ gh release view v0.5.10 --repo foxden-app/foxclaw --json tagName,name,url,publishedAt,isDraft,isPrerelease
76
+ ```
77
+
78
+ ## 验收 `/update`
79
+
80
+ 在仍安装旧版本的节点上发送 `/update`,期望看到:
81
+
82
+ ```text
83
+ FoxClaw 已升级并重启:0.5.9 -> 0.5.10。
84
+
85
+ 更新内容:
86
+ - ...
87
+ ```
88
+
89
+ 如果节点已经是最新版本,再次 `/update` 可能显示:
90
+
91
+ ```text
92
+ FoxClaw 已升级并重启:0.5.10 -> 0.5.10。
93
+ ```
94
+
95
+ 这只表示重新安装了当前 npm latest,并不代表有新版本。
96
+
97
+ ## 发布失败处理
98
+
99
+ - 如果 workflow 在版本校验失败,先检查 tag 和 `package.json` 是否一致。
100
+ - 如果 npm 显示版本已存在,不要复用同一个版本号;修正后发布下一个 patch 版本。
101
+ - 如果 npm publish 成功但 GitHub Release 创建失败,可以重新运行 workflow 或手动修复 release notes。
102
+ - 已发布到 npm 的版本不要删除重发。需要撤回线上 latest 时,优先发布修复版;只有紧急情况下才考虑调整 npm dist-tag。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.5.10",
3
+ "version": "0.5.11",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",