@foxden-app/foxclaw 0.10.0 → 0.11.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.
- package/.env.example +9 -8
- package/CHANGELOG.md +25 -0
- package/dist/antigravity/adapter.d.ts +2 -1
- package/dist/antigravity/adapter.js +180 -42
- package/dist/antigravity/auth.d.ts +5 -0
- package/dist/antigravity/auth.js +37 -0
- package/dist/antigravity/client.js +8 -2
- package/dist/antigravity/controller.d.ts +5 -0
- package/dist/antigravity/controller.js +113 -28
- package/dist/antigravity/events.d.ts +1 -0
- package/dist/antigravity/events.js +29 -0
- package/dist/antigravity/runtime.d.ts +2 -0
- package/dist/antigravity/subagents.d.ts +66 -0
- package/dist/antigravity/subagents.js +459 -0
- package/dist/config.d.ts +3 -1
- package/dist/config.js +36 -29
- package/dist/controller/controller.d.ts +4 -0
- package/dist/controller/controller.js +71 -0
- package/dist/core/engine_spi.d.ts +1 -0
- package/dist/core/orchestrator.d.ts +6 -1
- package/dist/core/orchestrator.js +291 -139
- package/dist/main.js +346 -551
- package/dist/opencode/adapter.js +1 -0
- package/dist/opencode/controller.js +2 -2
- package/dist/store/database.d.ts +3 -0
- package/dist/store/database.js +33 -0
- package/dist/telegram/api.d.ts +1 -0
- package/dist/telegram/api.js +60 -12
- package/dist/telegram/bot_home.js +5 -4
- package/dist/types.d.ts +2 -0
- package/dist/update.js +17 -1
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -9,7 +9,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
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
|
+
import { getAntigravityTelegramCommands, getTelegramCommands } from './i18n.js';
|
|
13
13
|
import { installBundledCodexSkills } from './codex_skills.js';
|
|
14
14
|
import { acquireProcessLock, LockHeldError } from './lock.js';
|
|
15
15
|
import { readRuntimeStatus, writeRuntimeStatus } from './runtime.js';
|
|
@@ -191,6 +191,9 @@ async function main() {
|
|
|
191
191
|
...(process.env.CODEX_CLI_BIN || resolveCommand('codex')
|
|
192
192
|
? { codexCliBin: process.env.CODEX_CLI_BIN || resolveCommand('codex') }
|
|
193
193
|
: {}),
|
|
194
|
+
...(process.env.AGY_CLI_BIN || resolveCommand('agy')
|
|
195
|
+
? { agyCliBin: process.env.AGY_CLI_BIN || resolveCommand('agy') }
|
|
196
|
+
: {}),
|
|
194
197
|
...(notificationFile ? { notificationFile } : {}),
|
|
195
198
|
};
|
|
196
199
|
const outcome = performSelfUpdate(options);
|
|
@@ -346,7 +349,8 @@ async function runSendVoiceCli() {
|
|
|
346
349
|
throw new Error('Telegram voice files must be 50MB or smaller.');
|
|
347
350
|
}
|
|
348
351
|
const inferredBotId = parsed.botId ?? inferTelegramBotId(process.env.CODEX_HOME) ?? inferTelegramBotId(config.codexHome);
|
|
349
|
-
const
|
|
352
|
+
const allTokens = [...config.codexBotTokens, ...config.antigravityBotTokens];
|
|
353
|
+
const { botId, botToken } = resolveTelegramVoiceTarget(allTokens, inferredBotId);
|
|
350
354
|
const { BridgeStore } = await import('./store/database.js');
|
|
351
355
|
const store = new BridgeStore(config.storePath);
|
|
352
356
|
let chatId = parsed.chatId;
|
|
@@ -391,7 +395,8 @@ async function runSendMediaCli() {
|
|
|
391
395
|
}
|
|
392
396
|
const plan = planTelegramOutboundMedia(filePath);
|
|
393
397
|
const inferredBotId = parsed.botId ?? inferTelegramBotId(process.env.CODEX_HOME) ?? inferTelegramBotId(config.codexHome);
|
|
394
|
-
const
|
|
398
|
+
const allTokens = [...config.codexBotTokens, ...config.antigravityBotTokens];
|
|
399
|
+
const { botId, botToken } = resolveTelegramVoiceTarget(allTokens, inferredBotId);
|
|
395
400
|
const { BridgeStore } = await import('./store/database.js');
|
|
396
401
|
const store = new BridgeStore(config.storePath);
|
|
397
402
|
let chatId = parsed.chatId;
|
|
@@ -523,6 +528,17 @@ function formatRuntimeStatusSummary(status) {
|
|
|
523
528
|
else if (status.codexHome) {
|
|
524
529
|
lines.push(`Codex home: ${status.codexHome}`);
|
|
525
530
|
}
|
|
531
|
+
const agyHomes = (status.bots ?? [])
|
|
532
|
+
.flatMap(bot => bot.antigravityHome ? [{ label: bot.username ? `@${bot.username}` : bot.id, home: bot.antigravityHome }] : []);
|
|
533
|
+
if (agyHomes.length > 1) {
|
|
534
|
+
lines.push('Antigravity homes:');
|
|
535
|
+
for (const { label, home } of agyHomes) {
|
|
536
|
+
lines.push(` ${label}: ${home}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
else if (agyHomes.length === 1) {
|
|
540
|
+
lines.push(`Antigravity home: ${agyHomes[0].home}`);
|
|
541
|
+
}
|
|
526
542
|
if (status.codexAppServer) {
|
|
527
543
|
lines.push(`App server: ${status.codexAppServer.running ? 'running' : 'stopped'}${status.codexAppServer.pid ? ` pid=${status.codexAppServer.pid}` : ''}${status.codexAppServer.port ? ` port=${status.codexAppServer.port}` : ''}`);
|
|
528
544
|
}
|
|
@@ -589,7 +605,7 @@ function truncateStatusLine(value, maxLength) {
|
|
|
589
605
|
return normalized.length <= maxLength ? normalized : `${normalized.slice(0, Math.max(0, maxLength - 1))}…`;
|
|
590
606
|
}
|
|
591
607
|
async function runServeCli() {
|
|
592
|
-
const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter }, { AuthCandidateMirror }, { CrossNodeAuthSync }, { OpencodeTelegramRuntime }, {
|
|
608
|
+
const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter }, { AuthCandidateMirror }, { CrossNodeAuthSync }, { OpencodeTelegramRuntime }, { UnifiedBridgeCore }, { AntigravityAppClient }, { AntigravityAuthManager, linkAntigravityAuthTokens },] = await Promise.all([
|
|
593
609
|
import('./channels/bridge_messaging_router.js'),
|
|
594
610
|
import('./channels/telegram/telegram_messaging_port.js'),
|
|
595
611
|
import('./channels/weixin/weixin_channel_adapter.js'),
|
|
@@ -605,7 +621,6 @@ async function runServeCli() {
|
|
|
605
621
|
import('./auth/mirror.js'),
|
|
606
622
|
import('./auth/cross_node_sync.js'),
|
|
607
623
|
import('./opencode/runtime.js'),
|
|
608
|
-
import('./antigravity/runtime.js'),
|
|
609
624
|
import('./antigravity/controller.js'),
|
|
610
625
|
import('./antigravity/client.js'),
|
|
611
626
|
import('./antigravity/auth.js'),
|
|
@@ -624,12 +639,20 @@ async function runServeCli() {
|
|
|
624
639
|
let activeAuthMirror = null;
|
|
625
640
|
let activeAuthSync = null;
|
|
626
641
|
let activeOpencodeRuntime = null;
|
|
627
|
-
const activeAntigravityRuntimes = [];
|
|
628
642
|
let sharedCodexApp = null;
|
|
629
643
|
let sharedAntigravityApp = null;
|
|
630
644
|
let sharedAntigravityAuth = null;
|
|
631
645
|
try {
|
|
632
646
|
store = new BridgeStore(config.storePath);
|
|
647
|
+
const selfUpdater = createSelfUpdateRuntime({
|
|
648
|
+
entryPoint,
|
|
649
|
+
nodePath: process.execPath,
|
|
650
|
+
version: readPackageVersion(),
|
|
651
|
+
statusPath: config.statusPath,
|
|
652
|
+
logPath: path.join(APP_HOME, 'logs', 'update.log'),
|
|
653
|
+
codexCliBin: config.codexCliBin,
|
|
654
|
+
agyCliBin: config.antigravityCliBin,
|
|
655
|
+
});
|
|
633
656
|
sharedAntigravityAuth = new AntigravityAuthManager(config.antigravityAuthDir, logger);
|
|
634
657
|
sharedAntigravityApp = new AntigravityAppClient(config.antigravityCliBin, logger);
|
|
635
658
|
if (config.codexCliBin) {
|
|
@@ -640,474 +663,226 @@ async function runServeCli() {
|
|
|
640
663
|
await activeOpencodeRuntime.start();
|
|
641
664
|
logger.info('opencode.bridge.started', activeOpencodeRuntime.getRuntimeStatus());
|
|
642
665
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
666
|
+
const totalBots = config.codexBotTokens.length + config.antigravityBotTokens.length;
|
|
667
|
+
const namespacedScopes = totalBots > 1;
|
|
668
|
+
const seeds = [];
|
|
669
|
+
const canonicalAuthDir = config.codexAuthDir ?? config.codexHome ?? path.join(os.homedir(), '.codex');
|
|
670
|
+
const specs = [
|
|
671
|
+
...config.codexBotTokens.map((token, i) => ({
|
|
672
|
+
token,
|
|
673
|
+
backend: 'codex',
|
|
674
|
+
isDefaultRuntimeForBackend: config.tgDefaultRuntimeBotToken
|
|
675
|
+
? config.tgDefaultRuntimeBotToken === token
|
|
676
|
+
: i === 0,
|
|
677
|
+
})),
|
|
678
|
+
...config.antigravityBotTokens.map((token, i) => ({
|
|
679
|
+
token,
|
|
680
|
+
backend: 'antigravity',
|
|
681
|
+
isDefaultRuntimeForBackend: config.antigravityDefaultRuntimeBotToken
|
|
649
682
|
? config.antigravityDefaultRuntimeBotToken === token
|
|
650
|
-
:
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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());
|
|
683
|
+
: i === 0,
|
|
684
|
+
})),
|
|
685
|
+
];
|
|
686
|
+
const identities = await Promise.all(specs.map(async (spec) => {
|
|
687
|
+
const bot = new TelegramGateway(spec.token, config.tgAllowedUserId, config.tgAllowedChatId, config.telegramPollIntervalMs, store, logger, namespacedScopes, spec.backend === 'antigravity' ? getAntigravityTelegramCommands : getTelegramCommands);
|
|
688
|
+
const id = await bot.initializeIdentity();
|
|
689
|
+
const username = await bot.resolveUsername();
|
|
690
|
+
return { ...spec, bot, id, username };
|
|
691
|
+
}));
|
|
692
|
+
for (const { token, backend, isDefaultRuntimeForBackend, bot, id, username } of identities) {
|
|
693
|
+
if (seeds.some((runtime) => runtime.id === id)) {
|
|
694
|
+
throw new Error(`Duplicate Telegram bot identity detected: ${id}`);
|
|
689
695
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
installBundledCodexSkills(packageRoot, home);
|
|
711
|
-
const runtimeConfig = {
|
|
712
|
-
...config,
|
|
713
|
-
tgBotToken: token,
|
|
714
|
-
tgBotTokens: [token],
|
|
715
|
-
tgScopeBotId: id,
|
|
716
|
-
codexAuthDir: sharedDefaultRuntime ? config.codexAuthDir : home,
|
|
717
|
-
codexHome: home,
|
|
718
|
-
codexAppAutolaunch: sharedDefaultRuntime ? config.codexAppAutolaunch : false,
|
|
719
|
-
codexAppServerStatePath: sharedDefaultRuntime
|
|
720
|
-
? config.codexAppServerStatePath
|
|
721
|
-
: path.join(APP_HOME, 'runtime', `codex-app-server-${id}.json`),
|
|
722
|
-
codexAppServerLogPath: sharedDefaultRuntime
|
|
723
|
-
? config.codexAppServerLogPath
|
|
724
|
-
: path.join(APP_HOME, 'logs', `codex-app-server-${id}.log`),
|
|
725
|
-
};
|
|
726
|
-
const childEnv = { CODEX_HOME: home };
|
|
727
|
-
const app = new CodexAppClient(runtimeConfig.codexCliBin, runtimeConfig.codexAppLaunchCmd, runtimeConfig.codexAppAutolaunch, runtimeConfig.codexAppServerStatePath, runtimeConfig.codexAppServerLogPath, logger, childEnv, [
|
|
696
|
+
// Codex environment
|
|
697
|
+
const isCodexDefault = backend === 'codex' && isDefaultRuntimeForBackend;
|
|
698
|
+
const codexHome = isCodexDefault
|
|
699
|
+
? (config.codexHome ?? path.join(os.homedir(), '.codex'))
|
|
700
|
+
: prepareTelegramBotHome(DEFAULT_CODEX_TELEGRAM_HOME, id, username, null);
|
|
701
|
+
const codexAuthDir = isCodexDefault ? (config.codexAuthDir ?? codexHome) : codexHome;
|
|
702
|
+
if (!isCodexDefault) {
|
|
703
|
+
fs.mkdirSync(codexHome, { recursive: true, mode: 0o700 });
|
|
704
|
+
}
|
|
705
|
+
installBundledCodexSkills(packageRoot, codexHome);
|
|
706
|
+
const codexAppAutolaunch = isCodexDefault ? config.codexAppAutolaunch : false;
|
|
707
|
+
const codexAppServerStatePath = isCodexDefault
|
|
708
|
+
? config.codexAppServerStatePath
|
|
709
|
+
: path.join(APP_HOME, 'runtime', `codex-app-server-${id}.json`);
|
|
710
|
+
const codexAppServerLogPath = isCodexDefault
|
|
711
|
+
? config.codexAppServerLogPath
|
|
712
|
+
: path.join(APP_HOME, 'logs', `codex-app-server-${id}.log`);
|
|
713
|
+
const codexApp = (isCodexDefault && sharedCodexApp)
|
|
714
|
+
? sharedCodexApp
|
|
715
|
+
: new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, codexAppAutolaunch, codexAppServerStatePath, codexAppServerLogPath, logger, { CODEX_HOME: codexHome }, [
|
|
728
716
|
...codexApiProviderOverrides,
|
|
729
|
-
...(
|
|
717
|
+
...(isCodexDefault ? [] : ['cli_auth_credentials_store="file"']),
|
|
730
718
|
]);
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
let
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
suppressBackgroundNotifications: () => true,
|
|
743
|
-
}),
|
|
744
|
-
})), logger, path.join(APP_HOME, 'runtime', 'auth-mirror.json'), {
|
|
745
|
-
onSynced: async (event) => {
|
|
746
|
-
await authSync?.publishCandidate(event.record.candidateName);
|
|
747
|
-
},
|
|
748
|
-
onRemoteImported: (event) => {
|
|
749
|
-
restoreImportedAuthCandidateState(store, event.record.candidateName, authRuntimeIds);
|
|
750
|
-
},
|
|
751
|
-
});
|
|
752
|
-
await mirror.initialize();
|
|
753
|
-
activeAuthMirror = mirror;
|
|
754
|
-
managedApps = seeds.map((runtime) => runtime.app);
|
|
755
|
-
const selfUpdater = createSelfUpdateRuntime({
|
|
756
|
-
entryPoint,
|
|
757
|
-
nodePath: process.execPath,
|
|
758
|
-
version: readPackageVersion(),
|
|
759
|
-
statusPath: config.statusPath,
|
|
760
|
-
logPath: path.join(APP_HOME, 'logs', 'update.log'),
|
|
761
|
-
codexCliBin: config.codexCliBin,
|
|
762
|
-
agyCliBin: config.antigravityCliBin,
|
|
763
|
-
});
|
|
764
|
-
const lastSelfUpdatePath = path.join(APP_HOME, 'runtime', 'last-self-update.json');
|
|
765
|
-
let lastSelfUpdate = readSelfUpdateStatus(lastSelfUpdatePath);
|
|
766
|
-
const runtimes = [];
|
|
767
|
-
const localAuthRefreshLease = createLocalAuthRefreshLease();
|
|
768
|
-
const writeAggregateStatus = (running = true) => {
|
|
769
|
-
const statuses = runtimes.map((runtime) => runtime.core.getRuntimeStatus());
|
|
770
|
-
const weixinStatus = activeWeixinCore?.getRuntimeStatus() ?? null;
|
|
771
|
-
const authProactiveRefresh = newestAuthProactiveRefreshStatus([...statuses, ...(weixinStatus ? [weixinStatus] : [])]);
|
|
772
|
-
const first = statuses[0] ?? null;
|
|
773
|
-
writeRuntimeStatus(config.statusPath, {
|
|
774
|
-
running,
|
|
775
|
-
connected: running
|
|
776
|
-
&& statuses.every((status) => status.connected)
|
|
777
|
-
&& (!weixinStatus || weixinStatus.connected),
|
|
778
|
-
userAgent: first?.userAgent ?? null,
|
|
779
|
-
codexHome: first?.codexHome ?? null,
|
|
780
|
-
...(first?.codexAppServer ? { codexAppServer: first.codexAppServer } : {}),
|
|
781
|
-
botUsername: first?.botUsername ?? null,
|
|
782
|
-
currentBindings: store.countBindings(),
|
|
783
|
-
pendingApprovals: store.countPendingApprovals(),
|
|
784
|
-
pendingUserInputs: store.countPendingUserInputs(),
|
|
785
|
-
queuedTurns: store.countQueuedTurnInputs(),
|
|
786
|
-
activeTurns: statuses.reduce((sum, status) => sum + status.activeTurns, 0)
|
|
787
|
-
+ (weixinStatus?.activeTurns ?? 0),
|
|
788
|
-
lastError: statuses.find((status) => status.lastError)?.lastError
|
|
789
|
-
?? weixinStatus?.lastError
|
|
790
|
-
?? null,
|
|
791
|
-
updatedAt: new Date().toISOString(),
|
|
792
|
-
channels: { telegram: running, weixin: running && config.wxEnabled },
|
|
793
|
-
bots: runtimes.map((runtime, index) => ({
|
|
794
|
-
id: runtime.id,
|
|
795
|
-
username: statuses[index]?.botUsername ?? runtime.bot.username,
|
|
796
|
-
connected: running && Boolean(statuses[index]?.connected),
|
|
797
|
-
activeTurns: running ? (statuses[index]?.activeTurns ?? 0) : 0,
|
|
798
|
-
runtimeKind: runtime.sharedDefaultRuntime ? 'default' : 'isolated',
|
|
799
|
-
codexHome: statuses[index]?.codexHome ?? runtime.home,
|
|
800
|
-
...(statuses[index]?.codexAppServer ? { codexAppServer: statuses[index].codexAppServer } : {}),
|
|
801
|
-
})),
|
|
802
|
-
...(weixinStatus ? {
|
|
803
|
-
weixinRuntime: {
|
|
804
|
-
connected: running && weixinStatus.connected,
|
|
805
|
-
activeTurns: running ? weixinStatus.activeTurns : 0,
|
|
806
|
-
...(weixinStatus.codexAppServer ? { codexAppServer: weixinStatus.codexAppServer } : {}),
|
|
807
|
-
},
|
|
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
|
-
} : {}),
|
|
823
|
-
authMirror: mirror.getStatus(),
|
|
824
|
-
authSync: authSync?.getStatus() ?? null,
|
|
825
|
-
authProactiveRefresh,
|
|
826
|
-
lastUpdate: lastSelfUpdate,
|
|
827
|
-
});
|
|
828
|
-
};
|
|
829
|
-
const authSyncLocalIdle = () => runtimes.every((runtime) => runtime.core.isIdleForServiceUpdate())
|
|
830
|
-
&& (!activeWeixinCore || activeWeixinCore.isIdleForServiceUpdate())
|
|
831
|
-
&& mirror.isIdle();
|
|
832
|
-
const clusterUpdateScheduler = createClusterUpdateScheduler({
|
|
833
|
-
selfUpdater,
|
|
834
|
-
canUpdate: () => authSyncLocalIdle()
|
|
835
|
-
&& (authSync ? authSync.isIdle() : localAuthRefreshLease.isIdle()),
|
|
836
|
-
currentVersion: readPackageVersion,
|
|
837
|
-
logger,
|
|
838
|
-
});
|
|
839
|
-
const coordinator = {
|
|
840
|
-
canSelfUpdate: () => authSyncLocalIdle()
|
|
841
|
-
&& (authSync ? authSync.isIdle() : localAuthRefreshLease.isIdle()),
|
|
842
|
-
authCandidateUpdated: (runtimeId, candidateName) => mirror.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined),
|
|
843
|
-
authCandidateDeleted: async (_runtimeId, candidateName, reason = null) => {
|
|
844
|
-
await mirror.deleteCandidate(candidateName);
|
|
845
|
-
await authSync?.publishCandidateDeletion(candidateName, reason);
|
|
846
|
-
},
|
|
847
|
-
recoverAuthCandidate: async (runtimeId, candidateName, options = {}) => {
|
|
848
|
-
const local = await mirror.recoverRuntimeCandidate(runtimeId, candidateName);
|
|
849
|
-
if (local)
|
|
850
|
-
return true;
|
|
851
|
-
if (options.crossNode === false)
|
|
852
|
-
return false;
|
|
853
|
-
const current = await mirror.readRuntimeCandidate(runtimeId, candidateName)
|
|
854
|
-
?? await mirror.readNewestCandidate(candidateName);
|
|
855
|
-
return await authSync?.requestRecovery(candidateName, {
|
|
856
|
-
accountId: current?.accountId ?? null,
|
|
857
|
-
quotaIdentityId: current?.quotaIdentityId ?? null,
|
|
858
|
-
lastRefreshMs: current?.lastRefreshMs ?? null,
|
|
859
|
-
}) ?? false;
|
|
860
|
-
},
|
|
861
|
-
acquireAuthRefreshLease: (reason) => authSync?.acquireRefreshLease(reason)
|
|
862
|
-
?? localAuthRefreshLease.acquire(reason),
|
|
863
|
-
releaseAuthRefreshLease: (leaseId) => authSync?.releaseRefreshLease(leaseId)
|
|
864
|
-
?? localAuthRefreshLease.release(leaseId),
|
|
865
|
-
getAuthSyncStatus: () => authSync?.getStatus() ?? null,
|
|
866
|
-
authSyncSafeAll: async () => {
|
|
867
|
-
const local = await mirror.syncAllRuntimeCandidates();
|
|
868
|
-
const remote = await authSync?.pushAll() ?? { sent: 0, skipped: 0 };
|
|
869
|
-
return {
|
|
870
|
-
localSynced: local.synced,
|
|
871
|
-
localSkipped: local.skipped,
|
|
872
|
-
sent: remote.sent,
|
|
873
|
-
skipped: remote.skipped,
|
|
874
|
-
};
|
|
875
|
-
},
|
|
876
|
-
authSyncPushAll: () => authSync?.pushAll() ?? Promise.resolve({ sent: 0, skipped: 0 }),
|
|
877
|
-
authSyncTest: () => authSync?.testPeers() ?? Promise.resolve({ sent: 0, replied: 0, missing: [] }),
|
|
878
|
-
authSyncAudit: () => authSync?.auditCluster() ?? Promise.resolve(null),
|
|
879
|
-
statusUpdated: () => writeAggregateStatus(),
|
|
880
|
-
getServiceStatus: async () => ({
|
|
881
|
-
currentVersion: readPackageVersion(),
|
|
882
|
-
bots: await Promise.all(runtimes.map(async (runtime) => {
|
|
883
|
-
const status = runtime.core.getRuntimeStatus();
|
|
884
|
-
return {
|
|
885
|
-
id: runtime.id,
|
|
886
|
-
username: status.botUsername ?? runtime.bot.username,
|
|
887
|
-
connected: status.connected,
|
|
888
|
-
activeTurns: status.activeTurns,
|
|
889
|
-
runtimeKind: runtime.sharedDefaultRuntime ? 'default' : 'isolated',
|
|
890
|
-
currentAuth: await runtime.core.getCurrentAuthLabel().catch(() => null),
|
|
891
|
-
codexHome: status.codexHome ?? runtime.home,
|
|
892
|
-
...(status.codexAppServer ? { codexAppServer: status.codexAppServer } : {}),
|
|
893
|
-
};
|
|
894
|
-
})),
|
|
895
|
-
...(activeWeixinCore ? {
|
|
896
|
-
weixinRuntime: {
|
|
897
|
-
connected: activeWeixinCore.getRuntimeStatus().connected,
|
|
898
|
-
activeTurns: activeWeixinCore.getRuntimeStatus().activeTurns,
|
|
899
|
-
codexAppServer: activeWeixinCore.getRuntimeStatus().codexAppServer,
|
|
900
|
-
},
|
|
901
|
-
} : {}),
|
|
902
|
-
authMirror: mirror.getStatus(),
|
|
903
|
-
authSync: authSync?.getStatus() ?? null,
|
|
904
|
-
authProactiveRefresh: newestAuthProactiveRefreshStatus([
|
|
905
|
-
...runtimes.map(runtime => runtime.core.getRuntimeStatus()),
|
|
906
|
-
...(activeWeixinCore ? [activeWeixinCore.getRuntimeStatus()] : []),
|
|
907
|
-
]),
|
|
908
|
-
lastUpdate: lastSelfUpdate,
|
|
909
|
-
}),
|
|
910
|
-
selfUpdateCompleted: (status) => {
|
|
911
|
-
lastSelfUpdate = status;
|
|
912
|
-
writeSelfUpdateStatus(lastSelfUpdatePath, status);
|
|
913
|
-
writeAggregateStatus();
|
|
914
|
-
},
|
|
915
|
-
};
|
|
916
|
-
for (const seed of seeds) {
|
|
917
|
-
const telegramMessaging = new TelegramMessagingPort(seed.bot);
|
|
918
|
-
const outbound = new BridgeMessagingRouter(telegramMessaging, null);
|
|
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
|
-
});
|
|
927
|
-
runtimes.push({ ...seed, core, telegram: new TelegramChannelAdapter(core) });
|
|
719
|
+
// Antigravity environment
|
|
720
|
+
const isAntigravityDefault = backend === 'antigravity' && isDefaultRuntimeForBackend;
|
|
721
|
+
let antigravityHome;
|
|
722
|
+
let antigravityAuthDir;
|
|
723
|
+
let antigravityApp;
|
|
724
|
+
let antigravityAuth;
|
|
725
|
+
if (isAntigravityDefault) {
|
|
726
|
+
antigravityHome = os.homedir();
|
|
727
|
+
antigravityAuthDir = config.antigravityAuthDir;
|
|
728
|
+
antigravityApp = sharedAntigravityApp;
|
|
729
|
+
antigravityAuth = sharedAntigravityAuth;
|
|
928
730
|
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
731
|
+
else {
|
|
732
|
+
antigravityHome = prepareTelegramBotHome(DEFAULT_ANTIGRAVITY_TELEGRAM_HOME, id, username, null);
|
|
733
|
+
antigravityAuthDir = path.join(antigravityHome, '.gemini', 'antigravity-cli');
|
|
734
|
+
fs.mkdirSync(antigravityAuthDir, { recursive: true, mode: 0o700 });
|
|
735
|
+
linkAntigravityAuthTokens(config.antigravityAuthDir, antigravityAuthDir);
|
|
736
|
+
antigravityApp = new AntigravityAppClient(config.antigravityCliBin, logger, { HOME: antigravityHome });
|
|
737
|
+
antigravityAuth = new AntigravityAuthManager(antigravityAuthDir, logger);
|
|
935
738
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
if (!authSyncLocalIdle()) {
|
|
952
|
-
return { ok: false, reason: 'runtime is not idle' };
|
|
953
|
-
}
|
|
954
|
-
const runtime = runtimes.find((entry) => entry.core.isIdleForServiceUpdate()) ?? runtimes[0] ?? null;
|
|
955
|
-
if (!runtime) {
|
|
956
|
-
return { ok: false, reason: 'no validation runtime is available' };
|
|
957
|
-
}
|
|
958
|
-
return runtime.core.validateExternalCodexAuthCandidate(candidateName, raw, expectedAccountId);
|
|
959
|
-
},
|
|
960
|
-
importCandidate: (candidateName, raw, source) => mirror.importExternalCandidate(candidateName, raw, source),
|
|
961
|
-
markCandidateState: (candidateName, state, expected) => markAuditedAuthCandidateState(store, mirror, candidateName, state, expected, authRuntimeIds),
|
|
962
|
-
deleteLocalCandidate: async (candidateName, source) => {
|
|
963
|
-
if (!authSyncLocalIdle()) {
|
|
964
|
-
return { ok: false, deleted: false, reason: 'runtime is not idle' };
|
|
965
|
-
}
|
|
966
|
-
await mirror.deleteCandidate(candidateName);
|
|
967
|
-
store.deleteCodexAuthCandidate(candidateName);
|
|
968
|
-
await Promise.all(runtimes.map(runtime => runtime.core.handleExternalCodexAuthCandidateDeleted(candidateName, source.reason ?? null)));
|
|
969
|
-
if (activeWeixinCore) {
|
|
970
|
-
await activeWeixinCore.handleExternalCodexAuthCandidateDeleted(candidateName, source.reason ?? null);
|
|
971
|
-
}
|
|
972
|
-
return { ok: true, deleted: true, reason: source.reason ?? null };
|
|
973
|
-
},
|
|
974
|
-
scheduleServiceUpdate: (source) => clusterUpdateScheduler.schedule(source),
|
|
975
|
-
isIdle: authSyncLocalIdle,
|
|
976
|
-
notify: createAuthSyncNotifier(store, authSyncTransportBot.bot, authNotificationAggregator, {
|
|
977
|
-
quietAuthPoolMode: () => config.authAutoDeleteNeedsRepair,
|
|
978
|
-
suppressBackgroundNotifications: () => true,
|
|
979
|
-
}),
|
|
980
|
-
});
|
|
981
|
-
await authSync.initialize();
|
|
982
|
-
activeAuthSync = authSync;
|
|
983
|
-
attachTelegramAuthSync(authSyncTransportBot.bot, authSync, config, logger);
|
|
984
|
-
authSync.start();
|
|
985
|
-
void publishPendingClusterUpdateBroadcast(authSync, logger);
|
|
739
|
+
const runtimeConfig = {
|
|
740
|
+
...config,
|
|
741
|
+
tgBotToken: token,
|
|
742
|
+
tgBotTokens: [token],
|
|
743
|
+
tgScopeBotId: id,
|
|
744
|
+
codexAuthDir,
|
|
745
|
+
codexHome,
|
|
746
|
+
antigravityHome,
|
|
747
|
+
antigravityAuthDir,
|
|
748
|
+
codexAppAutolaunch,
|
|
749
|
+
codexAppServerStatePath,
|
|
750
|
+
codexAppServerLogPath,
|
|
751
|
+
};
|
|
752
|
+
if (namespacedScopes && (isCodexDefault || isAntigravityDefault)) {
|
|
753
|
+
store.migrateTelegramUnnamespacedScope(id, config.tgAllowedUserId);
|
|
986
754
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
755
|
+
seeds.push({
|
|
756
|
+
id,
|
|
757
|
+
username,
|
|
758
|
+
home: codexHome,
|
|
759
|
+
authDir: codexAuthDir,
|
|
760
|
+
sharedDefaultRuntime: isCodexDefault || isAntigravityDefault,
|
|
761
|
+
config: runtimeConfig,
|
|
762
|
+
bot,
|
|
763
|
+
app: codexApp,
|
|
764
|
+
antigravityApp,
|
|
765
|
+
antigravityAuth,
|
|
766
|
+
antigravityHome,
|
|
767
|
+
antigravityAuthDir,
|
|
768
|
+
defaultBackend: backend,
|
|
993
769
|
});
|
|
994
|
-
for (const runtime of runtimes) {
|
|
995
|
-
await runtime.telegram.start();
|
|
996
|
-
}
|
|
997
|
-
if (weixinAdapter) {
|
|
998
|
-
await activeWeixinCore.startCodexApp();
|
|
999
|
-
await weixinAdapter.start();
|
|
1000
|
-
}
|
|
1001
|
-
writeAggregateStatus();
|
|
1002
|
-
logger.info('bridge.started', { bots: runtimes.map((runtime) => runtime.id) });
|
|
1003
|
-
const shutdown = async (signal) => {
|
|
1004
|
-
logger.info('bridge.shutting_down', { signal });
|
|
1005
|
-
await authNotificationAggregator.flushAll();
|
|
1006
|
-
authSync?.stop();
|
|
1007
|
-
mirror.stop();
|
|
1008
|
-
await weixinAdapter?.stop();
|
|
1009
|
-
await activeWeixinCore?.stop();
|
|
1010
|
-
await Promise.all(runtimes.map((runtime) => runtime.telegram.stop()));
|
|
1011
|
-
await activeOpencodeRuntime?.stop();
|
|
1012
|
-
await Promise.all(activeAntigravityRuntimes.map((runtime) => runtime.stop().catch(() => { })));
|
|
1013
|
-
writeAggregateStatus(false);
|
|
1014
|
-
const allManaged = [...managedApps, ...(sharedCodexApp ? [sharedCodexApp] : [])];
|
|
1015
|
-
await Promise.all(allManaged.map((app) => app.stop({ terminateServer: true }).catch((error) => {
|
|
1016
|
-
logger.warn('codex.app-server.stop_failed', { error: serializeError(error) });
|
|
1017
|
-
})));
|
|
1018
|
-
store?.close();
|
|
1019
|
-
processLock.release();
|
|
1020
|
-
process.exit(0);
|
|
1021
|
-
};
|
|
1022
|
-
process.on('SIGINT', () => void shutdown('SIGINT'));
|
|
1023
|
-
process.on('SIGTERM', () => void shutdown('SIGTERM'));
|
|
1024
|
-
return;
|
|
1025
770
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
:
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
771
|
+
let authSync = null;
|
|
772
|
+
const authRuntimeIds = seeds.map((runtime) => runtime.id);
|
|
773
|
+
const mirror = new AuthCandidateMirror(canonicalAuthDir, seeds.map((runtime) => ({
|
|
774
|
+
id: runtime.id,
|
|
775
|
+
label: runtime.bot.username ? `@${runtime.bot.username}` : runtime.id,
|
|
776
|
+
authDir: runtime.authDir,
|
|
777
|
+
validate: async (context) => validateRefreshedAuthCandidate(runtime, context.candidateName),
|
|
778
|
+
notify: createAuthMirrorNotifier(store, runtime.id, runtime.bot, authNotificationAggregator, {
|
|
779
|
+
quietAuthPoolMode: () => config.authAutoDeleteNeedsRepair,
|
|
780
|
+
suppressBackgroundNotifications: () => true,
|
|
781
|
+
}),
|
|
782
|
+
})), logger, path.join(APP_HOME, 'runtime', 'auth-mirror.json'), {
|
|
783
|
+
onSynced: async (event) => {
|
|
784
|
+
await authSync?.publishCandidate(event.record.candidateName);
|
|
785
|
+
},
|
|
786
|
+
onRemoteImported: (event) => {
|
|
787
|
+
restoreImportedAuthCandidateState(store, event.record.candidateName, authRuntimeIds);
|
|
788
|
+
},
|
|
1044
789
|
});
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
const
|
|
1049
|
-
|
|
790
|
+
await mirror.initialize();
|
|
791
|
+
activeAuthMirror = mirror;
|
|
792
|
+
managedApps = seeds.map((runtime) => runtime.app);
|
|
793
|
+
const lastSelfUpdatePath = path.join(APP_HOME, 'runtime', 'last-self-update.json');
|
|
794
|
+
let lastSelfUpdate = readSelfUpdateStatus(lastSelfUpdatePath);
|
|
795
|
+
const runtimes = [];
|
|
796
|
+
const localAuthRefreshLease = createLocalAuthRefreshLease();
|
|
797
|
+
const writeAggregateStatus = (running = true) => {
|
|
798
|
+
const statuses = runtimes.map((runtime) => runtime.core.getRuntimeStatus());
|
|
799
|
+
const weixinStatus = activeWeixinCore?.getRuntimeStatus() ?? null;
|
|
800
|
+
const authProactiveRefresh = newestAuthProactiveRefreshStatus([...statuses, ...(weixinStatus ? [weixinStatus] : [])]);
|
|
801
|
+
const first = statuses[0] ?? null;
|
|
1050
802
|
writeRuntimeStatus(config.statusPath, {
|
|
1051
803
|
running,
|
|
1052
|
-
connected: running
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
804
|
+
connected: running
|
|
805
|
+
&& statuses.every((status) => status.connected)
|
|
806
|
+
&& (!weixinStatus || weixinStatus.connected),
|
|
807
|
+
userAgent: first?.userAgent ?? null,
|
|
808
|
+
codexHome: first?.codexHome ?? null,
|
|
809
|
+
...(first?.codexAppServer ? { codexAppServer: first.codexAppServer } : {}),
|
|
810
|
+
botUsername: first?.botUsername ?? null,
|
|
811
|
+
currentBindings: store.countBindings(),
|
|
812
|
+
pendingApprovals: store.countPendingApprovals(),
|
|
813
|
+
pendingUserInputs: store.countPendingUserInputs(),
|
|
814
|
+
queuedTurns: store.countQueuedTurnInputs(),
|
|
815
|
+
activeTurns: statuses.reduce((sum, status) => sum + status.activeTurns, 0)
|
|
816
|
+
+ (weixinStatus?.activeTurns ?? 0),
|
|
817
|
+
lastError: statuses.find((status) => status.lastError)?.lastError
|
|
818
|
+
?? weixinStatus?.lastError
|
|
819
|
+
?? null,
|
|
1063
820
|
updatedAt: new Date().toISOString(),
|
|
1064
821
|
channels: { telegram: running, weixin: running && config.wxEnabled },
|
|
1065
|
-
|
|
1066
|
-
|
|
822
|
+
bots: runtimes.map((runtime, index) => ({
|
|
823
|
+
id: runtime.id,
|
|
824
|
+
username: statuses[index]?.botUsername ?? runtime.bot.username,
|
|
825
|
+
connected: running && Boolean(statuses[index]?.connected),
|
|
826
|
+
activeTurns: running ? (statuses[index]?.activeTurns ?? 0) : 0,
|
|
827
|
+
runtimeKind: runtime.sharedDefaultRuntime ? 'default' : 'isolated',
|
|
828
|
+
defaultBackend: runtime.defaultBackend,
|
|
829
|
+
codexHome: statuses[index]?.codexHome ?? runtime.home,
|
|
830
|
+
antigravityHome: runtime.antigravityHome,
|
|
831
|
+
...(statuses[index]?.codexAppServer ? { codexAppServer: statuses[index].codexAppServer } : {}),
|
|
832
|
+
})),
|
|
833
|
+
...(weixinStatus ? {
|
|
834
|
+
weixinRuntime: {
|
|
835
|
+
connected: running && weixinStatus.connected,
|
|
836
|
+
activeTurns: running ? weixinStatus.activeTurns : 0,
|
|
837
|
+
...(weixinStatus.codexAppServer ? { codexAppServer: weixinStatus.codexAppServer } : {}),
|
|
838
|
+
},
|
|
839
|
+
} : {}),
|
|
840
|
+
authMirror: mirror.getStatus(),
|
|
841
|
+
authSync: authSync?.getStatus() ?? null,
|
|
842
|
+
authProactiveRefresh,
|
|
843
|
+
lastUpdate: lastSelfUpdate,
|
|
1067
844
|
});
|
|
1068
845
|
};
|
|
1069
|
-
const
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
const singleClusterUpdateScheduler = createClusterUpdateScheduler({
|
|
846
|
+
const authSyncLocalIdle = () => runtimes.every((runtime) => runtime.core.isIdleForServiceUpdate())
|
|
847
|
+
&& (!activeWeixinCore || activeWeixinCore.isIdleForServiceUpdate())
|
|
848
|
+
&& mirror.isIdle();
|
|
849
|
+
const clusterUpdateScheduler = createClusterUpdateScheduler({
|
|
1074
850
|
selfUpdater,
|
|
1075
|
-
canUpdate: () =>
|
|
1076
|
-
&& (
|
|
851
|
+
canUpdate: () => authSyncLocalIdle()
|
|
852
|
+
&& (authSync ? authSync.isIdle() : localAuthRefreshLease.isIdle()),
|
|
1077
853
|
currentVersion: readPackageVersion,
|
|
1078
854
|
logger,
|
|
1079
855
|
});
|
|
1080
|
-
const
|
|
1081
|
-
canSelfUpdate: () =>
|
|
1082
|
-
&& (
|
|
1083
|
-
authCandidateUpdated: (runtimeId, candidateName) =>
|
|
856
|
+
const coordinator = {
|
|
857
|
+
canSelfUpdate: () => authSyncLocalIdle()
|
|
858
|
+
&& (authSync ? authSync.isIdle() : localAuthRefreshLease.isIdle()),
|
|
859
|
+
authCandidateUpdated: (runtimeId, candidateName) => mirror.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined),
|
|
1084
860
|
authCandidateDeleted: async (_runtimeId, candidateName, reason = null) => {
|
|
1085
|
-
await
|
|
1086
|
-
await
|
|
861
|
+
await mirror.deleteCandidate(candidateName);
|
|
862
|
+
await authSync?.publishCandidateDeletion(candidateName, reason);
|
|
1087
863
|
},
|
|
1088
864
|
recoverAuthCandidate: async (runtimeId, candidateName, options = {}) => {
|
|
1089
|
-
const local = await
|
|
865
|
+
const local = await mirror.recoverRuntimeCandidate(runtimeId, candidateName);
|
|
1090
866
|
if (local)
|
|
1091
867
|
return true;
|
|
1092
868
|
if (options.crossNode === false)
|
|
1093
869
|
return false;
|
|
1094
|
-
const current = await
|
|
1095
|
-
?? await
|
|
1096
|
-
|
|
1097
|
-
return await singleAuthSync?.requestRecovery(candidateName, {
|
|
870
|
+
const current = await mirror.readRuntimeCandidate(runtimeId, candidateName)
|
|
871
|
+
?? await mirror.readNewestCandidate(candidateName);
|
|
872
|
+
return await authSync?.requestRecovery(candidateName, {
|
|
1098
873
|
accountId: current?.accountId ?? null,
|
|
1099
874
|
quotaIdentityId: current?.quotaIdentityId ?? null,
|
|
1100
875
|
lastRefreshMs: current?.lastRefreshMs ?? null,
|
|
1101
876
|
}) ?? false;
|
|
1102
877
|
},
|
|
1103
|
-
acquireAuthRefreshLease: (reason) =>
|
|
1104
|
-
??
|
|
1105
|
-
releaseAuthRefreshLease: (leaseId) =>
|
|
1106
|
-
??
|
|
1107
|
-
getAuthSyncStatus: () =>
|
|
878
|
+
acquireAuthRefreshLease: (reason) => authSync?.acquireRefreshLease(reason)
|
|
879
|
+
?? localAuthRefreshLease.acquire(reason),
|
|
880
|
+
releaseAuthRefreshLease: (leaseId) => authSync?.releaseRefreshLease(leaseId)
|
|
881
|
+
?? localAuthRefreshLease.release(leaseId),
|
|
882
|
+
getAuthSyncStatus: () => authSync?.getStatus() ?? null,
|
|
1108
883
|
authSyncSafeAll: async () => {
|
|
1109
|
-
const local = await
|
|
1110
|
-
const remote = await
|
|
884
|
+
const local = await mirror.syncAllRuntimeCandidates();
|
|
885
|
+
const remote = await authSync?.pushAll() ?? { sent: 0, skipped: 0 };
|
|
1111
886
|
return {
|
|
1112
887
|
localSynced: local.synced,
|
|
1113
888
|
localSkipped: local.skipped,
|
|
@@ -1115,131 +890,148 @@ async function runServeCli() {
|
|
|
1115
890
|
skipped: remote.skipped,
|
|
1116
891
|
};
|
|
1117
892
|
},
|
|
1118
|
-
authSyncPushAll: () =>
|
|
1119
|
-
authSyncTest: () =>
|
|
1120
|
-
authSyncAudit: () =>
|
|
1121
|
-
statusUpdated: (
|
|
1122
|
-
|
|
1123
|
-
|
|
893
|
+
authSyncPushAll: () => authSync?.pushAll() ?? Promise.resolve({ sent: 0, skipped: 0 }),
|
|
894
|
+
authSyncTest: () => authSync?.testPeers() ?? Promise.resolve({ sent: 0, replied: 0, missing: [] }),
|
|
895
|
+
authSyncAudit: () => authSync?.auditCluster() ?? Promise.resolve(null),
|
|
896
|
+
statusUpdated: () => writeAggregateStatus(),
|
|
897
|
+
getServiceStatus: async () => ({
|
|
898
|
+
currentVersion: readPackageVersion(),
|
|
899
|
+
bots: await Promise.all(runtimes.map(async (runtime) => {
|
|
900
|
+
const status = runtime.core.getRuntimeStatus();
|
|
901
|
+
return {
|
|
902
|
+
id: runtime.id,
|
|
903
|
+
username: status.botUsername ?? runtime.bot.username,
|
|
904
|
+
connected: status.connected,
|
|
905
|
+
activeTurns: status.activeTurns,
|
|
906
|
+
runtimeKind: runtime.sharedDefaultRuntime ? 'default' : 'isolated',
|
|
907
|
+
currentAuth: await runtime.core.getCurrentAuthLabel().catch(() => null),
|
|
908
|
+
codexHome: status.codexHome ?? runtime.home,
|
|
909
|
+
...(status.codexAppServer ? { codexAppServer: status.codexAppServer } : {}),
|
|
910
|
+
};
|
|
911
|
+
})),
|
|
912
|
+
...(activeWeixinCore ? {
|
|
913
|
+
weixinRuntime: {
|
|
914
|
+
connected: activeWeixinCore.getRuntimeStatus().connected,
|
|
915
|
+
activeTurns: activeWeixinCore.getRuntimeStatus().activeTurns,
|
|
916
|
+
codexAppServer: activeWeixinCore.getRuntimeStatus().codexAppServer,
|
|
917
|
+
},
|
|
918
|
+
} : {}),
|
|
919
|
+
authMirror: mirror.getStatus(),
|
|
920
|
+
authSync: authSync?.getStatus() ?? null,
|
|
921
|
+
authProactiveRefresh: newestAuthProactiveRefreshStatus([
|
|
922
|
+
...runtimes.map(runtime => runtime.core.getRuntimeStatus()),
|
|
923
|
+
...(activeWeixinCore ? [activeWeixinCore.getRuntimeStatus()] : []),
|
|
924
|
+
]),
|
|
925
|
+
lastUpdate: lastSelfUpdate,
|
|
926
|
+
}),
|
|
927
|
+
selfUpdateCompleted: (status) => {
|
|
928
|
+
lastSelfUpdate = status;
|
|
929
|
+
writeSelfUpdateStatus(lastSelfUpdatePath, status);
|
|
930
|
+
writeAggregateStatus();
|
|
1124
931
|
},
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
onSynced: async (event) => {
|
|
1138
|
-
await singleAuthSync?.publishCandidate(event.record.candidateName);
|
|
1139
|
-
},
|
|
1140
|
-
onRemoteImported: (event) => {
|
|
1141
|
-
restoreImportedAuthCandidateState(store, event.record.candidateName, ['default']);
|
|
1142
|
-
},
|
|
932
|
+
};
|
|
933
|
+
for (const seed of seeds) {
|
|
934
|
+
const telegramMessaging = new TelegramMessagingPort(seed.bot);
|
|
935
|
+
const outbound = new BridgeMessagingRouter(telegramMessaging, null);
|
|
936
|
+
const codexCore = new BridgeSessionCore(seed.config, store, logger, seed.bot, seed.app, outbound, selfUpdater, coordinator, false);
|
|
937
|
+
const core = new UnifiedBridgeCore(seed.config, store, logger, seed.bot, seed.antigravityApp, seed.antigravityAuth, telegramMessaging, {
|
|
938
|
+
codexApp: seed.app,
|
|
939
|
+
codexCore,
|
|
940
|
+
antigravityApp: seed.antigravityApp,
|
|
941
|
+
antigravityAuth: seed.antigravityAuth,
|
|
942
|
+
defaultBackendId: seed.defaultBackend,
|
|
943
|
+
selfUpdater,
|
|
1143
944
|
});
|
|
1144
|
-
|
|
1145
|
-
activeAuthMirror = singleMirror;
|
|
945
|
+
runtimes.push({ ...seed, core, telegram: new TelegramChannelAdapter(core) });
|
|
1146
946
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
947
|
+
if (config.wxEnabled) {
|
|
948
|
+
const weixinApp = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger, null, codexApiProviderOverrides);
|
|
949
|
+
const outbound = new BridgeMessagingRouter(new TelegramMessagingPort(seeds[0].bot), new WeixinMessagingPort(store, (id) => loadWeixinAccount(config.weixinAccountsDir, id)));
|
|
950
|
+
activeWeixinCore = new BridgeSessionCore(config, store, logger, seeds[0].bot, weixinApp, outbound, selfUpdater, coordinator, false);
|
|
951
|
+
managedApps.push(weixinApp);
|
|
952
|
+
weixinAdapter = new WeixinChannelAdapter(activeWeixinCore, store, config, logger);
|
|
953
|
+
}
|
|
954
|
+
activeTelegramAdapters = runtimes.map((runtime) => runtime.telegram);
|
|
955
|
+
if (config.authSyncEnabled) {
|
|
956
|
+
const authSyncTransportBot = seeds[0];
|
|
957
|
+
const authSyncTransportLabel = authSyncTransportBot.bot.username
|
|
958
|
+
? `@${authSyncTransportBot.bot.username}`
|
|
959
|
+
: authSyncTransportBot.id;
|
|
960
|
+
authSync = new CrossNodeAuthSync(buildAuthSyncConfig(config, authSyncTransportLabel), logger, {
|
|
1159
961
|
send: async (peer, envelope) => {
|
|
1160
|
-
await bot.sendDocument(peer, `foxclaw-auth-sync-${Date.now()}.json`, Buffer.from(envelope, 'utf8'), 'FOXCLAW_AUTH_SYNC_V1');
|
|
962
|
+
await authSyncTransportBot.bot.sendDocument(peer, `foxclaw-auth-sync-${Date.now()}.json`, Buffer.from(envelope, 'utf8'), 'FOXCLAW_AUTH_SYNC_V1');
|
|
1161
963
|
},
|
|
1162
964
|
}, {
|
|
1163
|
-
readLocalCandidate: (candidateName) =>
|
|
1164
|
-
listLocalCandidates: () =>
|
|
1165
|
-
listLocalCandidateCopies: () =>
|
|
965
|
+
readLocalCandidate: (candidateName) => mirror.readNewestCandidate(candidateName),
|
|
966
|
+
listLocalCandidates: () => mirror.listNewestCandidates(),
|
|
967
|
+
listLocalCandidateCopies: () => mirror.listCandidateCopies(),
|
|
1166
968
|
validateCandidate: async (candidateName, raw, expectedAccountId) => {
|
|
1167
|
-
if (!
|
|
969
|
+
if (!authSyncLocalIdle()) {
|
|
1168
970
|
return { ok: false, reason: 'runtime is not idle' };
|
|
1169
971
|
}
|
|
1170
|
-
|
|
972
|
+
const runtime = runtimes.find((entry) => entry.core.isIdleForServiceUpdate()) ?? runtimes[0] ?? null;
|
|
973
|
+
if (!runtime) {
|
|
974
|
+
return { ok: false, reason: 'no validation runtime is available' };
|
|
975
|
+
}
|
|
976
|
+
return runtime.core.validateExternalCodexAuthCandidate(candidateName, raw, expectedAccountId);
|
|
1171
977
|
},
|
|
1172
|
-
importCandidate: (candidateName, raw, source) =>
|
|
1173
|
-
markCandidateState: (candidateName, state, expected) => markAuditedAuthCandidateState(store,
|
|
978
|
+
importCandidate: (candidateName, raw, source) => mirror.importExternalCandidate(candidateName, raw, source),
|
|
979
|
+
markCandidateState: (candidateName, state, expected) => markAuditedAuthCandidateState(store, mirror, candidateName, state, expected, authRuntimeIds),
|
|
1174
980
|
deleteLocalCandidate: async (candidateName, source) => {
|
|
1175
|
-
if (!
|
|
981
|
+
if (!authSyncLocalIdle()) {
|
|
1176
982
|
return { ok: false, deleted: false, reason: 'runtime is not idle' };
|
|
1177
983
|
}
|
|
1178
|
-
await
|
|
984
|
+
await mirror.deleteCandidate(candidateName);
|
|
1179
985
|
store.deleteCodexAuthCandidate(candidateName);
|
|
1180
|
-
await core.handleExternalCodexAuthCandidateDeleted(candidateName, source.reason ?? null);
|
|
986
|
+
await Promise.all(runtimes.map(runtime => runtime.core.handleExternalCodexAuthCandidateDeleted(candidateName, source.reason ?? null)));
|
|
987
|
+
if (activeWeixinCore) {
|
|
988
|
+
await activeWeixinCore.handleExternalCodexAuthCandidateDeleted(candidateName, source.reason ?? null);
|
|
989
|
+
}
|
|
1181
990
|
return { ok: true, deleted: true, reason: source.reason ?? null };
|
|
1182
991
|
},
|
|
1183
|
-
scheduleServiceUpdate: (source) =>
|
|
1184
|
-
isIdle:
|
|
1185
|
-
notify: createAuthSyncNotifier(store, bot, authNotificationAggregator, {
|
|
992
|
+
scheduleServiceUpdate: (source) => clusterUpdateScheduler.schedule(source),
|
|
993
|
+
isIdle: authSyncLocalIdle,
|
|
994
|
+
notify: createAuthSyncNotifier(store, authSyncTransportBot.bot, authNotificationAggregator, {
|
|
1186
995
|
quietAuthPoolMode: () => config.authAutoDeleteNeedsRepair,
|
|
1187
996
|
suppressBackgroundNotifications: () => true,
|
|
1188
997
|
}),
|
|
1189
998
|
});
|
|
1190
|
-
await
|
|
1191
|
-
activeAuthSync =
|
|
1192
|
-
attachTelegramAuthSync(bot,
|
|
1193
|
-
|
|
1194
|
-
void publishPendingClusterUpdateBroadcast(
|
|
1195
|
-
singleMirror.start();
|
|
1196
|
-
}
|
|
1197
|
-
const telegram = new TelegramChannelAdapter(core);
|
|
1198
|
-
managedApps = [app];
|
|
1199
|
-
activeTelegramAdapters = [telegram];
|
|
1200
|
-
if (config.wxEnabled) {
|
|
1201
|
-
weixinAdapter = new WeixinChannelAdapter(core, store, config, logger);
|
|
999
|
+
await authSync.initialize();
|
|
1000
|
+
activeAuthSync = authSync;
|
|
1001
|
+
attachTelegramAuthSync(authSyncTransportBot.bot, authSync, config, logger);
|
|
1002
|
+
authSync.start();
|
|
1003
|
+
void publishPendingClusterUpdateBroadcast(authSync, logger);
|
|
1202
1004
|
}
|
|
1005
|
+
mirror.start();
|
|
1203
1006
|
process.on('unhandledRejection', (error) => {
|
|
1204
1007
|
logger.error('process.unhandled_rejection', { error: serializeError(error) });
|
|
1205
1008
|
});
|
|
1206
1009
|
process.on('uncaughtException', (error) => {
|
|
1207
1010
|
logger.error('process.uncaught_exception', { error: serializeError(error) });
|
|
1208
1011
|
});
|
|
1209
|
-
|
|
1012
|
+
for (const runtime of runtimes) {
|
|
1013
|
+
await runtime.telegram.start();
|
|
1014
|
+
}
|
|
1210
1015
|
if (weixinAdapter) {
|
|
1016
|
+
await activeWeixinCore.startCodexApp();
|
|
1211
1017
|
await weixinAdapter.start();
|
|
1212
1018
|
}
|
|
1213
|
-
|
|
1214
|
-
logger.info('bridge.started',
|
|
1019
|
+
writeAggregateStatus();
|
|
1020
|
+
logger.info('bridge.started', { bots: runtimes.map((runtime) => runtime.id) });
|
|
1215
1021
|
const shutdown = async (signal) => {
|
|
1216
1022
|
logger.info('bridge.shutting_down', { signal });
|
|
1217
1023
|
await authNotificationAggregator.flushAll();
|
|
1218
|
-
|
|
1219
|
-
|
|
1024
|
+
authSync?.stop();
|
|
1025
|
+
mirror.stop();
|
|
1220
1026
|
await weixinAdapter?.stop();
|
|
1221
|
-
await
|
|
1027
|
+
await activeWeixinCore?.stop();
|
|
1028
|
+
await Promise.all(runtimes.map((runtime) => runtime.telegram.stop()));
|
|
1222
1029
|
await activeOpencodeRuntime?.stop();
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
connected: false,
|
|
1227
|
-
userAgent: app.getUserAgent(),
|
|
1228
|
-
codexHome: singleCodexHome,
|
|
1229
|
-
codexAppServer: app.getServerStatus(),
|
|
1230
|
-
botUsername: bot.username,
|
|
1231
|
-
currentBindings: 0,
|
|
1232
|
-
pendingApprovals: 0,
|
|
1233
|
-
pendingUserInputs: 0,
|
|
1234
|
-
queuedTurns: 0,
|
|
1235
|
-
activeTurns: 0,
|
|
1236
|
-
lastError: null,
|
|
1237
|
-
updatedAt: new Date().toISOString(),
|
|
1238
|
-
channels: { telegram: false, weixin: false },
|
|
1239
|
-
});
|
|
1240
|
-
await app.stop({ terminateServer: true }).catch((error) => {
|
|
1030
|
+
writeAggregateStatus(false);
|
|
1031
|
+
const allManaged = [...managedApps, ...(sharedCodexApp ? [sharedCodexApp] : [])];
|
|
1032
|
+
await Promise.all(allManaged.map((app) => app.stop({ terminateServer: true }).catch((error) => {
|
|
1241
1033
|
logger.warn('codex.app-server.stop_failed', { error: serializeError(error) });
|
|
1242
|
-
});
|
|
1034
|
+
})));
|
|
1243
1035
|
store?.close();
|
|
1244
1036
|
processLock.release();
|
|
1245
1037
|
process.exit(0);
|
|
@@ -1255,7 +1047,6 @@ async function runServeCli() {
|
|
|
1255
1047
|
await activeWeixinCore?.stop().catch(() => { });
|
|
1256
1048
|
await Promise.allSettled(activeTelegramAdapters.map((adapter) => adapter.stop()));
|
|
1257
1049
|
await activeOpencodeRuntime?.stop().catch(() => { });
|
|
1258
|
-
await Promise.allSettled(activeAntigravityRuntimes.map((runtime) => runtime.stop()));
|
|
1259
1050
|
await Promise.allSettled(managedApps.map((app) => app.stop({ terminateServer: true })));
|
|
1260
1051
|
store?.close();
|
|
1261
1052
|
processLock.release();
|
|
@@ -1716,15 +1507,22 @@ async function configureEnvInteractively(envPath, existed) {
|
|
|
1716
1507
|
const skipped = [];
|
|
1717
1508
|
const warnings = [];
|
|
1718
1509
|
Object.assign(updates, await maybeSaveProxyEnvFromShell(rl, envPath));
|
|
1719
|
-
const
|
|
1720
|
-
if (
|
|
1721
|
-
updates.
|
|
1722
|
-
if (
|
|
1723
|
-
warnings.push('One or more
|
|
1510
|
+
const codexTokens = sanitizeEnvInput(await rl.question('Codex Telegram bot token(s), comma-separated (CODEX_BOT_TOKENS): '));
|
|
1511
|
+
if (codexTokens) {
|
|
1512
|
+
updates.CODEX_BOT_TOKENS = codexTokens;
|
|
1513
|
+
if (codexTokens.split(',').map((token) => token.trim()).some((token) => !/^\d+:[A-Za-z0-9_-]+$/.test(token))) {
|
|
1514
|
+
warnings.push('One or more CODEX_BOT_TOKENS values do not look like standard Telegram bot tokens.');
|
|
1724
1515
|
}
|
|
1725
1516
|
}
|
|
1726
1517
|
else {
|
|
1727
|
-
skipped.push('
|
|
1518
|
+
skipped.push('CODEX_BOT_TOKENS');
|
|
1519
|
+
}
|
|
1520
|
+
const agyTokens = sanitizeEnvInput(await rl.question('Antigravity Telegram bot token(s), comma-separated (ANTIGRAVITY_BOT_TOKENS) [optional]: '));
|
|
1521
|
+
if (agyTokens) {
|
|
1522
|
+
updates.ANTIGRAVITY_BOT_TOKENS = agyTokens;
|
|
1523
|
+
if (agyTokens.split(',').map((token) => token.trim()).some((token) => !/^\d+:[A-Za-z0-9_-]+$/.test(token))) {
|
|
1524
|
+
warnings.push('One or more ANTIGRAVITY_BOT_TOKENS values do not look like standard Telegram bot tokens.');
|
|
1525
|
+
}
|
|
1728
1526
|
}
|
|
1729
1527
|
const userId = sanitizeEnvInput(await rl.question('Telegram numeric user ID (TG_ALLOWED_USER_ID): '));
|
|
1730
1528
|
if (userId) {
|
|
@@ -1935,32 +1733,29 @@ function stopService() {
|
|
|
1935
1733
|
}
|
|
1936
1734
|
function runDoctorChecks() {
|
|
1937
1735
|
const configuredCodexBin = process.env.CODEX_CLI_BIN;
|
|
1736
|
+
const codexTokens = [
|
|
1737
|
+
...(process.env.CODEX_BOT_TOKENS ?? '').split(','),
|
|
1738
|
+
process.env.CODEX_BOT_TOKEN ?? '',
|
|
1739
|
+
].map((value) => value.trim()).filter(Boolean);
|
|
1740
|
+
const antigravityTokens = [
|
|
1741
|
+
...(process.env.ANTIGRAVITY_BOT_TOKENS ?? '').split(','),
|
|
1742
|
+
process.env.ANTIGRAVITY_BOT_TOKEN ?? '',
|
|
1743
|
+
].map((value) => value.trim()).filter(Boolean);
|
|
1938
1744
|
const checks = [
|
|
1939
1745
|
['node >= 24', Number(process.versions.node.split('.')[0]) >= 24],
|
|
1940
1746
|
['codex cli available', hasConfiguredCommand(configuredCodexBin, 'codex')],
|
|
1941
|
-
['telegram bot token(s) configured',
|
|
1747
|
+
['telegram bot token(s) configured', codexTokens.length > 0 || antigravityTokens.length > 0],
|
|
1942
1748
|
['telegram allowed user configured', Boolean(process.env.TG_ALLOWED_USER_ID)],
|
|
1943
1749
|
];
|
|
1944
1750
|
if (process.env.OPENCODE_BOT_TOKEN?.trim()) {
|
|
1945
1751
|
const configuredOpencodeBin = process.env.OPENCODE_CLI_BIN;
|
|
1946
1752
|
checks.push(['opencode cli available', hasConfiguredCommand(configuredOpencodeBin, 'opencode')]);
|
|
1947
|
-
const
|
|
1948
|
-
|
|
1949
|
-
process.env.TG_BOT_TOKEN ?? '',
|
|
1950
|
-
].map((value) => value.trim()).filter(Boolean);
|
|
1951
|
-
checks.push(['OpenCode uses an independent Telegram bot', !codexTokens.includes(process.env.OPENCODE_BOT_TOKEN.trim())]);
|
|
1753
|
+
const allOtherTokens = [...codexTokens, ...antigravityTokens];
|
|
1754
|
+
checks.push(['OpenCode uses an independent Telegram bot', !allOtherTokens.includes(process.env.OPENCODE_BOT_TOKEN.trim())]);
|
|
1952
1755
|
}
|
|
1953
|
-
const antigravityTokens = [
|
|
1954
|
-
...(process.env.ANTIGRAVITY_BOT_TOKENS ?? '').split(','),
|
|
1955
|
-
process.env.ANTIGRAVITY_BOT_TOKEN ?? '',
|
|
1956
|
-
].map((value) => value.trim()).filter(Boolean);
|
|
1957
1756
|
if (antigravityTokens.length > 0) {
|
|
1958
1757
|
const configuredAgyBin = process.env.ANTIGRAVITY_CLI_BIN;
|
|
1959
1758
|
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
1759
|
const hasConflict = antigravityTokens.some((t) => codexTokens.includes(t));
|
|
1965
1760
|
checks.push(['Antigravity uses independent Telegram bot(s)', !hasConflict]);
|
|
1966
1761
|
const authDir = process.env.ANTIGRAVITY_AUTH_DIR || path.join(os.homedir(), '.gemini', 'antigravity-cli');
|