@foxden-app/foxclaw 0.5.77 → 0.6.3

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.
@@ -523,7 +523,11 @@ export class BridgeSessionCore {
523
523
  }
524
524
  lines.push(...codexUsageLines);
525
525
  lines.push(...codexLocalUsageLines);
526
- await this.sendRichInternalMessage(scopeId, '/status', lines.join('\n'));
526
+ lines.splice(5, 0, t(locale, 'status_codex_home', {
527
+ value: this.config.codexHome ?? path.join(os.homedir(), '.codex'),
528
+ }));
529
+ const messageId = await this.sendRichInternalMessage(scopeId, '/status', lines.join('\n'));
530
+ this.scheduleStalePanelDeletion(scopeId, messageId);
527
531
  return;
528
532
  }
529
533
  case 'account': {
@@ -4948,7 +4952,7 @@ export class BridgeSessionCore {
4948
4952
  await this.sendMessage(scopeId, message);
4949
4953
  return;
4950
4954
  }
4951
- if (action === 'audit' || action === 'check') {
4955
+ if (action === 'audit' || action === 'check' || action === 'safe') {
4952
4956
  if (!this.canRunGlobalAuthRefresh()) {
4953
4957
  await this.sendMessage(scopeId, t(locale, 'auth_cluster_audit_blocked_active'));
4954
4958
  return;
@@ -4959,10 +4963,10 @@ export class BridgeSessionCore {
4959
4963
  await this.sendMessage(scopeId, t(locale, 'auth_sync_disabled'));
4960
4964
  return;
4961
4965
  }
4962
- await this.sendRichInternalMessage(scopeId, '/auth sync audit', formatAuthClusterAuditResult(locale, outcome));
4966
+ await this.sendRichInternalMessage(scopeId, '/auth sync safe', formatAuthClusterAuditResult(locale, outcome));
4963
4967
  return;
4964
4968
  }
4965
- if (action === 'safe' || (action === 'push' && args[1]?.toLowerCase() === 'all')) {
4969
+ if (action === 'push' && args[1]?.toLowerCase() === 'all') {
4966
4970
  if (!this.canRunGlobalAuthRefresh()) {
4967
4971
  await this.sendMessage(scopeId, t(locale, 'auth_sync_push_blocked_active'));
4968
4972
  return;
@@ -5803,8 +5807,20 @@ export class BridgeSessionCore {
5803
5807
  if (record.messageId !== null) {
5804
5808
  await this.editAuthPanelMessage(event.scopeId, record.messageId, t(locale, 'auth_sync_safe_starting'), []);
5805
5809
  }
5806
- const result = await this.runAuthSafeSyncAll();
5807
- if (!result) {
5810
+ if (record.messageId !== null) {
5811
+ this.pauseStalePanelDeletion(event.scopeId, record.messageId);
5812
+ }
5813
+ let outcome;
5814
+ try {
5815
+ outcome = await this.runAuthClusterAudit();
5816
+ }
5817
+ catch (error) {
5818
+ if (record.messageId !== null) {
5819
+ await this.editAuthPanelMessage(event.scopeId, record.messageId, t(locale, 'auth_cluster_audit_failed', { error: formatUserError(error) }), authChoiceKeyboard(locale, record));
5820
+ }
5821
+ return;
5822
+ }
5823
+ if (!outcome) {
5808
5824
  if (record.messageId !== null) {
5809
5825
  await this.editAuthPanelMessage(event.scopeId, record.messageId, t(locale, 'auth_sync_disabled'), authChoiceKeyboard(locale, record));
5810
5826
  }
@@ -5816,12 +5832,7 @@ export class BridgeSessionCore {
5816
5832
  record.createdAt = Date.now();
5817
5833
  clampCodexAuthListOffset(record);
5818
5834
  if (record.messageId !== null) {
5819
- await this.editAuthPanelMessage(event.scopeId, record.messageId, `${t(locale, 'auth_sync_safe_done', {
5820
- localSynced: result.localSynced,
5821
- localSkipped: result.localSkipped,
5822
- sent: result.sent,
5823
- skipped: result.skipped,
5824
- })}\n\n${renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record)}`, authChoiceKeyboard(locale, record));
5835
+ await this.editAuthPanelMessage(event.scopeId, record.messageId, `${formatAuthClusterAuditResult(locale, outcome)}\n\n${renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record)}`, authChoiceKeyboard(locale, record));
5825
5836
  }
5826
5837
  return;
5827
5838
  }
@@ -10493,17 +10504,8 @@ function authChoiceKeyboard(locale, record) {
10493
10504
  if (record.searchTerm) {
10494
10505
  rows.push([{ text: t(locale, 'button_clear_filter'), callback_data: `auth:${record.localId}:clear_search` }]);
10495
10506
  }
10496
- rows.push([
10497
- { text: t(locale, 'button_permissions'), callback_data: 'nav:permissions' },
10498
- { text: t(locale, 'button_login_device'), callback_data: `auth:${record.localId}:login_device` },
10499
- ]);
10500
- rows.push([
10501
- { text: t(locale, 'button_auth_cluster_audit'), callback_data: `auth:${record.localId}:cluster_audit` },
10502
- ]);
10503
- rows.push([
10504
- { text: t(locale, 'button_auth_safe_sync'), callback_data: `auth:${record.localId}:safe_sync` },
10505
- { text: t(locale, 'button_auth_reload'), callback_data: `auth:${record.localId}:reload` },
10506
- ]);
10507
+ rows.push([{ text: t(locale, 'button_login_device'), callback_data: `auth:${record.localId}:login_device` }]);
10508
+ rows.push([{ text: t(locale, 'button_auth_safe_sync'), callback_data: `auth:${record.localId}:safe_sync` }]);
10507
10509
  return rows;
10508
10510
  }
10509
10511
  function formatCodexAuthCandidateDisplayName(name) {
@@ -542,7 +542,9 @@ export function normalizeRequestedEffort(value) {
542
542
  || normalized === 'low'
543
543
  || normalized === 'medium'
544
544
  || normalized === 'high'
545
- || normalized === 'xhigh') {
545
+ || normalized === 'xhigh'
546
+ || normalized === 'max'
547
+ || normalized === 'ultra') {
546
548
  return normalized;
547
549
  }
548
550
  return null;
package/dist/i18n.d.ts CHANGED
@@ -95,6 +95,7 @@ declare const MESSAGES: {
95
95
  readonly status_user_agent: "Codex/FoxClaw agent: {value}";
96
96
  readonly status_current_thread: "Current thread: {value}";
97
97
  readonly status_configured_model: "Configured model: {value}";
98
+ readonly status_codex_home: "Codex home: {value}";
98
99
  readonly status_configured_effort: "Configured effort: {value}";
99
100
  readonly status_fast: "Fast: {value}";
100
101
  readonly status_collaboration_mode: "Mode: {value}";
@@ -245,7 +246,7 @@ declare const MESSAGES: {
245
246
  readonly auth_sync_test_sent: "Auth sync test complete: sent {sent}, replies {replied}.";
246
247
  readonly auth_sync_test_missing: "Missing replies: {value}";
247
248
  readonly auth_sync_push_blocked_active: "Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.";
248
- readonly auth_sync_safe_starting: "Safely syncing auth across local bot runtimes and cross-node peers...";
249
+ readonly auth_sync_safe_starting: "Auditing every node, selecting the newest valid auth, and safely synchronizing the cluster...";
249
250
  readonly auth_sync_safe_done: "Safe auth sync complete: local synced {localSynced}, local skipped {localSkipped}; cross-node sent {sent}, skipped {skipped}.";
250
251
  readonly auth_cluster_audit_starting: "Asking all auth-sync peers to validate accounts, reconcile the newest usable credentials, and refresh credentials at least 8 days old...";
251
252
  readonly auth_cluster_audit_blocked_active: "Cluster auth check requires every local runtime, approval, input, login, and auth mirror write to be idle.";
@@ -282,7 +283,7 @@ declare const MESSAGES: {
282
283
  readonly auth_sync_trace_missing: "Usage: /auth sync trace <requestId>";
283
284
  readonly button_login_device: "🔑 Login";
284
285
  readonly button_auth_reload: "🔄 Reload auth";
285
- readonly button_auth_safe_sync: "🧷 Safe sync";
286
+ readonly button_auth_safe_sync: "🩺 Safe sync";
286
287
  readonly button_auth_cluster_audit: "🩺 Check all nodes and reconcile auth";
287
288
  readonly button_auth_refresh_all_confirm: "⚠️ Accept risk & refresh";
288
289
  readonly button_auth_repair_login: "🔑 Login repair";
@@ -394,7 +395,7 @@ declare const MESSAGES: {
394
395
  readonly effort_adjusted_model: "Effort {effort} is not supported by {model}, so it was adjusted.";
395
396
  readonly effort_change_blocked: "Cannot change reasoning effort while a turn is active. Use /interrupt or wait.";
396
397
  readonly effort_reset: "Configured effort reset to server default.";
397
- readonly usage_effort: "Usage: /effort <none|minimal|low|medium|high|xhigh|default>\nOr just use /models and tap buttons.";
398
+ readonly usage_effort: "Usage: /effort <none|minimal|low|medium|high|xhigh|max|ultra|default>\nOr just use /models and tap buttons.";
398
399
  readonly model_does_not_support_effort: "{model} does not support {effort}.\nSupported: {supported}";
399
400
  readonly effort_configured: "Configured effort: {effort}";
400
401
  readonly usage_fast: "Usage: /fast <on|off|toggle>";
@@ -818,6 +819,7 @@ declare const MESSAGES: {
818
819
  readonly status_user_agent: "Codex/FoxClaw 标识:{value}";
819
820
  readonly status_current_thread: "当前线程:{value}";
820
821
  readonly status_configured_model: "已配置模型:{value}";
822
+ readonly status_codex_home: "Codex Home:{value}";
821
823
  readonly status_configured_effort: "已配置推理强度:{value}";
822
824
  readonly status_fast: "Fast:{value}";
823
825
  readonly status_collaboration_mode: "模式:{value}";
@@ -968,7 +970,7 @@ declare const MESSAGES: {
968
970
  readonly auth_sync_test_sent: "auth sync 测试完成:已发送 {sent},收到回应 {replied}。";
969
971
  readonly auth_sync_test_missing: "未回应:{value}";
970
972
  readonly auth_sync_push_blocked_active: "当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。";
971
- readonly auth_sync_safe_starting: "正在安全同步本机多 bot runtime 和跨节点 auth...";
973
+ readonly auth_sync_safe_starting: "正在自检全部节点、选择最新有效 auth,并安全同步整个集群...";
972
974
  readonly auth_sync_safe_done: "安全 auth 同步完成:本机同步 {localSynced},本机跳过 {localSkipped};跨节点发送 {sent},跳过 {skipped}。";
973
975
  readonly auth_cluster_audit_starting: "正在通知所有 auth sync 节点逐账号自检,协商最新有效凭据,并由本节点刷新已满 8 天的 auth...";
974
976
  readonly auth_cluster_audit_blocked_active: "集群 auth 自检要求本机所有 runtime、审批、待输入、登录和 auth 镜像写入均为空闲。";
@@ -1005,7 +1007,7 @@ declare const MESSAGES: {
1005
1007
  readonly auth_sync_trace_missing: "用法:/auth sync trace <requestId>";
1006
1008
  readonly button_login_device: "🔑 设备登录";
1007
1009
  readonly button_auth_reload: "🔄 重载 auth";
1008
- readonly button_auth_safe_sync: "🧷 安全同步";
1010
+ readonly button_auth_safe_sync: "🩺 安全同步";
1009
1011
  readonly button_auth_cluster_audit: "🩺 全节点自检并同步";
1010
1012
  readonly button_auth_refresh_all_confirm: "⚠️ 接受风险并刷新";
1011
1013
  readonly button_auth_repair_login: "🔑 登录修复";
@@ -1117,7 +1119,7 @@ declare const MESSAGES: {
1117
1119
  readonly effort_adjusted_model: "{model} 不支持 {effort},所以已自动调整推理强度。";
1118
1120
  readonly effort_change_blocked: "当前有回复在进行中,暂时不能切换推理强度。请先等待,或使用 /interrupt。";
1119
1121
  readonly effort_reset: "推理强度已重置为服务端默认值。";
1120
- readonly usage_effort: "用法:/effort <none|minimal|low|medium|high|xhigh|default>\n或者直接使用 /models 点按钮。";
1122
+ readonly usage_effort: "用法:/effort <none|minimal|low|medium|high|xhigh|max|ultra|default>\n或者直接使用 /models 点按钮。";
1121
1123
  readonly model_does_not_support_effort: "{model} 不支持 {effort}。\n支持的强度:{supported}";
1122
1124
  readonly effort_configured: "已配置推理强度:{effort}";
1123
1125
  readonly usage_fast: "用法:/fast <on|off|toggle>";
@@ -1455,4 +1457,8 @@ export declare function getTelegramCommands(locale: AppLocale): Array<{
1455
1457
  command: string;
1456
1458
  description: string;
1457
1459
  }>;
1460
+ export declare function getOpencodeTelegramCommands(locale: AppLocale): Array<{
1461
+ command: string;
1462
+ description: string;
1463
+ }>;
1458
1464
  export {};
package/dist/i18n.js CHANGED
@@ -93,6 +93,7 @@ const MESSAGES = {
93
93
  status_user_agent: 'Codex/FoxClaw agent: {value}',
94
94
  status_current_thread: 'Current thread: {value}',
95
95
  status_configured_model: 'Configured model: {value}',
96
+ status_codex_home: 'Codex home: {value}',
96
97
  status_configured_effort: 'Configured effort: {value}',
97
98
  status_fast: 'Fast: {value}',
98
99
  status_collaboration_mode: 'Mode: {value}',
@@ -243,7 +244,7 @@ const MESSAGES = {
243
244
  auth_sync_test_sent: 'Auth sync test complete: sent {sent}, replies {replied}.',
244
245
  auth_sync_test_missing: 'Missing replies: {value}',
245
246
  auth_sync_push_blocked_active: 'Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.',
246
- auth_sync_safe_starting: 'Safely syncing auth across local bot runtimes and cross-node peers...',
247
+ auth_sync_safe_starting: 'Auditing every node, selecting the newest valid auth, and safely synchronizing the cluster...',
247
248
  auth_sync_safe_done: 'Safe auth sync complete: local synced {localSynced}, local skipped {localSkipped}; cross-node sent {sent}, skipped {skipped}.',
248
249
  auth_cluster_audit_starting: 'Asking all auth-sync peers to validate accounts, reconcile the newest usable credentials, and refresh credentials at least 8 days old...',
249
250
  auth_cluster_audit_blocked_active: 'Cluster auth check requires every local runtime, approval, input, login, and auth mirror write to be idle.',
@@ -280,7 +281,7 @@ const MESSAGES = {
280
281
  auth_sync_trace_missing: 'Usage: /auth sync trace <requestId>',
281
282
  button_login_device: '🔑 Login',
282
283
  button_auth_reload: '🔄 Reload auth',
283
- button_auth_safe_sync: '🧷 Safe sync',
284
+ button_auth_safe_sync: '🩺 Safe sync',
284
285
  button_auth_cluster_audit: '🩺 Check all nodes and reconcile auth',
285
286
  button_auth_refresh_all_confirm: '⚠️ Accept risk & refresh',
286
287
  button_auth_repair_login: '🔑 Login repair',
@@ -392,7 +393,7 @@ const MESSAGES = {
392
393
  effort_adjusted_model: 'Effort {effort} is not supported by {model}, so it was adjusted.',
393
394
  effort_change_blocked: 'Cannot change reasoning effort while a turn is active. Use /interrupt or wait.',
394
395
  effort_reset: 'Configured effort reset to server default.',
395
- usage_effort: 'Usage: /effort <none|minimal|low|medium|high|xhigh|default>\nOr just use /models and tap buttons.',
396
+ usage_effort: 'Usage: /effort <none|minimal|low|medium|high|xhigh|max|ultra|default>\nOr just use /models and tap buttons.',
396
397
  model_does_not_support_effort: '{model} does not support {effort}.\nSupported: {supported}',
397
398
  effort_configured: 'Configured effort: {effort}',
398
399
  usage_fast: 'Usage: /fast <on|off|toggle>',
@@ -816,6 +817,7 @@ const MESSAGES = {
816
817
  status_user_agent: 'Codex/FoxClaw 标识:{value}',
817
818
  status_current_thread: '当前线程:{value}',
818
819
  status_configured_model: '已配置模型:{value}',
820
+ status_codex_home: 'Codex Home:{value}',
819
821
  status_configured_effort: '已配置推理强度:{value}',
820
822
  status_fast: 'Fast:{value}',
821
823
  status_collaboration_mode: '模式:{value}',
@@ -966,7 +968,7 @@ const MESSAGES = {
966
968
  auth_sync_test_sent: 'auth sync 测试完成:已发送 {sent},收到回应 {replied}。',
967
969
  auth_sync_test_missing: '未回应:{value}',
968
970
  auth_sync_push_blocked_active: '当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。',
969
- auth_sync_safe_starting: '正在安全同步本机多 bot runtime 和跨节点 auth...',
971
+ auth_sync_safe_starting: '正在自检全部节点、选择最新有效 auth,并安全同步整个集群...',
970
972
  auth_sync_safe_done: '安全 auth 同步完成:本机同步 {localSynced},本机跳过 {localSkipped};跨节点发送 {sent},跳过 {skipped}。',
971
973
  auth_cluster_audit_starting: '正在通知所有 auth sync 节点逐账号自检,协商最新有效凭据,并由本节点刷新已满 8 天的 auth...',
972
974
  auth_cluster_audit_blocked_active: '集群 auth 自检要求本机所有 runtime、审批、待输入、登录和 auth 镜像写入均为空闲。',
@@ -1003,7 +1005,7 @@ const MESSAGES = {
1003
1005
  auth_sync_trace_missing: '用法:/auth sync trace <requestId>',
1004
1006
  button_login_device: '🔑 设备登录',
1005
1007
  button_auth_reload: '🔄 重载 auth',
1006
- button_auth_safe_sync: '🧷 安全同步',
1008
+ button_auth_safe_sync: '🩺 安全同步',
1007
1009
  button_auth_cluster_audit: '🩺 全节点自检并同步',
1008
1010
  button_auth_refresh_all_confirm: '⚠️ 接受风险并刷新',
1009
1011
  button_auth_repair_login: '🔑 登录修复',
@@ -1115,7 +1117,7 @@ const MESSAGES = {
1115
1117
  effort_adjusted_model: '{model} 不支持 {effort},所以已自动调整推理强度。',
1116
1118
  effort_change_blocked: '当前有回复在进行中,暂时不能切换推理强度。请先等待,或使用 /interrupt。',
1117
1119
  effort_reset: '推理强度已重置为服务端默认值。',
1118
- usage_effort: '用法:/effort <none|minimal|low|medium|high|xhigh|default>\n或者直接使用 /models 点按钮。',
1120
+ usage_effort: '用法:/effort <none|minimal|low|medium|high|xhigh|max|ultra|default>\n或者直接使用 /models 点按钮。',
1119
1121
  model_does_not_support_effort: '{model} 不支持 {effort}。\n支持的强度:{supported}',
1120
1122
  effort_configured: '已配置推理强度:{effort}',
1121
1123
  usage_fast: '用法:/fast <on|off|toggle>',
@@ -1505,3 +1507,40 @@ export function getTelegramCommands(locale) {
1505
1507
  { command: 'interrupt', description: t(locale, 'cmd_desc_interrupt') },
1506
1508
  ];
1507
1509
  }
1510
+ export function getOpencodeTelegramCommands(locale) {
1511
+ const descriptions = locale === 'zh' ? {
1512
+ help: '查看命令', setup: 'OpenCode 设置', status: '桥接状态', update: '请改用 Codex Bot', threads: '最近会话', open: '打开会话',
1513
+ watch: '观察会话', unwatch: '停止观察', new: '新建会话', models: '模型列表', model: '选择模型',
1514
+ effort: '推理档位', fast: 'Fast 能力说明', mode: 'Agent 或 Plan', plan: '下一轮 Plan', agent: '选择 Agent', permissions: '权限设置',
1515
+ active: '运行中新消息', steer: '引导当前回复', queue: '排队下一轮', takeover: '中断并接管',
1516
+ history: '最近消息', files: '查找文件', fork: 'Fork 会话', undo: '原生回退', redo: '原生重做', rename: '重命名会话',
1517
+ review: '原生代码审查', archive: '归档当前会话', unarchive: '恢复归档会话', rich: '富文本测试', voice: '朗读文本或发送语音',
1518
+ diff: '会话文件变更', where: '当前会话信息', reveal: '当前会话信息', compact: '压缩上下文', loaded: '已加载会话', skills: 'OpenCode Skills',
1519
+ mcp: 'MCP 状态', apps: 'MCP 应用', provider: '模型 Provider', auth: 'Provider 认证状态', auth_reload: '无 OpenCode 等价 API',
1520
+ account: '无 OpenCode 账户 API', quota: '无 OpenCode 配额 API', login_device: '请用终端 opencode auth', goal: '无 OpenCode Goal 原语', remote: 'Codex Remote 专用',
1521
+ plugins: 'OpenCode Plugins', hooks: 'Plugin hooks', features: '实验能力', config: '配置摘要', approve: '待审批', answer: '待回答', interrupt: '中断回复',
1522
+ } : {
1523
+ help: 'Show commands', setup: 'OpenCode settings', status: 'Bridge status', update: 'Use the Codex bot', threads: 'Recent sessions', open: 'Open a session',
1524
+ watch: 'Watch a session', unwatch: 'Stop watching', new: 'New session', models: 'Model list', model: 'Select model',
1525
+ effort: 'Reasoning variant', fast: 'Fast capability note', mode: 'Agent or Plan', plan: 'Plan next turn', agent: 'Select agent', permissions: 'Access settings',
1526
+ active: 'Active-turn messages', steer: 'Steer active turn', queue: 'Queue next turn', takeover: 'Interrupt and take over',
1527
+ history: 'Recent messages', files: 'Find files', fork: 'Fork session', undo: 'Native undo', redo: 'Native redo', rename: 'Rename session',
1528
+ review: 'Native code review', archive: 'Archive current session', unarchive: 'Restore archived session', rich: 'Rich-text test', voice: 'Read text or send voice',
1529
+ diff: 'Session file changes', where: 'Current session', reveal: 'Current session', compact: 'Compact context', loaded: 'Loaded sessions', skills: 'OpenCode skills',
1530
+ mcp: 'MCP status', apps: 'MCP apps', provider: 'Model providers', auth: 'Provider auth status', auth_reload: 'No OpenCode equivalent',
1531
+ account: 'No OpenCode account API', quota: 'No OpenCode quota API', login_device: 'Use opencode auth terminal', goal: 'No OpenCode Goal primitive', remote: 'Codex Remote only',
1532
+ plugins: 'OpenCode plugins', hooks: 'Plugin hooks', features: 'Experimental capabilities', config: 'Config summary', approve: 'Pending approvals', answer: 'Pending questions', interrupt: 'Stop active turn',
1533
+ };
1534
+ const localized = descriptions;
1535
+ const codexMenu = getTelegramCommands(locale).map((entry) => ({
1536
+ command: entry.command,
1537
+ description: localized[entry.command] ?? entry.description,
1538
+ }));
1539
+ const shared = new Set(codexMenu.map((entry) => entry.command));
1540
+ return [
1541
+ ...codexMenu,
1542
+ ...Object.entries(localized)
1543
+ .filter(([command]) => !shared.has(command))
1544
+ .map(([command, description]) => ({ command, description })),
1545
+ ];
1546
+ }
package/dist/main.js CHANGED
@@ -552,7 +552,7 @@ function truncateStatusLine(value, maxLength) {
552
552
  return normalized.length <= maxLength ? normalized : `${normalized.slice(0, Math.max(0, maxLength - 1))}…`;
553
553
  }
554
554
  async function runServeCli() {
555
- const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter }, { AuthCandidateMirror }, { CrossNodeAuthSync },] = await Promise.all([
555
+ const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter }, { AuthCandidateMirror }, { CrossNodeAuthSync }, { OpencodeTelegramRuntime },] = await Promise.all([
556
556
  import('./channels/bridge_messaging_router.js'),
557
557
  import('./channels/telegram/telegram_messaging_port.js'),
558
558
  import('./channels/weixin/weixin_channel_adapter.js'),
@@ -567,6 +567,7 @@ async function runServeCli() {
567
567
  import('./channels/telegram/telegram_channel_adapter.js'),
568
568
  import('./auth/mirror.js'),
569
569
  import('./auth/cross_node_sync.js'),
570
+ import('./opencode/runtime.js'),
570
571
  ]);
571
572
  const config = loadConfig();
572
573
  const logger = new Logger(config.logLevel, config.logPath);
@@ -581,8 +582,14 @@ async function runServeCli() {
581
582
  let managedApps = [];
582
583
  let activeAuthMirror = null;
583
584
  let activeAuthSync = null;
585
+ let activeOpencodeRuntime = null;
584
586
  try {
585
587
  store = new BridgeStore(config.storePath);
588
+ if (config.opencodeBotToken) {
589
+ activeOpencodeRuntime = new OpencodeTelegramRuntime(config, store, logger);
590
+ await activeOpencodeRuntime.start();
591
+ logger.info('opencode.bridge.started', activeOpencodeRuntime.getRuntimeStatus());
592
+ }
586
593
  if (config.tgMultiBotMode) {
587
594
  const seeds = [];
588
595
  const canonicalAuthDir = config.codexAuthDir ?? config.codexHome ?? path.join(os.homedir(), '.codex');
@@ -881,6 +888,7 @@ async function runServeCli() {
881
888
  await weixinAdapter?.stop();
882
889
  await activeWeixinCore?.stop();
883
890
  await Promise.all(runtimes.map((runtime) => runtime.telegram.stop()));
891
+ await activeOpencodeRuntime?.stop();
884
892
  writeAggregateStatus(false);
885
893
  await Promise.all(managedApps.map((app) => app.stop({ terminateServer: true }).catch((error) => {
886
894
  logger.warn('codex.app-server.stop_failed', { error: serializeError(error) });
@@ -1060,6 +1068,7 @@ async function runServeCli() {
1060
1068
  singleMirror?.stop();
1061
1069
  await weixinAdapter?.stop();
1062
1070
  await telegram.stop();
1071
+ await activeOpencodeRuntime?.stop();
1063
1072
  writeRuntimeStatus(config.statusPath, {
1064
1073
  running: false,
1065
1074
  connected: false,
@@ -1093,6 +1102,7 @@ async function runServeCli() {
1093
1102
  await weixinAdapter?.stop().catch(() => { });
1094
1103
  await activeWeixinCore?.stop().catch(() => { });
1095
1104
  await Promise.allSettled(activeTelegramAdapters.map((adapter) => adapter.stop()));
1105
+ await activeOpencodeRuntime?.stop().catch(() => { });
1096
1106
  await Promise.allSettled(managedApps.map((app) => app.stop({ terminateServer: true })));
1097
1107
  store?.close();
1098
1108
  processLock.release();
@@ -1778,6 +1788,15 @@ function runDoctorChecks() {
1778
1788
  ['telegram bot token(s) configured', Boolean(process.env.TG_BOT_TOKENS?.trim() || process.env.TG_BOT_TOKEN?.trim())],
1779
1789
  ['telegram allowed user configured', Boolean(process.env.TG_ALLOWED_USER_ID)],
1780
1790
  ];
1791
+ if (process.env.OPENCODE_BOT_TOKEN?.trim()) {
1792
+ const configuredOpencodeBin = process.env.OPENCODE_CLI_BIN;
1793
+ checks.push(['opencode cli available', hasConfiguredCodexBin(configuredOpencodeBin) || hasCommand('opencode')]);
1794
+ const codexTokens = [
1795
+ ...(process.env.TG_BOT_TOKENS ?? '').split(','),
1796
+ process.env.TG_BOT_TOKEN ?? '',
1797
+ ].map((value) => value.trim()).filter(Boolean);
1798
+ checks.push(['OpenCode uses an independent Telegram bot', !codexTokens.includes(process.env.OPENCODE_BOT_TOKEN.trim())]);
1799
+ }
1781
1800
  if (process.env.WX_ENABLED === 'true' || process.env.WX_ENABLED === '1') {
1782
1801
  const accountsDir = process.env.WEIXIN_ACCOUNTS_DIR || path.join(APP_HOME, 'weixin', 'accounts');
1783
1802
  let hasAccounts = false;
@@ -0,0 +1,66 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { type OpencodeClient } from '@opencode-ai/sdk/v2';
3
+ import type { Logger } from '../logger.js';
4
+ import { type OpencodeBridgeEvent } from './events.js';
5
+ export interface OpencodeServerRuntimeStatus {
6
+ pid: number | null;
7
+ port: number | null;
8
+ running: boolean;
9
+ connected: boolean;
10
+ url: string | null;
11
+ version: string | null;
12
+ managed: boolean;
13
+ }
14
+ export interface OpencodeAppClientEvents {
15
+ event: [event: OpencodeBridgeEvent];
16
+ connected: [];
17
+ disconnected: [detail: {
18
+ source: string;
19
+ message?: string;
20
+ }];
21
+ }
22
+ /** Owns one password-protected localhost `opencode serve` process. */
23
+ export declare class OpencodeAppClient extends EventEmitter<OpencodeAppClientEvents> {
24
+ private readonly cliBin;
25
+ private readonly configuredPassword;
26
+ private readonly statePath;
27
+ private readonly logPath;
28
+ private readonly logger;
29
+ private readonly childEnv;
30
+ private readonly normalizer;
31
+ private client;
32
+ private child;
33
+ private serverState;
34
+ private connected;
35
+ private sseAbort;
36
+ private sseLoop;
37
+ private readonly pollers;
38
+ private desiredRunning;
39
+ private starting;
40
+ private reconnectTimer;
41
+ constructor(cliBin: string, configuredPassword: string | null, statePath: string, logPath: string, logger: Logger, childEnv?: NodeJS.ProcessEnv | null);
42
+ getClient(): OpencodeClient;
43
+ isConnected(): boolean;
44
+ getServerStatus(): OpencodeServerRuntimeStatus;
45
+ start(): Promise<void>;
46
+ stop(options?: {
47
+ terminateServer?: boolean;
48
+ }): Promise<void>;
49
+ restart(): Promise<void>;
50
+ watchSessionUntilIdle(sessionId: string, directory: string): void;
51
+ recoverPendingRequests(directories: readonly string[]): Promise<void>;
52
+ private pollSession;
53
+ private attachPersistedServer;
54
+ private spawnServer;
55
+ private adoptServer;
56
+ private startSseLoop;
57
+ private runSseLoop;
58
+ private acceptEvent;
59
+ private recoverPendingForDirectory;
60
+ private handleDisconnect;
61
+ private scheduleReconnect;
62
+ private writeState;
63
+ private readState;
64
+ private clearStateForPid;
65
+ }
66
+ export declare function formatSdkError(error: unknown): string;