@foxden-app/foxclaw 0.3.19 → 0.4.1

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/dist/i18n.d.ts CHANGED
@@ -118,18 +118,27 @@ declare const MESSAGES: {
118
118
  readonly status_codex_local_usage_unavailable: "Codex local history: unavailable ({error})";
119
119
  readonly status_codex_credits: "Codex credits: {value}";
120
120
  readonly status_codex_limit_reached: "Codex limit: {value}";
121
+ readonly status_runtime_overview: "Telegram bot runtimes:";
122
+ readonly status_runtime_bot: "- {bot}: connected {connected}, auth {auth}, active turns {turns}";
123
+ readonly status_runtime_weixin: "- Weixin default runtime: connected {connected}, active turns {turns}";
124
+ readonly status_auth_mirror_none: "Last auth mirror: none recorded";
125
+ readonly status_auth_mirror_synced: "Last auth mirror: {candidate} from {source} at {time}";
126
+ readonly status_last_update_none: "Last service update: none recorded";
127
+ readonly status_last_update: "Last service update: {from} -> {to} at {time}";
128
+ readonly status_last_codex_update: "Last Codex update: {value}";
121
129
  readonly update_started: "FoxClaw update started. I will report here after installation, checks, and service restart complete.";
122
130
  readonly update_succeeded: "FoxClaw updated and restarted: {from} -> {to}.";
123
131
  readonly update_failed: "FoxClaw update failed: {error}\nRun foxclaw update in a terminal for details.";
124
132
  readonly update_unavailable: "Self-update is unavailable in this runtime. Run foxclaw update in a terminal.";
125
133
  readonly update_already_running: "A FoxClaw update is already running. I will report here when it finishes.";
126
- readonly update_blocked_active: "Cannot update FoxClaw while a turn, approval, or question is active. Wait or use /interrupt first.";
134
+ readonly update_blocked_active: "Cannot update FoxClaw while a runtime activity or auth mirror write is active. Wait or use /interrupt first.";
127
135
  readonly auth_reload_restarting: "Restarting Codex app-server to reload auth...";
128
136
  readonly auth_reload_done: "Codex app-server restarted. Current auth has been reloaded.";
129
137
  readonly auth_reload_blocked_active: "Cannot reload Codex auth while a turn, approval, or question is active. Wait or use /interrupt first.";
130
138
  readonly usage_auth: "Usage: /auth [list|use <n>|enable <n>|disable <n>|reload|add <name>]";
131
139
  readonly usage_auth_add: "Usage: /auth add <name>. Use letters, numbers, dot, dash, or underscore.";
132
140
  readonly auth_list_title: "Codex auth files:";
141
+ readonly auth_bot: "Bot runtime: {value}";
133
142
  readonly auth_current: "Current auth: {value}";
134
143
  readonly auth_dir: "Auth dir: {value}";
135
144
  readonly auth_candidate_count: "Candidates: {value}";
@@ -682,18 +691,27 @@ declare const MESSAGES: {
682
691
  readonly status_codex_local_usage_unavailable: "Codex 本地历史:无法获取({error})";
683
692
  readonly status_codex_credits: "Codex 额度:{value}";
684
693
  readonly status_codex_limit_reached: "Codex 限制:{value}";
694
+ readonly status_runtime_overview: "Telegram bot 运行时:";
695
+ readonly status_runtime_bot: "- {bot}:连接 {connected},auth {auth},进行中回复 {turns}";
696
+ readonly status_runtime_weixin: "- 微信默认运行时:连接 {connected},进行中回复 {turns}";
697
+ readonly status_auth_mirror_none: "最近 auth 镜像:暂无记录";
698
+ readonly status_auth_mirror_synced: "最近 auth 镜像:{candidate} 由 {source} 于 {time} 同步";
699
+ readonly status_last_update_none: "最近服务升级:暂无记录";
700
+ readonly status_last_update: "最近服务升级:{from} -> {to}({time})";
701
+ readonly status_last_codex_update: "最近 Codex 升级:{value}";
685
702
  readonly update_started: "已开始升级 FoxClaw。安装、自检和服务重启完成后,我会在这里回报结果。";
686
703
  readonly update_succeeded: "FoxClaw 已升级并重启:{from} -> {to}。";
687
704
  readonly update_failed: "FoxClaw 升级失败:{error}\n请在终端运行 foxclaw update 查看详情。";
688
705
  readonly update_unavailable: "当前运行方式不支持自升级,请在终端运行 foxclaw update。";
689
706
  readonly update_already_running: "FoxClaw 升级已经在进行中,结束后我会在这里回报结果。";
690
- readonly update_blocked_active: "当前有回复、审批或问题在进行中,不能升级 FoxClaw。请先等待,或使用 /interrupt。";
707
+ readonly update_blocked_active: "当前有 runtime 操作或 auth 镜像写入在进行中,不能升级 FoxClaw。请先等待,或使用 /interrupt。";
691
708
  readonly auth_reload_restarting: "正在重启 Codex app-server 以重新读取 auth...";
692
709
  readonly auth_reload_done: "Codex app-server 已重启,当前 auth 已重新读取。";
693
710
  readonly auth_reload_blocked_active: "当前有回复、审批或问题在进行中,不能重载 Codex auth。请先等待,或使用 /interrupt。";
694
711
  readonly usage_auth: "用法:/auth [list|use <编号>|enable <编号>|disable <编号>|reload|add <名称>]";
695
712
  readonly usage_auth_add: "用法:/auth add <名称>。名称只能包含字母、数字、点、短横线或下划线。";
696
713
  readonly auth_list_title: "Codex auth 文件:";
714
+ readonly auth_bot: "Bot runtime:{value}";
697
715
  readonly auth_current: "当前 auth:{value}";
698
716
  readonly auth_dir: "Auth 目录:{value}";
699
717
  readonly auth_candidate_count: "候选数量:{value}";
package/dist/i18n.js CHANGED
@@ -116,18 +116,27 @@ const MESSAGES = {
116
116
  status_codex_local_usage_unavailable: 'Codex local history: unavailable ({error})',
117
117
  status_codex_credits: 'Codex credits: {value}',
118
118
  status_codex_limit_reached: 'Codex limit: {value}',
119
+ status_runtime_overview: 'Telegram bot runtimes:',
120
+ status_runtime_bot: '- {bot}: connected {connected}, auth {auth}, active turns {turns}',
121
+ status_runtime_weixin: '- Weixin default runtime: connected {connected}, active turns {turns}',
122
+ status_auth_mirror_none: 'Last auth mirror: none recorded',
123
+ status_auth_mirror_synced: 'Last auth mirror: {candidate} from {source} at {time}',
124
+ status_last_update_none: 'Last service update: none recorded',
125
+ status_last_update: 'Last service update: {from} -> {to} at {time}',
126
+ status_last_codex_update: 'Last Codex update: {value}',
119
127
  update_started: 'FoxClaw update started. I will report here after installation, checks, and service restart complete.',
120
128
  update_succeeded: 'FoxClaw updated and restarted: {from} -> {to}.',
121
129
  update_failed: 'FoxClaw update failed: {error}\nRun foxclaw update in a terminal for details.',
122
130
  update_unavailable: 'Self-update is unavailable in this runtime. Run foxclaw update in a terminal.',
123
131
  update_already_running: 'A FoxClaw update is already running. I will report here when it finishes.',
124
- update_blocked_active: 'Cannot update FoxClaw while a turn, approval, or question is active. Wait or use /interrupt first.',
132
+ update_blocked_active: 'Cannot update FoxClaw while a runtime activity or auth mirror write is active. Wait or use /interrupt first.',
125
133
  auth_reload_restarting: 'Restarting Codex app-server to reload auth...',
126
134
  auth_reload_done: 'Codex app-server restarted. Current auth has been reloaded.',
127
135
  auth_reload_blocked_active: 'Cannot reload Codex auth while a turn, approval, or question is active. Wait or use /interrupt first.',
128
136
  usage_auth: 'Usage: /auth [list|use <n>|enable <n>|disable <n>|reload|add <name>]',
129
137
  usage_auth_add: 'Usage: /auth add <name>. Use letters, numbers, dot, dash, or underscore.',
130
138
  auth_list_title: 'Codex auth files:',
139
+ auth_bot: 'Bot runtime: {value}',
131
140
  auth_current: 'Current auth: {value}',
132
141
  auth_dir: 'Auth dir: {value}',
133
142
  auth_candidate_count: 'Candidates: {value}',
@@ -680,18 +689,27 @@ const MESSAGES = {
680
689
  status_codex_local_usage_unavailable: 'Codex 本地历史:无法获取({error})',
681
690
  status_codex_credits: 'Codex 额度:{value}',
682
691
  status_codex_limit_reached: 'Codex 限制:{value}',
692
+ status_runtime_overview: 'Telegram bot 运行时:',
693
+ status_runtime_bot: '- {bot}:连接 {connected},auth {auth},进行中回复 {turns}',
694
+ status_runtime_weixin: '- 微信默认运行时:连接 {connected},进行中回复 {turns}',
695
+ status_auth_mirror_none: '最近 auth 镜像:暂无记录',
696
+ status_auth_mirror_synced: '最近 auth 镜像:{candidate} 由 {source} 于 {time} 同步',
697
+ status_last_update_none: '最近服务升级:暂无记录',
698
+ status_last_update: '最近服务升级:{from} -> {to}({time})',
699
+ status_last_codex_update: '最近 Codex 升级:{value}',
683
700
  update_started: '已开始升级 FoxClaw。安装、自检和服务重启完成后,我会在这里回报结果。',
684
701
  update_succeeded: 'FoxClaw 已升级并重启:{from} -> {to}。',
685
702
  update_failed: 'FoxClaw 升级失败:{error}\n请在终端运行 foxclaw update 查看详情。',
686
703
  update_unavailable: '当前运行方式不支持自升级,请在终端运行 foxclaw update。',
687
704
  update_already_running: 'FoxClaw 升级已经在进行中,结束后我会在这里回报结果。',
688
- update_blocked_active: '当前有回复、审批或问题在进行中,不能升级 FoxClaw。请先等待,或使用 /interrupt。',
705
+ update_blocked_active: '当前有 runtime 操作或 auth 镜像写入在进行中,不能升级 FoxClaw。请先等待,或使用 /interrupt。',
689
706
  auth_reload_restarting: '正在重启 Codex app-server 以重新读取 auth...',
690
707
  auth_reload_done: 'Codex app-server 已重启,当前 auth 已重新读取。',
691
708
  auth_reload_blocked_active: '当前有回复、审批或问题在进行中,不能重载 Codex auth。请先等待,或使用 /interrupt。',
692
709
  usage_auth: '用法:/auth [list|use <编号>|enable <编号>|disable <编号>|reload|add <名称>]',
693
710
  usage_auth_add: '用法:/auth add <名称>。名称只能包含字母、数字、点、短横线或下划线。',
694
711
  auth_list_title: 'Codex auth 文件:',
712
+ auth_bot: 'Bot runtime:{value}',
695
713
  auth_current: '当前 auth:{value}',
696
714
  auth_dir: 'Auth 目录:{value}',
697
715
  auth_candidate_count: '候选数量:{value}',
package/dist/main.js CHANGED
@@ -10,7 +10,7 @@ import { APP_HOME, DEFAULT_CODEX_TELEGRAM_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PA
10
10
  import { acquireProcessLock, LockHeldError } from './lock.js';
11
11
  import { readRuntimeStatus, writeRuntimeStatus } from './runtime.js';
12
12
  import { refreshFoxclawExecStartDropIns, removeFoxclawExecStartDropIns } from './systemd.js';
13
- import { createSelfUpdateRuntime, inferPnpmHomeFromEntryPoint, performSelfUpdate } from './update.js';
13
+ import { createSelfUpdateRuntime, inferPnpmHomeFromEntryPoint, performSelfUpdate, readSelfUpdateStatus, writeSelfUpdateStatus, } from './update.js';
14
14
  const rawCommand = process.argv[2];
15
15
  const command = rawCommand || 'serve';
16
16
  loadEnv();
@@ -162,6 +162,7 @@ async function runServeCli() {
162
162
  const processLock = acquireProcessLock(config.lockPath);
163
163
  let store = null;
164
164
  let weixinAdapter = null;
165
+ let activeWeixinCore = null;
165
166
  let activeTelegramAdapters = [];
166
167
  let managedApps = [];
167
168
  let activeAuthMirror = null;
@@ -184,23 +185,26 @@ async function runServeCli() {
184
185
  tgScopeBotId: id,
185
186
  codexAuthDir: home,
186
187
  codexHome: home,
188
+ codexAppAutolaunch: false,
187
189
  codexAppServerStatePath: path.join(APP_HOME, 'runtime', `codex-app-server-${id}.json`),
188
190
  codexAppServerLogPath: path.join(APP_HOME, 'logs', `codex-app-server-${id}.log`),
189
191
  };
190
- const app = new CodexAppClient(runtimeConfig.codexCliBin, runtimeConfig.codexAppLaunchCmd, runtimeConfig.codexAppAutolaunch, runtimeConfig.codexAppServerStatePath, runtimeConfig.codexAppServerLogPath, logger, { CODEX_HOME: home });
192
+ const app = new CodexAppClient(runtimeConfig.codexCliBin, runtimeConfig.codexAppLaunchCmd, runtimeConfig.codexAppAutolaunch, runtimeConfig.codexAppServerStatePath, runtimeConfig.codexAppServerLogPath, logger, { CODEX_HOME: home }, ['cli_auth_credentials_store="file"']);
191
193
  seeds.push({ id, home, config: runtimeConfig, bot, app });
192
194
  }
193
195
  const canonicalAuthDir = config.codexAuthDir ?? config.codexHome ?? path.join(os.homedir(), '.codex');
194
196
  const mirror = new AuthCandidateMirror(canonicalAuthDir, seeds.map((runtime) => ({
195
197
  id: runtime.id,
198
+ label: runtime.bot.username ? `@${runtime.bot.username}` : runtime.id,
196
199
  authDir: runtime.home,
200
+ validate: async (context) => validateRefreshedAuthCandidate(runtime, context.candidateName),
197
201
  notify: async (message) => {
198
202
  const chatId = store.getTelegramPrivateChatId(runtime.id);
199
203
  if (chatId) {
200
204
  await runtime.bot.sendMessage(chatId, message);
201
205
  }
202
206
  },
203
- })), logger);
207
+ })), logger, path.join(APP_HOME, 'runtime', 'auth-mirror.json'));
204
208
  await mirror.initialize();
205
209
  mirror.start();
206
210
  activeAuthMirror = mirror;
@@ -213,21 +217,29 @@ async function runServeCli() {
213
217
  logPath: path.join(APP_HOME, 'logs', 'update.log'),
214
218
  codexCliBin: config.codexCliBin,
215
219
  });
220
+ const lastSelfUpdatePath = path.join(APP_HOME, 'runtime', 'last-self-update.json');
221
+ let lastSelfUpdate = readSelfUpdateStatus(lastSelfUpdatePath);
216
222
  const runtimes = [];
217
223
  const writeAggregateStatus = (running = true) => {
218
224
  const statuses = runtimes.map((runtime) => runtime.core.getRuntimeStatus());
225
+ const weixinStatus = activeWeixinCore?.getRuntimeStatus() ?? null;
219
226
  const first = statuses[0] ?? null;
220
227
  writeRuntimeStatus(config.statusPath, {
221
228
  running,
222
- connected: running && statuses.every((status) => status.connected),
229
+ connected: running
230
+ && statuses.every((status) => status.connected)
231
+ && (!weixinStatus || weixinStatus.connected),
223
232
  userAgent: first?.userAgent ?? null,
224
233
  ...(first?.codexAppServer ? { codexAppServer: first.codexAppServer } : {}),
225
234
  botUsername: first?.botUsername ?? null,
226
235
  currentBindings: store.countBindings(),
227
236
  pendingApprovals: store.countPendingApprovals(),
228
237
  pendingUserInputs: store.countPendingUserInputs(),
229
- activeTurns: statuses.reduce((sum, status) => sum + status.activeTurns, 0),
230
- lastError: statuses.find((status) => status.lastError)?.lastError ?? null,
238
+ activeTurns: statuses.reduce((sum, status) => sum + status.activeTurns, 0)
239
+ + (weixinStatus?.activeTurns ?? 0),
240
+ lastError: statuses.find((status) => status.lastError)?.lastError
241
+ ?? weixinStatus?.lastError
242
+ ?? null,
231
243
  updatedAt: new Date().toISOString(),
232
244
  channels: { telegram: running, weixin: running && config.wxEnabled },
233
245
  bots: runtimes.map((runtime, index) => ({
@@ -237,24 +249,63 @@ async function runServeCli() {
237
249
  activeTurns: running ? (statuses[index]?.activeTurns ?? 0) : 0,
238
250
  ...(statuses[index]?.codexAppServer ? { codexAppServer: statuses[index].codexAppServer } : {}),
239
251
  })),
252
+ ...(weixinStatus ? {
253
+ weixinRuntime: {
254
+ connected: running && weixinStatus.connected,
255
+ activeTurns: running ? weixinStatus.activeTurns : 0,
256
+ ...(weixinStatus.codexAppServer ? { codexAppServer: weixinStatus.codexAppServer } : {}),
257
+ },
258
+ } : {}),
259
+ authMirror: mirror.getStatus(),
260
+ lastUpdate: lastSelfUpdate,
240
261
  });
241
262
  };
242
263
  const coordinator = {
243
- canSelfUpdate: () => runtimes.every((runtime) => runtime.core.isIdleForServiceUpdate()),
264
+ canSelfUpdate: () => runtimes.every((runtime) => runtime.core.isIdleForServiceUpdate())
265
+ && (!activeWeixinCore || activeWeixinCore.isIdleForServiceUpdate())
266
+ && mirror.isIdle(),
244
267
  authCandidateUpdated: (runtimeId, candidateName) => mirror.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined),
245
268
  statusUpdated: () => writeAggregateStatus(),
269
+ getServiceStatus: async () => ({
270
+ bots: await Promise.all(runtimes.map(async (runtime) => {
271
+ const status = runtime.core.getRuntimeStatus();
272
+ return {
273
+ id: runtime.id,
274
+ username: status.botUsername ?? runtime.bot.username,
275
+ connected: status.connected,
276
+ activeTurns: status.activeTurns,
277
+ currentAuth: await runtime.core.getCurrentAuthLabel().catch(() => null),
278
+ ...(status.codexAppServer ? { codexAppServer: status.codexAppServer } : {}),
279
+ };
280
+ })),
281
+ ...(activeWeixinCore ? {
282
+ weixinRuntime: {
283
+ connected: activeWeixinCore.getRuntimeStatus().connected,
284
+ activeTurns: activeWeixinCore.getRuntimeStatus().activeTurns,
285
+ codexAppServer: activeWeixinCore.getRuntimeStatus().codexAppServer,
286
+ },
287
+ } : {}),
288
+ authMirror: mirror.getStatus(),
289
+ lastUpdate: lastSelfUpdate,
290
+ }),
291
+ selfUpdateCompleted: (status) => {
292
+ lastSelfUpdate = status;
293
+ writeSelfUpdateStatus(lastSelfUpdatePath, status);
294
+ writeAggregateStatus();
295
+ },
246
296
  };
247
- for (const [index, seed] of seeds.entries()) {
297
+ for (const seed of seeds) {
248
298
  const telegramMessaging = new TelegramMessagingPort(seed.bot);
249
- const weixinMessaging = index === 0 && config.wxEnabled
250
- ? new WeixinMessagingPort(store, (id) => loadWeixinAccount(config.weixinAccountsDir, id))
251
- : null;
252
- const outbound = new BridgeMessagingRouter(telegramMessaging, weixinMessaging);
299
+ const outbound = new BridgeMessagingRouter(telegramMessaging, null);
253
300
  const core = new BridgeSessionCore(seed.config, store, logger, seed.bot, seed.app, outbound, selfUpdater, coordinator);
254
301
  runtimes.push({ ...seed, core, telegram: new TelegramChannelAdapter(core) });
255
302
  }
256
303
  if (config.wxEnabled) {
257
- weixinAdapter = new WeixinChannelAdapter(runtimes[0].core, store, runtimes[0].config, logger);
304
+ const weixinApp = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger);
305
+ const outbound = new BridgeMessagingRouter(new TelegramMessagingPort(seeds[0].bot), new WeixinMessagingPort(store, (id) => loadWeixinAccount(config.weixinAccountsDir, id)));
306
+ activeWeixinCore = new BridgeSessionCore(config, store, logger, seeds[0].bot, weixinApp, outbound, selfUpdater, coordinator, false);
307
+ managedApps.push(weixinApp);
308
+ weixinAdapter = new WeixinChannelAdapter(activeWeixinCore, store, config, logger);
258
309
  }
259
310
  activeTelegramAdapters = runtimes.map((runtime) => runtime.telegram);
260
311
  process.on('unhandledRejection', (error) => {
@@ -267,6 +318,7 @@ async function runServeCli() {
267
318
  await runtime.telegram.start();
268
319
  }
269
320
  if (weixinAdapter) {
321
+ await activeWeixinCore.startCodexApp();
270
322
  await weixinAdapter.start();
271
323
  }
272
324
  writeAggregateStatus();
@@ -275,10 +327,11 @@ async function runServeCli() {
275
327
  logger.info('bridge.shutting_down', { signal });
276
328
  mirror.stop();
277
329
  await weixinAdapter?.stop();
330
+ await activeWeixinCore?.stop();
278
331
  await Promise.all(runtimes.map((runtime) => runtime.telegram.stop()));
279
332
  writeAggregateStatus(false);
280
- await Promise.all(runtimes.map((runtime) => runtime.app.stop({ terminateServer: true }).catch((error) => {
281
- logger.warn('codex.app-server.stop_failed', { runtimeId: runtime.id, error: serializeError(error) });
333
+ await Promise.all(managedApps.map((app) => app.stop({ terminateServer: true }).catch((error) => {
334
+ logger.warn('codex.app-server.stop_failed', { error: serializeError(error) });
282
335
  })));
283
336
  store?.close();
284
337
  processLock.release();
@@ -352,6 +405,7 @@ async function runServeCli() {
352
405
  catch (error) {
353
406
  activeAuthMirror?.stop();
354
407
  await weixinAdapter?.stop().catch(() => { });
408
+ await activeWeixinCore?.stop().catch(() => { });
355
409
  await Promise.allSettled(activeTelegramAdapters.map((adapter) => adapter.stop()));
356
410
  await Promise.allSettled(managedApps.map((app) => app.stop({ terminateServer: true })));
357
411
  store?.close();
@@ -359,6 +413,26 @@ async function runServeCli() {
359
413
  throw error;
360
414
  }
361
415
  }
416
+ async function validateRefreshedAuthCandidate(runtime, candidateName) {
417
+ const authPath = path.join(runtime.home, 'auth.json');
418
+ const candidatePath = path.join(runtime.home, candidateName);
419
+ const [currentTarget, candidateTarget] = await Promise.all([
420
+ fs.promises.realpath(authPath).catch(() => null),
421
+ fs.promises.realpath(candidatePath).catch(() => null),
422
+ ]);
423
+ if (!currentTarget || !candidateTarget || currentTarget !== candidateTarget) {
424
+ return { ok: false, reason: 'candidate is not the current auth target' };
425
+ }
426
+ if (!runtime.app.isConnected()) {
427
+ return { ok: false, reason: 'source app-server is not connected' };
428
+ }
429
+ const account = await runtime.app.readAccount();
430
+ if (!account || account.type !== 'chatgpt' || account.requiresOpenaiAuth) {
431
+ return { ok: false, reason: 'source app-server did not report an active ChatGPT account' };
432
+ }
433
+ await runtime.app.readAccountRateLimits();
434
+ return { ok: true };
435
+ }
362
436
  async function initConfig() {
363
437
  const envPath = process.env.FOXCLAW_ENV?.trim() || DEFAULT_ENV_PATH;
364
438
  fs.mkdirSync(path.dirname(envPath), { recursive: true });
package/dist/types.d.ts CHANGED
@@ -345,6 +345,25 @@ export interface RuntimeStatus {
345
345
  username: string | null;
346
346
  connected: boolean;
347
347
  activeTurns: number;
348
+ currentAuth?: string | null;
348
349
  codexAppServer?: RuntimeStatus['codexAppServer'];
349
350
  }>;
351
+ weixinRuntime?: {
352
+ connected: boolean;
353
+ activeTurns: number;
354
+ codexAppServer?: RuntimeStatus['codexAppServer'];
355
+ };
356
+ authMirror?: {
357
+ candidateName: string;
358
+ sourceRuntimeId: string;
359
+ sourceLabel: string;
360
+ syncedAt: string;
361
+ } | null;
362
+ lastUpdate?: {
363
+ state: string;
364
+ fromVersion: string;
365
+ toVersion: string | null;
366
+ codexUpdate?: string | null;
367
+ updatedAt: string;
368
+ } | null;
350
369
  }
@@ -17,7 +17,7 @@ Optional later:
17
17
  - `TG_ALLOWED_CHAT_ID`
18
18
  - `TG_ALLOWED_TOPIC_ID`
19
19
 
20
- Use private Telegram chat first. Configure groups/topics only after private chat works.
20
+ Use private Telegram chat first. Configure groups/topics only after private chat works. Multiple tokens mean independent Codex runtimes inside one service; validate each bot privately.
21
21
 
22
22
  ## Copy-Paste Prompt
23
23
 
@@ -45,11 +45,12 @@ Tasks:
45
45
  5. Run foxclaw init, then write ~/.foxclaw/.env. Never print or commit the bot token.
46
46
  6. Run foxclaw doctor.
47
47
  7. Start FoxClaw with foxclaw start.
48
- 8. Ask me to send /help and /status to the Telegram bot.
48
+ 8. Ask me to send /help, /status, and /auth privately to each configured Telegram bot; confirm /auth names that bot runtime.
49
49
  9. Verify the final state:
50
50
  - foxclaw.service is active/enabled on Linux
51
51
  - foxclaw status works
52
52
  10. Report the commands used, the final status, and the log command I should use if something stops working. Redact TG_BOT_TOKENS and never print the full token or full .env content.
53
+ 11. If multiple bots are enabled, confirm foxclaw status lists independent app-servers; group-chat tests must mention or reply to the intended bot.
53
54
  ```
54
55
 
55
56
  ## Safety Notes
@@ -123,7 +123,7 @@ npm install -g @foxden-app/foxclaw
123
123
  foxclaw init
124
124
  ```
125
125
 
126
- This creates the config file at `~/.foxclaw/.env` and prompts for the Telegram bot token, your numeric Telegram user id, and the default workspace.
126
+ This creates the config file at `~/.foxclaw/.env` and prompts for one or more comma-separated Telegram bot tokens, your numeric Telegram user id, and the default workspace. New installs use `TG_BOT_TOKENS`; legacy `TG_BOT_TOKEN` is only for existing single-bot configurations.
127
127
 
128
128
  If you prefer pnpm:
129
129
 
@@ -154,6 +154,14 @@ DEFAULT_SANDBOX_MODE=workspace-write
154
154
 
155
155
  Keep `TG_ALLOWED_CHAT_ID=` and `TG_ALLOWED_TOPIC_ID=` empty for the first install. Do not delete those lines; leaving them empty means private-chat mode.
156
156
 
157
+ Start with one bot. To add three independent Codex lanes later, put the tokens on the same line:
158
+
159
+ ```dotenv
160
+ TG_BOT_TOKENS=123456789:token_a,234567890:token_b,345678901:token_c
161
+ ```
162
+
163
+ FoxClaw still installs one service, but each bot receives its own app-server, session home, and current auth selection. After startup, privately send `/help` and `/status` to each bot.
164
+
157
165
  `DEFAULT_CWD` must be a real folder. Examples:
158
166
 
159
167
  ```dotenv
@@ -280,7 +288,7 @@ Update FoxClaw later:
280
288
  foxclaw update
281
289
  ```
282
290
 
283
- You can also send `/update` in an authorized Telegram chat. When every bot is idle with no approval or question active, it attempts to update an npm/pnpm-managed Codex CLI, upgrades FoxClaw, checks, restarts the service, and reports the result after restart.
291
+ You can also send `/update` in an authorized Telegram chat. When every Telegram bot runtime, an enabled Weixin default runtime, and auth mirror writes are idle, it attempts to update an npm/pnpm-managed Codex CLI, upgrades FoxClaw, checks, restarts the service, and reports the result through the bot that started it.
284
292
 
285
293
  ## Next Step
286
294
 
@@ -182,6 +182,18 @@ Bridge logs are stored here:
182
182
  tail -f ~/.foxclaw/logs/service.log
183
183
  ```
184
184
 
185
+ ## Checking Multi-Bot Mode
186
+
187
+ After configuring `TG_BOT_TOKENS`, `foxclaw status` should contain one bot id, connection status, and independent app-server for each token. Send `/status` privately to any bot to see the runtime summary; send `/auth` to confirm the panel names the current `@botname` and its auth directory.
188
+
189
+ Each isolated app-server log is stored at:
190
+
191
+ ```bash
192
+ tail -f ~/.foxclaw/logs/codex-app-server-bot<id>.log
193
+ ```
194
+
195
+ When multiple bots share a group, unaddressed messages intentionally do not trigger them; mention `@botname`, reply to the intended bot, or send `/status@botname`. When enabled, Weixin stays on the default Codex runtime and does not appear inside an isolated Telegram bot's thread list.
196
+
185
197
  ## ChatGPT Backend 403 Or Unable To Load Site
186
198
 
187
199
  If Telegram shows `ChatGPT backend 403 Forbidden`, or the app-server log contains `Unable to load site`, `cf-ray`, or `chatgpt.com/backend-api`, the auth file is not necessarily broken. The service process is usually reaching ChatGPT with the wrong network/proxy/IP.
@@ -230,10 +230,10 @@ Later commands are sorted by recent usage. Plain text, photos, and files continu
230
230
 
231
231
  ### 3.2 `/status`, `/account`, `/quota`, `/update`
232
232
 
233
- - `/status`: FoxClaw, app-server, current thread binding, model, access, and Codex usage summary. Local session, token, and visible-reply-throughput metrics use a background-generated historical snapshot instead of scanning large logs during the request; throughput is computed end-to-end for completed turns, excluding reasoning tokens while including waiting and tool execution time.
233
+ - `/status`: FoxClaw, app-server, current thread binding, model, access, and Codex usage summary. In multi-bot mode it also lists every bot's connection, current auth, active turns, and the most recent auth mirror and service/Codex update outcomes. Local session, token, and visible-reply-throughput metrics use a background-generated historical snapshot instead of scanning large logs during the request; throughput is computed end-to-end for completed turns, excluding reasoning tokens while including waiting and tool execution time.
234
234
  - `/account`: current Codex account.
235
235
  - `/quota`: Codex usage and quota window.
236
- - `/update`: upgrade FoxClaw, attempt to update an npm/pnpm-managed Codex CLI, run checks, and restart the service; it refuses while any bot runtime has a turn, approval, or question active, then reports the result after restart.
236
+ - `/update`: upgrade FoxClaw, attempt to update an npm/pnpm-managed Codex CLI, run checks, and restart the service; it refuses while any Telegram bot runtime, an enabled Weixin default runtime, or an auth mirror write is busy, then reports the result through the initiating bot after restart.
237
237
 
238
238
  ### 3.3 `/config`, `/requirements`, `/provider`
239
239
 
@@ -335,7 +335,7 @@ Watch mode mirrors live turn progress and approval requests. The watching chat i
335
335
 
336
336
  ## 6. Codex Login And Auth Rotation
337
337
 
338
- This is a key FoxClaw feature. Codex auth is usually stored at `~/.codex/auth.json`. FoxClaw stores multiple accounts as candidate files and switches which candidate the active `auth.json` points to. In `TG_BOT_TOKENS` mode, each bot has an isolated Codex home, app-server, and current candidate, so bots can run and switch accounts independently; validated login/refresh credentials are safely mirrored between bot homes.
338
+ This is a key FoxClaw feature. Codex auth is usually stored at `~/.codex/auth.json`. FoxClaw stores multiple accounts as candidate files and switches which candidate the active `auth.json` points to. In `TG_BOT_TOKENS` mode, each bot has an isolated Codex home, app-server, and current candidate, so bots can run and switch accounts independently; isolated Telegram runtimes force file-backed credential storage. Validated login/refresh credentials are safely mirrored between bot homes, but sessions are never shared.
339
339
 
340
340
  ### 6.1 File Format
341
341
 
@@ -387,7 +387,7 @@ If the login is cancelled or fails, FoxClaw tries to restore the previous auth t
387
387
 
388
388
  ### 6.3 The `/auth` Panel
389
389
 
390
- `/auth` lists candidate accounts, the current account, and the auth directory. It also provides buttons for switching, disabling, login, and reload. 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.
390
+ `/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.
391
391
 
392
392
  Approximation:
393
393
 
@@ -17,7 +17,7 @@
17
17
  - `TG_ALLOWED_CHAT_ID`
18
18
  - `TG_ALLOWED_TOPIC_ID`
19
19
 
20
- 第一次请先用 Telegram 私聊跑通。群组和话题模式等私聊稳定后再开。
20
+ 第一次请先用 Telegram 私聊跑通。群组和话题模式等私聊稳定后再开。多个 token 表示同一服务内多个独立 Codex runtime;每个 bot 都必须单独完成私聊验收。
21
21
 
22
22
  ## 复制给 agent 的安装提示词
23
23
 
@@ -44,11 +44,12 @@ DEFAULT_CWD=<把绝对工作目录粘贴在这里>
44
44
  5. 运行 foxclaw init,然后写入 ~/.foxclaw/.env。不要打印或提交 bot token。
45
45
  6. 运行 foxclaw doctor。
46
46
  7. 用 foxclaw start 启动 FoxClaw。
47
- 8. 让我在 Telegram bot 里发送 /help 和 /status
47
+ 8. 让我在每个已配置的 Telegram bot 私聊里发送 /help、/status 和 /auth;确认 /auth 显示对应 bot runtime
48
48
  9. 验证最终状态:
49
49
  - Linux 上 foxclaw.service 处于 active/enabled
50
50
  - foxclaw status 可以正常输出
51
51
  10. 汇报执行过的命令、最终状态和后续看日志的命令。请隐藏 TG_BOT_TOKENS,不要打印完整 token 或完整 .env。
52
+ 11. 如果启用了多个 bot,确认 foxclaw status 列出独立 app-server;群聊测试必须点名或回复目标 bot。
52
53
  ```
53
54
 
54
55
  ## 安全注意事项
@@ -14,11 +14,13 @@
14
14
  2. 让 Codex 使用 `skills/foxclaw`。
15
15
  3. 如果是远程机器,提供 SSH 目标。
16
16
  4. 让 Codex 执行安装、写配置、跑 `foxclaw doctor`。
17
- 5. 启动服务后,在 Telegram bot 里发送 `/help` 和 `/status` 验证。
17
+ 5. 启动服务后,在每个配置的 Telegram bot 私聊里发送 `/help`、`/status` 和 `/auth` 验证。
18
18
 
19
19
  ## 注意事项
20
20
 
21
21
  - 不要让 agent 把完整 bot token 打印到日志或提交到仓库。
22
22
  - 第一次请先用私聊模式跑通。
23
+ - 多个 token 会在同一服务中建立多个独立 Codex home、session 与 auth 选择;群组中必须点名或回复目标 bot。
24
+ - 同时启用微信时,微信仍使用默认 Codex runtime,不共享隔离 Telegram bot 的线程。
23
25
  - 不要把整个 home 目录或根目录作为首次 `DEFAULT_CWD`。
24
26
  - 只有在 `doctor` 通过、服务已启动、Telegram 首条消息验证通过后,才算安装完成。
@@ -121,7 +121,7 @@ npm install -g @foxden-app/foxclaw
121
121
  foxclaw init
122
122
  ```
123
123
 
124
- 这会创建默认配置文件 `~/.foxclaw/.env`,并提示你填写 Telegram bot tokenTelegram 数字用户 ID 和默认工作目录。
124
+ 这会创建默认配置文件 `~/.foxclaw/.env`,并提示你填写一个或多个 Telegram bot token(多个用英文逗号分隔)、Telegram 数字用户 ID 和默认工作目录。新安装使用 `TG_BOT_TOKENS`;旧的 `TG_BOT_TOKEN` 仅用于兼容已有单 bot 配置。
125
125
 
126
126
  如果你用 pnpm:
127
127
 
@@ -152,6 +152,14 @@ DEFAULT_SANDBOX_MODE=workspace-write
152
152
 
153
153
  `TG_ALLOWED_CHAT_ID=` 和 `TG_ALLOWED_TOPIC_ID=` 第一次保持为空,不要删掉这两行;留空表示私聊模式。
154
154
 
155
+ 只有一个 bot 时先按上面的配置跑通即可。需要三条互不打断的 Codex 会话时,把多个 token 写在同一行:
156
+
157
+ ```dotenv
158
+ TG_BOT_TOKENS=123456789:token_a,234567890:token_b,345678901:token_c
159
+ ```
160
+
161
+ FoxClaw 仍只安装一个服务,但每个 bot 会有独立 app-server、会话目录和当前 auth。服务启动后请分别私聊每个 bot 发送 `/help` 与 `/status`。
162
+
155
163
  `DEFAULT_CWD` 必须是真实存在的目录,例如:
156
164
 
157
165
  ```dotenv
@@ -278,7 +286,7 @@ foxclaw uninstall-systemd
278
286
  foxclaw update
279
287
  ```
280
288
 
281
- 也可以在已授权的 Telegram 私聊里发送 `/update`。它会在所有 bot 都没有运行中回复、审批或待确认问题时,尝试升级 npm/pnpm 安装的 Codex CLI,完成 FoxClaw 升级、自检和服务重启,并在重启后回报结果。
289
+ 也可以在已授权的 Telegram 私聊里发送 `/update`。它会在所有 Telegram bot、已启用的微信默认 runtime 和 auth 镜像写入都空闲时,尝试升级 npm/pnpm 安装的 Codex CLI,完成 FoxClaw 升级、自检和服务重启,并在发起命令的 bot 中回报结果。
282
290
 
283
291
  如果 `~/.foxclaw/.env` 已经存在,`foxclaw init` 会先询问是否更新 Telegram 和工作目录相关字段,其它配置保持不变。
284
292
 
@@ -183,6 +183,18 @@ Bridge 日志默认在:
183
183
  tail -f ~/.foxclaw/logs/service.log
184
184
  ```
185
185
 
186
+ ## 多 bot 模式核查
187
+
188
+ 配置 `TG_BOT_TOKENS` 后,`foxclaw status` 的 `bots` 列表应为每个 token 显示一个 bot id、连接状态和独立 app-server。私聊任一 bot 发送 `/status` 会显示全部 runtime 摘要;发送 `/auth` 应显示当前 `@botname` 和该 bot 的 auth 目录。
189
+
190
+ 每个隔离 app-server 的日志路径为:
191
+
192
+ ```bash
193
+ tail -f ~/.foxclaw/logs/codex-app-server-bot<id>.log
194
+ ```
195
+
196
+ 群组里配置了多个 bot 时,普通未点名消息不会触发它们;使用 `@botname`、回复目标 bot,或 `/status@botname`。微信启用后仍使用默认 Codex runtime,不会出现在某个 Telegram bot 的隔离线程中。
197
+
186
198
  ## ChatGPT 后端 403 或 Unable to load site
187
199
 
188
200
  如果 Telegram 里看到 `ChatGPT backend 403 Forbidden`,或者 app-server 日志里出现 `Unable to load site`、`cf-ray`、`chatgpt.com/backend-api`,通常不是 `auth.json` 文件坏了,而是服务进程访问 ChatGPT 后端时没有走正确网络。
@@ -230,10 +230,10 @@ TG_ALLOWED_TOPIC_ID=42
230
230
 
231
231
  ### 3.2 `/status`、`/account`、`/quota`、`/update`
232
232
 
233
- - `/status`:查看 FoxClaw、app-server、当前绑定线程、模型、权限和 Codex 用量摘要。本地 session/Token/可见答复吞吐使用后台生成的历史快照,避免状态查询现场扫描大量日志;答复吞吐按完成轮次端到端耗时计算,排除推理 token,但包含等待与工具执行时间。
233
+ - `/status`:查看 FoxClaw、app-server、当前绑定线程、模型、权限和 Codex 用量摘要。多 bot 模式还会列出所有 bot 的连接、当前 auth、活动 turn,以及最近一次 auth 镜像和服务/Codex 升级结果。本地 session/Token/可见答复吞吐使用后台生成的历史快照,避免状态查询现场扫描大量日志;答复吞吐按完成轮次端到端耗时计算,排除推理 token,但包含等待与工具执行时间。
234
234
  - `/account`:查看当前 Codex 登录账号。
235
235
  - `/quota`:查看 Codex 用量和额度窗口。
236
- - `/update`:升级 FoxClaw,并尝试升级 npm/pnpm 安装的 Codex CLI,然后自检和重启服务;任意 bot runtime 有运行中回复、审批或待确认问题时都会拒绝执行,重启后会回报结果。
236
+ - `/update`:升级 FoxClaw,并尝试升级 npm/pnpm 安装的 Codex CLI,然后自检和重启服务;任意 Telegram bot、启用的微信默认 runtime 或 auth 镜像写入不空闲时都会拒绝执行,重启后通过发起命令的 bot 回报结果。
237
237
 
238
238
  ### 3.3 `/config`、`/requirements`、`/provider`
239
239
 
@@ -335,7 +335,7 @@ FoxClaw 的聊天是“绑定线程”的。你在手机上打开某个 Codex
335
335
 
336
336
  ## 6. Codex 登录和 auth 轮转
337
337
 
338
- 这是 FoxClaw 的特色功能。Codex 的登录状态通常保存在 `~/.codex/auth.json`。FoxClaw 把多个账号保存成候选文件,并通过切换 `auth.json` 指向哪个候选来换号。启用 `TG_BOT_TOKENS` 多 bot 模式后,每个 bot 使用独立 Codex home、独立 app-server 和独立当前候选,因此可以并行运行、单独切号;已验证的登录/刷新凭据会安全镜像到其他 bot home。
338
+ 这是 FoxClaw 的特色功能。Codex 的登录状态通常保存在 `~/.codex/auth.json`。FoxClaw 把多个账号保存成候选文件,并通过切换 `auth.json` 指向哪个候选来换号。启用 `TG_BOT_TOKENS` 多 bot 模式后,每个 bot 使用独立 Codex home、独立 app-server 和独立当前候选,因此可以并行运行、单独切号;隔离 Telegram runtime 会强制使用文件凭据存储。已验证的登录/刷新凭据会安全镜像到其他 bot home,但不会共享 session
339
339
 
340
340
  ### 6.1 文件格式
341
341
 
@@ -387,7 +387,7 @@ cp -L ~/.codex/auth.json ~/.codex/auth.json_personal
387
387
 
388
388
  ### 6.3 `/auth` 面板
389
389
 
390
- `/auth` 会列出候选账号、当前账号和 auth 目录,并提供按钮切换、禁用、登录和重载。每个候选名前的 `5h|7d` 数字表示上次记录到的两个额度窗口剩余百分比;当前 auth 会在打开面板时刷新,其他候选不会为了查询额度被自动切换。
390
+ `/auth` 会列出候选账号、当前账号和 auth 目录,并提供按钮切换、禁用、登录和重载。多 bot 模式中,面板顶部还会显示当前正在管理的 `@botname`,因为该 bot 内的私聊、群聊和话题共享同一个当前 auth。每个候选名前的 `5h|7d` 数字表示上次记录到的两个额度窗口剩余百分比;当前 auth 会在打开面板时刷新,其他候选不会为了查询额度被自动切换。
391
391
 
392
392
  示意:
393
393
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.3.19",
3
+ "version": "0.4.1",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -65,6 +65,8 @@ Telegram behavior:
65
65
  - private chat with `TG_ALLOWED_USER_ID` remains available even when `TG_ALLOWED_CHAT_ID` or `TG_ALLOWED_TOPIC_ID` is set
66
66
  - `TG_ALLOWED_CHAT_ID` and `TG_ALLOWED_TOPIC_ID` choose the default group or topic scope; they do not disable private chat
67
67
  - when `TG_BOT_TOKENS` contains multiple bots, one FoxClaw service starts independent Codex app-servers and auth selections per bot
68
+ - isolated Telegram runtimes use independent `CODEX_HOME` directories and file-backed credentials; they do not share Codex sessions
69
+ - if Weixin is enabled alongside multiple Telegram bots, it remains on the default Codex runtime instead of borrowing a Telegram bot runtime
68
70
  - in a group with multiple configured bots, address a bot by mention, reply, or suffixed command such as `/status@botname`
69
71
 
70
72
  If `TG_ALLOWED_CHAT_ID` or `TG_ALLOWED_TOPIC_ID` is missing, read [references/telegram-setup.md](./references/telegram-setup.md) and explicitly guide the user through collecting it.
@@ -142,7 +144,8 @@ After either bootstrap path:
142
144
  - `privacy mode` is disabled
143
145
  - the bot is an admin in the group
144
146
  - the configured `TG_ALLOWED_CHAT_ID` and `TG_ALLOWED_TOPIC_ID` match the target group/topic
145
- 5. If group or topic mode is enabled, also verify that private chat still responds for the configured `TG_ALLOWED_USER_ID`.
147
+ 5. If group or topic mode is enabled, also verify that private chat still responds for the configured `TG_ALLOWED_USER_ID`.
148
+ 6. With multiple bots, send `/status` and `/auth` privately to each bot; verify the status lists each app-server and the auth panel names the intended bot runtime.
146
149
 
147
150
  ## First Telegram Message Check
148
151
 
@@ -192,7 +195,7 @@ Use this checklist when the user asks for standard closing actions, release wrap
192
195
  - For macOS launchd, use the launchd install/start path from this skill and verify with `node dist/main.js status`.
193
196
  - Verify the running service reports the expected FoxClaw version in `status`.
194
197
  - If `doctor` fails only because `DEFAULT_CWD` is missing, report that separately; do not treat it as evidence that the service update failed.
195
- - `/update` now attempts to update globally npm/pnpm-managed Codex CLI installations and refuses to restart while any configured bot runtime is busy.
198
+ - `/update` now attempts to update globally npm/pnpm-managed Codex CLI installations and refuses to restart while any configured Telegram runtime, enabled Weixin default runtime, or auth mirror write is busy.
196
199
  7. Publish to npm when requested:
197
200
  - Prefer GitHub Actions trusted publishing via `.github/workflows/publish.yml`: bump and commit the package version, push `main`, then push a matching `v<version>` tag. The tag version must match `package.json`.
198
201
  - Treat `workflow_dispatch` only as a retry path from an existing matching release tag; do not manually run publishing from `main`.