@foxden-app/foxclaw 0.7.2 → 0.10.0

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.
Files changed (71) hide show
  1. package/.env.example +14 -0
  2. package/CHANGELOG.md +129 -0
  3. package/README.md +22 -2
  4. package/README_EN.md +22 -2
  5. package/dist/antigravity/adapter.d.ts +17 -0
  6. package/dist/antigravity/adapter.js +256 -0
  7. package/dist/antigravity/auth.d.ts +121 -0
  8. package/dist/antigravity/auth.js +896 -0
  9. package/dist/antigravity/client.d.ts +32 -0
  10. package/dist/antigravity/client.js +238 -0
  11. package/dist/antigravity/controller.d.ts +96 -0
  12. package/dist/antigravity/controller.js +1922 -0
  13. package/dist/antigravity/conversations.d.ts +29 -0
  14. package/dist/antigravity/conversations.js +289 -0
  15. package/dist/antigravity/events.d.ts +101 -0
  16. package/dist/antigravity/events.js +114 -0
  17. package/dist/antigravity/runtime.d.ts +47 -0
  18. package/dist/antigravity/runtime.js +82 -0
  19. package/dist/auth/cross_node_sync.js +2 -2
  20. package/dist/channels/telegram/telegram_channel_adapter.d.ts +10 -3
  21. package/dist/channels/telegram/telegram_channel_adapter.js +2 -1
  22. package/dist/channels/weixin/weixin_channel_adapter.d.ts +5 -1
  23. package/dist/codex_app/adapter.d.ts +20 -0
  24. package/dist/codex_app/adapter.js +213 -0
  25. package/dist/codex_app/client.d.ts +2 -1
  26. package/dist/codex_app/client.js +47 -4
  27. package/dist/codex_app/force_takeover.d.ts +12 -0
  28. package/dist/codex_app/force_takeover.js +27 -0
  29. package/dist/config.d.ts +12 -0
  30. package/dist/config.js +42 -0
  31. package/dist/controller/controller.d.ts +15 -5
  32. package/dist/controller/controller.js +252 -20
  33. package/dist/core/attachments.d.ts +11 -0
  34. package/dist/core/attachments.js +56 -0
  35. package/dist/core/engine_spi.d.ts +76 -0
  36. package/dist/core/engine_spi.js +1 -0
  37. package/dist/core/orchestrator.d.ts +101 -0
  38. package/dist/core/orchestrator.js +1404 -0
  39. package/dist/core/stream_preview.d.ts +29 -0
  40. package/dist/core/stream_preview.js +146 -0
  41. package/dist/core/turn_queue.d.ts +19 -0
  42. package/dist/core/turn_queue.js +41 -0
  43. package/dist/i18n.d.ts +36 -6
  44. package/dist/i18n.js +81 -6
  45. package/dist/main.js +190 -20
  46. package/dist/opencode/adapter.d.ts +11 -0
  47. package/dist/opencode/adapter.js +168 -0
  48. package/dist/store/database.d.ts +41 -0
  49. package/dist/store/database.js +242 -43
  50. package/dist/store/token_usage.d.ts +23 -0
  51. package/dist/store/token_usage.js +61 -0
  52. package/dist/telegram/api.js +34 -1
  53. package/dist/telegram/bot_home.d.ts +3 -0
  54. package/dist/telegram/bot_home.js +107 -0
  55. package/dist/telegram/gateway.d.ts +5 -0
  56. package/dist/telegram/gateway.js +21 -0
  57. package/dist/types.d.ts +1 -0
  58. package/dist/update.d.ts +6 -0
  59. package/dist/update.js +78 -6
  60. package/dist/voice/target.js +2 -1
  61. package/dist/voice/tts.js +4 -2
  62. package/docs/devlogs//346/212/200/346/234/257/351/200/232/350/256/257/02./347/273/237/344/270/200/351/200/232/351/201/223/350/260/203/345/272/246/345/231/250/344/270/216/345/217/257/346/217/222/346/213/224/345/274/225/346/223/216SPI/357/274/232FoxClaw/344/273/216/345/215/225/344/275/223/350/265/260/345/220/221/345/244/232Agent/345/272/225/345/272/247.md +192 -0
  63. package/docs/devlogs//347/254/254/344/270/200/344/272/272/347/247/260/347/211/210/02./347/257/207/344/272/214_/345/275/223/344/270/200/344/270/252/346/241/245/346/216/245/345/231/250/345/274/200/345/247/213/345/255/246/344/274/232/345/220/254/346/207/202/346/211/200/346/234/211Agent.md +45 -0
  64. package/docs/user-manual.md +11 -1
  65. package/docs/zh/2026-09-05-bot-home-migration.md +26 -0
  66. package/docs/zh/2026-09-05-reliability-review.md +53 -0
  67. package/docs/zh/troubleshooting.md +12 -0
  68. package/docs/zh/user-manual.md +13 -1
  69. package/package.json +2 -2
  70. package/scripts/force-takeover.py +118 -0
  71. package/scripts/force-takeover.test.py +122 -0
package/dist/main.js CHANGED
@@ -7,8 +7,9 @@ import process from 'node:process';
7
7
  import { createInterface } from 'node:readline/promises';
8
8
  import { spawnSync } from 'node:child_process';
9
9
  import { fileURLToPath } from 'node:url';
10
- import { APP_HOME, buildCodexApiProviderOverrides, DEFAULT_CODEX_TELEGRAM_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, getLoadedEnvPath, loadConfig, loadEnv, } from './config.js';
10
+ import { APP_HOME, buildCodexApiProviderOverrides, DEFAULT_CODEX_TELEGRAM_HOME, DEFAULT_ANTIGRAVITY_TELEGRAM_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, getLoadedEnvPath, loadConfig, loadEnv, } from './config.js';
11
11
  import { createAuthRefreshNotificationAggregator, } from './auth/notifications.js';
12
+ import { getAntigravityTelegramCommands } from './i18n.js';
12
13
  import { installBundledCodexSkills } from './codex_skills.js';
13
14
  import { acquireProcessLock, LockHeldError } from './lock.js';
14
15
  import { readRuntimeStatus, writeRuntimeStatus } from './runtime.js';
@@ -18,6 +19,7 @@ import { clearPendingClusterUpdateBroadcast, createSelfUpdateRuntime, inferPnpmH
18
19
  import { TELEGRAM_VOICE_MAX_BYTES, TELEGRAM_VOICE_SUPPORTED_EXTENSIONS, telegramVoiceContentType, } from './voice/files.js';
19
20
  import { inferTelegramBotId, resolveTelegramVoiceTarget } from './voice/target.js';
20
21
  import { shouldShowRuntimeLastUpdate } from './update_status.js';
22
+ import { prepareTelegramBotHome } from './telegram/bot_home.js';
21
23
  const rawCommand = process.argv[2];
22
24
  const command = rawCommand || 'serve';
23
25
  loadEnv();
@@ -225,6 +227,40 @@ async function main() {
225
227
  }
226
228
  return;
227
229
  }
230
+ if (command === 'resume') {
231
+ const args = process.argv.slice(3);
232
+ const botIndex = args.indexOf('--bot-id');
233
+ const botId = botIndex >= 0 ? args[botIndex + 1] : null;
234
+ if (botIndex >= 0)
235
+ args.splice(botIndex, 2);
236
+ if ((botIndex >= 0 && !botId) || args.length > 1 || args[0]?.startsWith('-')) {
237
+ throw new Error('Usage: foxclaw resume [thread-id] [--bot-id <bot-id>]');
238
+ }
239
+ const status = readRuntimeStatus(process.env.STATUS_PATH || DEFAULT_STATUS_PATH);
240
+ const runtime = botId ? status?.bots?.find(bot => bot.id === botId) : status;
241
+ if (!runtime)
242
+ throw new Error('No matching runtime status. Check foxclaw status.');
243
+ if (!botId && new Set(status?.bots?.map(bot => bot.codexAppServer?.port).filter(Boolean)).size > 1) {
244
+ throw new Error('Multiple Codex servers found. Select one with --bot-id from foxclaw status --json.');
245
+ }
246
+ const server = runtime.codexAppServer;
247
+ if (!server?.running || !server.pid || !server.port) {
248
+ throw new Error('Codex app-server is not running. Check foxclaw status.');
249
+ }
250
+ try {
251
+ process.kill(server.pid, 0);
252
+ }
253
+ catch {
254
+ throw new Error('Codex app-server status is stale; the process has exited.');
255
+ }
256
+ const result = spawnSync(process.env.CODEX_CLI_BIN || 'codex', [
257
+ 'resume', '--remote', `ws://127.0.0.1:${server.port}`, ...args,
258
+ ], { stdio: 'inherit' });
259
+ if (result.error)
260
+ throw result.error;
261
+ process.exitCode = result.status ?? 1;
262
+ return;
263
+ }
228
264
  if (command === 'doctor') {
229
265
  const failed = !runDoctorChecks();
230
266
  process.exit(failed ? 1 : 0);
@@ -278,6 +314,7 @@ Usage:
278
314
  foxclaw init
279
315
  foxclaw doctor
280
316
  foxclaw status
317
+ foxclaw resume [thread-id] [--bot-id <bot-id>]
281
318
  foxclaw start|restart|stop
282
319
  foxclaw update
283
320
  foxclaw send-voice <path> [caption]
@@ -552,7 +589,7 @@ function truncateStatusLine(value, maxLength) {
552
589
  return normalized.length <= maxLength ? normalized : `${normalized.slice(0, Math.max(0, maxLength - 1))}…`;
553
590
  }
554
591
  async function runServeCli() {
555
- const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter }, { AuthCandidateMirror }, { CrossNodeAuthSync }, { OpencodeTelegramRuntime },] = await Promise.all([
592
+ const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter }, { AuthCandidateMirror }, { CrossNodeAuthSync }, { OpencodeTelegramRuntime }, { AntigravityTelegramRuntime }, { UnifiedBridgeCore }, { AntigravityAppClient }, { AntigravityAuthManager },] = await Promise.all([
556
593
  import('./channels/bridge_messaging_router.js'),
557
594
  import('./channels/telegram/telegram_messaging_port.js'),
558
595
  import('./channels/weixin/weixin_channel_adapter.js'),
@@ -568,6 +605,10 @@ async function runServeCli() {
568
605
  import('./auth/mirror.js'),
569
606
  import('./auth/cross_node_sync.js'),
570
607
  import('./opencode/runtime.js'),
608
+ import('./antigravity/runtime.js'),
609
+ import('./antigravity/controller.js'),
610
+ import('./antigravity/client.js'),
611
+ import('./antigravity/auth.js'),
571
612
  ]);
572
613
  const config = loadConfig();
573
614
  const logger = new Logger(config.logLevel, config.logPath);
@@ -583,26 +624,85 @@ async function runServeCli() {
583
624
  let activeAuthMirror = null;
584
625
  let activeAuthSync = null;
585
626
  let activeOpencodeRuntime = null;
627
+ const activeAntigravityRuntimes = [];
628
+ let sharedCodexApp = null;
629
+ let sharedAntigravityApp = null;
630
+ let sharedAntigravityAuth = null;
586
631
  try {
587
632
  store = new BridgeStore(config.storePath);
633
+ sharedAntigravityAuth = new AntigravityAuthManager(config.antigravityAuthDir, logger);
634
+ sharedAntigravityApp = new AntigravityAppClient(config.antigravityCliBin, logger);
635
+ if (config.codexCliBin) {
636
+ sharedCodexApp = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger, config.codexHome ? { CODEX_HOME: config.codexHome } : null, codexApiProviderOverrides);
637
+ }
588
638
  if (config.opencodeBotToken) {
589
639
  activeOpencodeRuntime = new OpencodeTelegramRuntime(config, store, logger);
590
640
  await activeOpencodeRuntime.start();
591
641
  logger.info('opencode.bridge.started', activeOpencodeRuntime.getRuntimeStatus());
592
642
  }
643
+ if (config.antigravityBotTokens.length > 0) {
644
+ for (const token of config.antigravityBotTokens) {
645
+ const bot = new TelegramGateway(token, config.tgAllowedUserId, config.tgAllowedChatId, config.telegramPollIntervalMs, store, logger, true, getAntigravityTelegramCommands);
646
+ const id = await bot.initializeIdentity();
647
+ const username = await bot.resolveUsername();
648
+ const sharedDefaultRuntime = config.antigravityDefaultRuntimeBotToken
649
+ ? config.antigravityDefaultRuntimeBotToken === token
650
+ : (token === config.antigravityBotTokens[0]);
651
+ let home;
652
+ let authDir;
653
+ let app;
654
+ let auth;
655
+ if (sharedDefaultRuntime) {
656
+ home = os.homedir();
657
+ authDir = config.antigravityAuthDir;
658
+ app = sharedAntigravityApp;
659
+ auth = sharedAntigravityAuth;
660
+ }
661
+ else {
662
+ home = prepareTelegramBotHome(DEFAULT_ANTIGRAVITY_TELEGRAM_HOME, id, username, null);
663
+ authDir = path.join(home, '.gemini', 'antigravity-cli');
664
+ fs.mkdirSync(authDir, { recursive: true, mode: 0o700 });
665
+ app = new AntigravityAppClient(config.antigravityCliBin, logger, { HOME: home });
666
+ auth = new AntigravityAuthManager(authDir, logger);
667
+ }
668
+ const runtimeConfig = {
669
+ ...config,
670
+ antigravityBotToken: token,
671
+ antigravityBotTokens: [token],
672
+ tgScopeBotId: id,
673
+ antigravityAuthDir: authDir,
674
+ };
675
+ const runtime = new AntigravityTelegramRuntime(runtimeConfig, store, logger, {
676
+ botToken: token,
677
+ botId: id,
678
+ botUsername: username ?? undefined,
679
+ sharedDefaultRuntime,
680
+ home,
681
+ authDir,
682
+ codexApp: sharedCodexApp ?? undefined,
683
+ app,
684
+ auth,
685
+ });
686
+ await runtime.start();
687
+ activeAntigravityRuntimes.push(runtime);
688
+ logger.info('antigravity.bridge.started', runtime.getRuntimeStatus());
689
+ }
690
+ }
593
691
  if (config.tgMultiBotMode) {
594
692
  const seeds = [];
595
693
  const canonicalAuthDir = config.codexAuthDir ?? config.codexHome ?? path.join(os.homedir(), '.codex');
596
- for (const token of config.tgBotTokens) {
694
+ const identities = await Promise.all(config.tgBotTokens.map(async (token) => {
597
695
  const bot = new TelegramGateway(token, config.tgAllowedUserId, config.tgAllowedChatId, config.telegramPollIntervalMs, store, logger, true);
598
696
  const id = await bot.initializeIdentity();
697
+ const username = await bot.resolveUsername();
698
+ return { token, bot, id, username };
699
+ }));
700
+ for (const { token, bot, id, username } of identities) {
599
701
  if (seeds.some((runtime) => runtime.id === id)) {
600
702
  throw new Error(`TG_BOT_TOKENS contains duplicate Telegram bot identity: ${id}`);
601
703
  }
602
704
  const sharedDefaultRuntime = config.tgDefaultRuntimeBotToken === token;
603
- const home = sharedDefaultRuntime
604
- ? (config.codexHome ?? path.join(os.homedir(), '.codex'))
605
- : path.join(DEFAULT_CODEX_TELEGRAM_HOME, id, 'home');
705
+ const home = prepareTelegramBotHome(DEFAULT_CODEX_TELEGRAM_HOME, id, username, sharedDefaultRuntime ? (config.codexHome ?? path.join(os.homedir(), '.codex')) : null);
606
706
  const authDir = sharedDefaultRuntime ? canonicalAuthDir : home;
607
707
  if (!sharedDefaultRuntime) {
608
708
  fs.mkdirSync(home, { recursive: true, mode: 0o700 });
@@ -614,7 +714,7 @@ async function runServeCli() {
614
714
  tgBotTokens: [token],
615
715
  tgScopeBotId: id,
616
716
  codexAuthDir: sharedDefaultRuntime ? config.codexAuthDir : home,
617
- codexHome: sharedDefaultRuntime ? config.codexHome : home,
717
+ codexHome: home,
618
718
  codexAppAutolaunch: sharedDefaultRuntime ? config.codexAppAutolaunch : false,
619
719
  codexAppServerStatePath: sharedDefaultRuntime
620
720
  ? config.codexAppServerStatePath
@@ -623,9 +723,7 @@ async function runServeCli() {
623
723
  ? config.codexAppServerLogPath
624
724
  : path.join(APP_HOME, 'logs', `codex-app-server-${id}.log`),
625
725
  };
626
- const childEnv = sharedDefaultRuntime
627
- ? (config.codexHome ? { CODEX_HOME: config.codexHome } : null)
628
- : { CODEX_HOME: home };
726
+ const childEnv = { CODEX_HOME: home };
629
727
  const app = new CodexAppClient(runtimeConfig.codexCliBin, runtimeConfig.codexAppLaunchCmd, runtimeConfig.codexAppAutolaunch, runtimeConfig.codexAppServerStatePath, runtimeConfig.codexAppServerLogPath, logger, childEnv, [
630
728
  ...codexApiProviderOverrides,
631
729
  ...(sharedDefaultRuntime ? [] : ['cli_auth_credentials_store="file"']),
@@ -661,6 +759,7 @@ async function runServeCli() {
661
759
  statusPath: config.statusPath,
662
760
  logPath: path.join(APP_HOME, 'logs', 'update.log'),
663
761
  codexCliBin: config.codexCliBin,
762
+ agyCliBin: config.antigravityCliBin,
664
763
  });
665
764
  const lastSelfUpdatePath = path.join(APP_HOME, 'runtime', 'last-self-update.json');
666
765
  let lastSelfUpdate = readSelfUpdateStatus(lastSelfUpdatePath);
@@ -707,6 +806,20 @@ async function runServeCli() {
707
806
  ...(weixinStatus.codexAppServer ? { codexAppServer: weixinStatus.codexAppServer } : {}),
708
807
  },
709
808
  } : {}),
809
+ ...(activeAntigravityRuntimes.length > 0 ? {
810
+ antigravityBots: activeAntigravityRuntimes.map((runtime) => {
811
+ const status = runtime.getRuntimeStatus();
812
+ return {
813
+ id: runtime.id,
814
+ username: status.botUsername ?? runtime.botGateway.username,
815
+ connected: running && Boolean(status.connected),
816
+ activeTurns: running ? (status.activeTurns ?? 0) : 0,
817
+ runtimeKind: runtime.isSharedDefaultRuntime ? 'default' : 'isolated',
818
+ home: runtime.botHome,
819
+ authDir: runtime.authDirectory,
820
+ };
821
+ }),
822
+ } : {}),
710
823
  authMirror: mirror.getStatus(),
711
824
  authSync: authSync?.getStatus() ?? null,
712
825
  authProactiveRefresh,
@@ -803,7 +916,14 @@ async function runServeCli() {
803
916
  for (const seed of seeds) {
804
917
  const telegramMessaging = new TelegramMessagingPort(seed.bot);
805
918
  const outbound = new BridgeMessagingRouter(telegramMessaging, null);
806
- const core = new BridgeSessionCore(seed.config, store, logger, seed.bot, seed.app, outbound, selfUpdater, coordinator);
919
+ const codexCore = new BridgeSessionCore(seed.config, store, logger, seed.bot, seed.app, outbound, selfUpdater, coordinator, false);
920
+ const core = new UnifiedBridgeCore(seed.config, store, logger, seed.bot, sharedAntigravityApp ?? undefined, sharedAntigravityAuth ?? undefined, telegramMessaging, {
921
+ codexApp: seed.app,
922
+ codexCore,
923
+ antigravityApp: sharedAntigravityApp ?? undefined,
924
+ antigravityAuth: sharedAntigravityAuth ?? undefined,
925
+ defaultBackendId: 'codex',
926
+ });
807
927
  runtimes.push({ ...seed, core, telegram: new TelegramChannelAdapter(core) });
808
928
  }
809
929
  if (config.wxEnabled) {
@@ -889,8 +1009,10 @@ async function runServeCli() {
889
1009
  await activeWeixinCore?.stop();
890
1010
  await Promise.all(runtimes.map((runtime) => runtime.telegram.stop()));
891
1011
  await activeOpencodeRuntime?.stop();
1012
+ await Promise.all(activeAntigravityRuntimes.map((runtime) => runtime.stop().catch(() => { })));
892
1013
  writeAggregateStatus(false);
893
- await Promise.all(managedApps.map((app) => app.stop({ terminateServer: true }).catch((error) => {
1014
+ const allManaged = [...managedApps, ...(sharedCodexApp ? [sharedCodexApp] : [])];
1015
+ await Promise.all(allManaged.map((app) => app.stop({ terminateServer: true }).catch((error) => {
894
1016
  logger.warn('codex.app-server.stop_failed', { error: serializeError(error) });
895
1017
  })));
896
1018
  store?.close();
@@ -904,12 +1026,13 @@ async function runServeCli() {
904
1026
  const singleCodexHome = config.codexHome ?? path.join(os.homedir(), '.codex');
905
1027
  installBundledCodexSkills(packageRoot, singleCodexHome);
906
1028
  const bot = new TelegramGateway(config.tgBotToken, config.tgAllowedUserId, config.tgAllowedChatId, config.telegramPollIntervalMs, store, logger);
907
- const app = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger, config.codexHome ? { CODEX_HOME: config.codexHome } : null, codexApiProviderOverrides);
1029
+ const app = sharedCodexApp ?? new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger, config.codexHome ? { CODEX_HOME: config.codexHome } : null, codexApiProviderOverrides);
908
1030
  const telegramMessaging = new TelegramMessagingPort(bot);
909
1031
  const weixinMessaging = config.wxEnabled
910
1032
  ? new WeixinMessagingPort(store, (id) => loadWeixinAccount(config.weixinAccountsDir, id))
911
1033
  : null;
912
1034
  const outbound = new BridgeMessagingRouter(telegramMessaging, weixinMessaging);
1035
+ void outbound;
913
1036
  const selfUpdater = createSelfUpdateRuntime({
914
1037
  entryPoint,
915
1038
  nodePath: process.execPath,
@@ -917,10 +1040,32 @@ async function runServeCli() {
917
1040
  statusPath: config.statusPath,
918
1041
  logPath: path.join(APP_HOME, 'logs', 'update.log'),
919
1042
  codexCliBin: config.codexCliBin,
1043
+ agyCliBin: config.antigravityCliBin,
920
1044
  });
921
1045
  let singleAuthSync = null;
922
1046
  let singleMirror = null;
923
1047
  let core = null;
1048
+ const writeSingleStatus = (running = true) => {
1049
+ const coreStatus = core?.getRuntimeStatus();
1050
+ writeRuntimeStatus(config.statusPath, {
1051
+ running,
1052
+ connected: running && Boolean(coreStatus?.connected),
1053
+ userAgent: coreStatus?.userAgent ?? app.getUserAgent(),
1054
+ codexHome: coreStatus?.codexHome ?? singleCodexHome,
1055
+ ...(coreStatus?.codexAppServer ? { codexAppServer: coreStatus.codexAppServer } : app.getServerStatus() ? { codexAppServer: app.getServerStatus() } : {}),
1056
+ botUsername: coreStatus?.botUsername ?? bot.username,
1057
+ currentBindings: store?.countBindings() ?? 0,
1058
+ pendingApprovals: store?.countPendingApprovals() ?? 0,
1059
+ pendingUserInputs: store?.countPendingUserInputs() ?? 0,
1060
+ queuedTurns: store?.countQueuedTurnInputs() ?? 0,
1061
+ activeTurns: coreStatus?.activeTurns ?? 0,
1062
+ lastError: null,
1063
+ updatedAt: new Date().toISOString(),
1064
+ channels: { telegram: running, weixin: running && config.wxEnabled },
1065
+ authMirror: singleMirror?.getStatus() ?? null,
1066
+ authSync: singleAuthSync?.getStatus() ?? null,
1067
+ });
1068
+ };
924
1069
  const singleAuthDir = config.codexAuthDir ?? config.codexHome ?? process.env.CODEX_AUTH_DIR ?? path.join(os.homedir(), '.codex');
925
1070
  const singleLocalAuthRefreshLease = createLocalAuthRefreshLease();
926
1071
  const singleAuthSyncLocalIdle = () => Boolean(core?.isIdleForServiceUpdate())
@@ -973,12 +1118,9 @@ async function runServeCli() {
973
1118
  authSyncPushAll: () => singleAuthSync?.pushAll() ?? Promise.resolve({ sent: 0, skipped: 0 }),
974
1119
  authSyncTest: () => singleAuthSync?.testPeers() ?? Promise.resolve({ sent: 0, replied: 0, missing: [] }),
975
1120
  authSyncAudit: () => singleAuthSync?.auditCluster() ?? Promise.resolve(null),
976
- statusUpdated: (status) => {
977
- writeRuntimeStatus(config.statusPath, {
978
- ...status,
979
- authMirror: singleMirror?.getStatus() ?? null,
980
- authSync: singleAuthSync?.getStatus() ?? null,
981
- });
1121
+ statusUpdated: (_status) => {
1122
+ void _status;
1123
+ writeSingleStatus(true);
982
1124
  },
983
1125
  } : null;
984
1126
  if (config.authSyncEnabled) {
@@ -1002,7 +1144,15 @@ async function runServeCli() {
1002
1144
  await singleMirror.initialize();
1003
1145
  activeAuthMirror = singleMirror;
1004
1146
  }
1005
- core = new BridgeSessionCore(config, store, logger, bot, app, outbound, selfUpdater, singleCoordinator);
1147
+ const singleOutbound = new BridgeMessagingRouter(telegramMessaging, null);
1148
+ const singleCodexCore = new BridgeSessionCore(config, store, logger, bot, app, singleOutbound, selfUpdater, singleCoordinator, false);
1149
+ core = new UnifiedBridgeCore(config, store, logger, bot, sharedAntigravityApp ?? undefined, sharedAntigravityAuth ?? undefined, telegramMessaging, {
1150
+ codexApp: app,
1151
+ codexCore: singleCodexCore,
1152
+ antigravityApp: sharedAntigravityApp ?? undefined,
1153
+ antigravityAuth: sharedAntigravityAuth ?? undefined,
1154
+ defaultBackendId: 'codex',
1155
+ });
1006
1156
  if (config.authSyncEnabled && singleMirror) {
1007
1157
  await bot.initializeIdentity();
1008
1158
  singleAuthSync = new CrossNodeAuthSync(buildAuthSyncConfig(config, bot.username ? `@${bot.username}` : 'default'), logger, {
@@ -1060,6 +1210,7 @@ async function runServeCli() {
1060
1210
  if (weixinAdapter) {
1061
1211
  await weixinAdapter.start();
1062
1212
  }
1213
+ writeSingleStatus(true);
1063
1214
  logger.info('bridge.started', core.getRuntimeStatus());
1064
1215
  const shutdown = async (signal) => {
1065
1216
  logger.info('bridge.shutting_down', { signal });
@@ -1069,6 +1220,7 @@ async function runServeCli() {
1069
1220
  await weixinAdapter?.stop();
1070
1221
  await telegram.stop();
1071
1222
  await activeOpencodeRuntime?.stop();
1223
+ await Promise.all(activeAntigravityRuntimes.map((runtime) => runtime.stop().catch(() => { })));
1072
1224
  writeRuntimeStatus(config.statusPath, {
1073
1225
  running: false,
1074
1226
  connected: false,
@@ -1103,6 +1255,7 @@ async function runServeCli() {
1103
1255
  await activeWeixinCore?.stop().catch(() => { });
1104
1256
  await Promise.allSettled(activeTelegramAdapters.map((adapter) => adapter.stop()));
1105
1257
  await activeOpencodeRuntime?.stop().catch(() => { });
1258
+ await Promise.allSettled(activeAntigravityRuntimes.map((runtime) => runtime.stop()));
1106
1259
  await Promise.allSettled(managedApps.map((app) => app.stop({ terminateServer: true })));
1107
1260
  store?.close();
1108
1261
  processLock.release();
@@ -1797,6 +1950,23 @@ function runDoctorChecks() {
1797
1950
  ].map((value) => value.trim()).filter(Boolean);
1798
1951
  checks.push(['OpenCode uses an independent Telegram bot', !codexTokens.includes(process.env.OPENCODE_BOT_TOKEN.trim())]);
1799
1952
  }
1953
+ const antigravityTokens = [
1954
+ ...(process.env.ANTIGRAVITY_BOT_TOKENS ?? '').split(','),
1955
+ process.env.ANTIGRAVITY_BOT_TOKEN ?? '',
1956
+ ].map((value) => value.trim()).filter(Boolean);
1957
+ if (antigravityTokens.length > 0) {
1958
+ const configuredAgyBin = process.env.ANTIGRAVITY_CLI_BIN;
1959
+ checks.push(['antigravity (agy) cli available', hasConfiguredCommand(configuredAgyBin, 'agy')]);
1960
+ const codexTokens = [
1961
+ ...(process.env.TG_BOT_TOKENS ?? '').split(','),
1962
+ process.env.TG_BOT_TOKEN ?? '',
1963
+ ].map((value) => value.trim()).filter(Boolean);
1964
+ const hasConflict = antigravityTokens.some((t) => codexTokens.includes(t));
1965
+ checks.push(['Antigravity uses independent Telegram bot(s)', !hasConflict]);
1966
+ const authDir = process.env.ANTIGRAVITY_AUTH_DIR || path.join(os.homedir(), '.gemini', 'antigravity-cli');
1967
+ const hasAuth = fs.existsSync(path.join(authDir, 'antigravity-oauth-token'));
1968
+ checks.push(['antigravity oauth token available', hasAuth]);
1969
+ }
1800
1970
  if (process.env.WX_ENABLED === 'true' || process.env.WX_ENABLED === '1') {
1801
1971
  const accountsDir = process.env.WEIXIN_ACCOUNTS_DIR || path.join(APP_HOME, 'weixin', 'accounts');
1802
1972
  let hasAccounts = false;
@@ -0,0 +1,11 @@
1
+ import type { IEngineAdapter, EngineModel, EngineTurnRequest, EngineTurnExecution } from '../core/engine_spi.js';
2
+ import type { OpencodeAppClient } from './client.js';
3
+ export declare class OpencodeEngineAdapter implements IEngineAdapter {
4
+ private readonly client;
5
+ private readonly defaultModel?;
6
+ readonly id = "opencode";
7
+ readonly name = "OpenCode (SDK)";
8
+ constructor(client: OpencodeAppClient, defaultModel?: string | undefined);
9
+ listModels(): Promise<EngineModel[]>;
10
+ executeTurn(request: EngineTurnRequest): EngineTurnExecution;
11
+ }
@@ -0,0 +1,168 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { formatSdkError } from './client.js';
3
+ import { buildAttachmentPrompt } from '../telegram/media.js';
4
+ export class OpencodeEngineAdapter {
5
+ client;
6
+ defaultModel;
7
+ id = 'opencode';
8
+ name = 'OpenCode (SDK)';
9
+ constructor(client, defaultModel) {
10
+ this.client = client;
11
+ this.defaultModel = defaultModel;
12
+ }
13
+ async listModels() {
14
+ if (!this.client.isConnected())
15
+ return [];
16
+ try {
17
+ const response = await this.client.getClient().provider.list();
18
+ if (response.error || !response.data)
19
+ return [];
20
+ const { all, connected, default: defaultMap } = response.data;
21
+ const models = [];
22
+ for (const providerId of connected) {
23
+ const provider = all.find((p) => p.id === providerId);
24
+ if (!provider)
25
+ continue;
26
+ const defaultForProvider = defaultMap[providerId];
27
+ for (const m of Object.values(provider.models)) {
28
+ const modelFullId = `${providerId}/${m.id}`;
29
+ models.push({
30
+ id: modelFullId,
31
+ name: `${provider.name} - ${m.name}`,
32
+ isDefault: m.id === defaultForProvider,
33
+ });
34
+ }
35
+ }
36
+ return models;
37
+ }
38
+ catch {
39
+ return [];
40
+ }
41
+ }
42
+ executeTurn(request) {
43
+ const emitter = new EventEmitter();
44
+ let sessionId = request.threadId;
45
+ let cancelled = false;
46
+ let promptText = request.prompt;
47
+ if (request.stagedAttachments && request.stagedAttachments.length > 0 && !request.prompt.includes('Telegram attachments:')) {
48
+ promptText = buildAttachmentPrompt(request.prompt, request.stagedAttachments);
49
+ }
50
+ let finalResult = null;
51
+ let turnError = null;
52
+ const resultWaiters = [];
53
+ const resolveWaiters = (res) => {
54
+ while (resultWaiters.length > 0) {
55
+ resultWaiters.shift()(res);
56
+ }
57
+ };
58
+ const run = async () => {
59
+ try {
60
+ const sdk = this.client.getClient();
61
+ if (!sessionId) {
62
+ const createRes = await sdk.session.create({ directory: request.cwd });
63
+ if (createRes.error || !createRes.data) {
64
+ throw new Error(`Failed to create OpenCode session: ${formatSdkError(createRes.error)}`);
65
+ }
66
+ sessionId = createRes.data.id;
67
+ }
68
+ if (cancelled)
69
+ return;
70
+ let accumulatedResponse = '';
71
+ const onEvent = (ev) => {
72
+ if ('sessionId' in ev && ev.sessionId === sessionId) {
73
+ if (ev.kind === 'text') {
74
+ if (ev.delta) {
75
+ accumulatedResponse += ev.delta;
76
+ emitter.emit('delta', ev.delta);
77
+ }
78
+ }
79
+ else if (ev.kind === 'tool') {
80
+ emitter.emit('tool', {
81
+ name: ev.tool,
82
+ args: undefined,
83
+ output: undefined,
84
+ status: ev.status === 'running' ? 'running' : ev.status === 'error' ? 'failed' : 'completed',
85
+ });
86
+ }
87
+ else if (ev.kind === 'idle') {
88
+ cleanup();
89
+ finalResult = {
90
+ kind: 'result',
91
+ status: 'SUCCESS',
92
+ response: accumulatedResponse,
93
+ conversationId: sessionId,
94
+ };
95
+ emitter.emit('result', finalResult);
96
+ resolveWaiters(finalResult);
97
+ }
98
+ else if (ev.kind === 'error') {
99
+ cleanup();
100
+ turnError = new Error(ev.message);
101
+ finalResult = {
102
+ kind: 'result',
103
+ status: 'ERROR',
104
+ response: ev.message,
105
+ conversationId: sessionId,
106
+ };
107
+ emitter.emit('result', finalResult);
108
+ emitter.emit('error', turnError);
109
+ resolveWaiters(finalResult);
110
+ }
111
+ }
112
+ };
113
+ const cleanup = () => {
114
+ this.client.off('event', onEvent);
115
+ };
116
+ this.client.on('event', onEvent);
117
+ let modelParam;
118
+ const targetModel = request.model || this.defaultModel;
119
+ if (targetModel && targetModel.includes('/')) {
120
+ const [pId, ...mRest] = targetModel.split('/');
121
+ modelParam = { providerID: pId, modelID: mRest.join('/') };
122
+ }
123
+ const promptRes = await sdk.session.promptAsync({
124
+ sessionID: sessionId,
125
+ directory: request.cwd,
126
+ parts: [{ type: 'text', text: promptText }],
127
+ ...(modelParam ? { model: modelParam } : {}),
128
+ });
129
+ if (promptRes.error) {
130
+ cleanup();
131
+ turnError = new Error(formatSdkError(promptRes.error));
132
+ emitter.emit('error', turnError);
133
+ resolveWaiters(null);
134
+ }
135
+ else {
136
+ this.client.watchSessionUntilIdle(sessionId, request.cwd);
137
+ }
138
+ }
139
+ catch (err) {
140
+ turnError = err instanceof Error ? err : new Error(String(err));
141
+ emitter.emit('error', turnError);
142
+ resolveWaiters(null);
143
+ }
144
+ };
145
+ void run();
146
+ return {
147
+ turnId: sessionId ?? undefined,
148
+ cancel: () => {
149
+ cancelled = true;
150
+ if (sessionId) {
151
+ void this.client.getClient().session.abort({ sessionID: sessionId }).catch(() => { });
152
+ }
153
+ },
154
+ waitForResult: async () => {
155
+ if (finalResult)
156
+ return finalResult;
157
+ if (turnError)
158
+ return null;
159
+ return new Promise((resolve) => {
160
+ resultWaiters.push(resolve);
161
+ });
162
+ },
163
+ on: (event, listener) => {
164
+ emitter.on(event, listener);
165
+ },
166
+ };
167
+ }
168
+ }
@@ -1,4 +1,5 @@
1
1
  import type { AccessPresetValue, ActiveTurnMessageMode, AppLocale, CachedThread, ChatSessionSettings, CollaborationModeValue, GuidedPlanSessionRecord, GuidedPlanSessionState, PendingAttachmentBatchRecord, PendingAttachmentBatchStatus, PendingApprovalRecord, QueuedTurnInputRecord, QueuedTurnInputStatus, ReasoningEffortValue, ThreadBinding } from '../types.js';
2
+ import type { CumulativeTokenTotals, CumulativeTokenUsageRecord, TokenUsageDelta } from './token_usage.js';
2
3
  export interface ActiveTurnPreviewRecord {
3
4
  turnId: string;
4
5
  scopeId: string;
@@ -62,6 +63,12 @@ export declare class BridgeStore {
62
63
  setBinding(chatId: string, threadId: string, cwd: string | null): void;
63
64
  listBindings(): ThreadBinding[];
64
65
  clearBinding(chatId: string): void;
66
+ getWatchedThread(chatId: string): string | null;
67
+ setWatchedThread(chatId: string, threadId: string | null): void;
68
+ listWatchedThreads(): Array<{
69
+ scopeId: string;
70
+ threadId: string;
71
+ }>;
65
72
  getChatSettings(chatId: string): ChatSessionSettings | null;
66
73
  setChatSettings(chatId: string, model: string | null, reasoningEffort: ReasoningEffortValue | null, locale?: AppLocale | null): void;
67
74
  setChatLocale(chatId: string, locale: AppLocale): void;
@@ -69,6 +76,30 @@ export declare class BridgeStore {
69
76
  setChatCollaborationMode(chatId: string, collaborationMode: CollaborationModeValue | null): void;
70
77
  setChatServiceTier(chatId: string, serviceTier: string | null): void;
71
78
  setChatActiveTurnMessageMode(chatId: string, activeTurnMessageMode: ActiveTurnMessageMode | null): void;
79
+ getActiveBackend(chatId: string): string | null;
80
+ setActiveBackend(chatId: string, backendId: string | null): void;
81
+ getScopeBackendBinding(scopeId: string, backendId: string): {
82
+ threadId: string;
83
+ cwd: string | null;
84
+ model?: string | null;
85
+ reasoningEffort?: string | null;
86
+ activeTurnMessageMode?: string | null;
87
+ serviceTier?: string | null;
88
+ accessPreset?: string | null;
89
+ } | null;
90
+ setScopeBackendBinding(scopeId: string, backendId: string, threadId: string, cwd: string | null, settings?: {
91
+ model?: string | null;
92
+ reasoningEffort?: string | null;
93
+ activeTurnMessageMode?: string | null;
94
+ serviceTier?: string | null;
95
+ accessPreset?: string | null;
96
+ } | null): void;
97
+ listScopeBackendBindings(scopeId: string): Array<{
98
+ backendId: string;
99
+ threadId: string;
100
+ cwd: string | null;
101
+ updatedAt: number;
102
+ }>;
72
103
  findChatIdByThreadId(threadId: string): string | null;
73
104
  findAllChatIdsByThreadId(threadId: string): string[];
74
105
  countBindings(): number;
@@ -141,5 +172,15 @@ export declare class BridgeStore {
141
172
  getCodexAuthPoolStats(): CodexAuthPoolStats;
142
173
  setCodexAuthQuotaSnapshot(runtimeId: string, candidateName: string, accountId: string, quotaIdentityId: string, snapshot: Pick<CodexAuthQuotaSnapshotRecord, 'capturedAtMs' | 'planType' | 'primaryWindowDurationMins' | 'primaryRemainingPercent' | 'secondaryWindowDurationMins' | 'secondaryRemainingPercent'> & Partial<Pick<CodexAuthQuotaSnapshotRecord, 'primaryResetsAt' | 'secondaryResetsAt'>>): void;
143
174
  listCodexAuthQuotaSnapshots(quotaIdentityIds: string[]): CodexAuthQuotaSnapshotRecord[];
175
+ recordTokenUsage(delta: TokenUsageDelta, backendId?: string): void;
176
+ setBackendCumulativeTokenUsage(usage: {
177
+ inputTokens: number;
178
+ outputTokens: number;
179
+ cachedTokens?: number;
180
+ totalTokens?: number;
181
+ turnsCount?: number;
182
+ }, backendId: string): void;
183
+ getCumulativeTokenUsage(backendId?: string): CumulativeTokenTotals;
184
+ getAllBackendTokenUsages(): CumulativeTokenUsageRecord[];
144
185
  private ensureColumn;
145
186
  }