@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
@@ -1,12 +1,19 @@
1
1
  import type { BridgeSessionCore } from '../../controller/controller.js';
2
+ export interface ITelegramBridgeCore {
3
+ registerTelegramInboundHandlers(): void;
4
+ startCodexApp(): Promise<void>;
5
+ startTelegramPolling(): Promise<void>;
6
+ stop(): Promise<void>;
7
+ }
2
8
  /**
3
- * Telegram channel: inbound subscription + transport startup ordering for {@link BridgeSessionCore}.
9
+ * Telegram channel: inbound subscription + transport startup ordering for {@link BridgeSessionCore}
10
+ * or {@link UnifiedBridgeCore}.
4
11
  * Additional channels (e.g. Weixin) can compose the same core with their own adapters.
5
12
  */
6
13
  export declare class TelegramChannelAdapter {
7
14
  private readonly core;
8
- constructor(core: BridgeSessionCore);
15
+ constructor(core: BridgeSessionCore | ITelegramBridgeCore);
9
16
  start(): Promise<void>;
10
17
  stop(): Promise<void>;
11
- get sessionCore(): BridgeSessionCore;
18
+ get sessionCore(): BridgeSessionCore | ITelegramBridgeCore;
12
19
  }
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Telegram channel: inbound subscription + transport startup ordering for {@link BridgeSessionCore}.
2
+ * Telegram channel: inbound subscription + transport startup ordering for {@link BridgeSessionCore}
3
+ * or {@link UnifiedBridgeCore}.
3
4
  * Additional channels (e.g. Weixin) can compose the same core with their own adapters.
4
5
  */
5
6
  export class TelegramChannelAdapter {
@@ -2,6 +2,10 @@ import type { AppConfig } from '../../config.js';
2
2
  import type { BridgeSessionCore } from '../../controller/controller.js';
3
3
  import type { Logger } from '../../logger.js';
4
4
  import type { BridgeStore } from '../../store/database.js';
5
+ import type { TelegramTextEvent } from '../../telegram/gateway.js';
6
+ export interface IWeixinBridgeCore {
7
+ dispatchInboundLikeTelegramText(event: TelegramTextEvent): void;
8
+ }
5
9
  export declare class WeixinChannelAdapter {
6
10
  private readonly core;
7
11
  private readonly store;
@@ -9,7 +13,7 @@ export declare class WeixinChannelAdapter {
9
13
  private readonly logger;
10
14
  private readonly abort;
11
15
  private loops;
12
- constructor(core: BridgeSessionCore, store: BridgeStore, config: AppConfig, logger: Logger);
16
+ constructor(core: BridgeSessionCore | IWeixinBridgeCore, store: BridgeStore, config: AppConfig, logger: Logger);
13
17
  start(): Promise<void>;
14
18
  stop(): Promise<void>;
15
19
  private listAccounts;
@@ -0,0 +1,20 @@
1
+ import type { IEngineAdapter, EngineModel, EngineTurnRequest, EngineTurnExecution } from '../core/engine_spi.js';
2
+ import type { CodexAppClient } from './client.js';
3
+ import type { SandboxModeValue } from '../types.js';
4
+ export declare class CodexEngineAdapter implements IEngineAdapter {
5
+ private readonly client;
6
+ readonly id: string;
7
+ readonly name: string;
8
+ private readonly defaultModel;
9
+ private readonly defaultApprovalPolicy;
10
+ private readonly defaultSandboxMode;
11
+ constructor(client: CodexAppClient, options?: {
12
+ id?: string;
13
+ name?: string;
14
+ defaultModel?: string | undefined;
15
+ defaultApprovalPolicy?: string;
16
+ defaultSandboxMode?: SandboxModeValue;
17
+ });
18
+ listModels(): Promise<EngineModel[]>;
19
+ executeTurn(request: EngineTurnRequest): EngineTurnExecution;
20
+ }
@@ -0,0 +1,213 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { normalizeTurnActivityEvent } from '../controller/activity.js';
3
+ import { buildAttachmentPrompt } from '../telegram/media.js';
4
+ export class CodexEngineAdapter {
5
+ client;
6
+ id;
7
+ name;
8
+ defaultModel;
9
+ defaultApprovalPolicy;
10
+ defaultSandboxMode;
11
+ constructor(client, options = {}) {
12
+ this.client = client;
13
+ this.id = options.id ?? 'codex';
14
+ this.name = options.name ?? 'OpenAI Codex (App Server)';
15
+ this.defaultModel = options.defaultModel;
16
+ this.defaultApprovalPolicy = options.defaultApprovalPolicy ?? 'on-request';
17
+ this.defaultSandboxMode = options.defaultSandboxMode ?? 'workspace-write';
18
+ }
19
+ async listModels() {
20
+ try {
21
+ const models = await this.client.listModels();
22
+ const list = models.map((m) => ({
23
+ id: m.id,
24
+ name: m.displayName || m.id,
25
+ description: m.description,
26
+ isDefault: m.isDefault || m.id === this.defaultModel,
27
+ }));
28
+ if (!list.some((m) => m.id === 'gpt-6-sol')) {
29
+ list.push({
30
+ id: 'gpt-6-sol',
31
+ name: 'GPT-6-Sol',
32
+ description: 'Next-generation flagship model with deep reasoning',
33
+ isDefault: false,
34
+ });
35
+ }
36
+ return list;
37
+ }
38
+ catch {
39
+ return [];
40
+ }
41
+ }
42
+ executeTurn(request) {
43
+ const emitter = new EventEmitter();
44
+ let threadId = request.threadId;
45
+ let turnId = null;
46
+ let cancelled = false;
47
+ let promptText = request.prompt;
48
+ if (request.stagedAttachments && request.stagedAttachments.length > 0 && !request.prompt.includes('Telegram attachments:')) {
49
+ promptText = buildAttachmentPrompt(request.prompt, request.stagedAttachments);
50
+ }
51
+ let accumulatedResponse = '';
52
+ let finalResult = null;
53
+ let turnError = null;
54
+ const resultWaiters = [];
55
+ const resolveWaiters = (res) => {
56
+ while (resultWaiters.length > 0) {
57
+ resultWaiters.shift()(res);
58
+ }
59
+ };
60
+ const onNotification = (msg) => {
61
+ try {
62
+ const ev = normalizeTurnActivityEvent(msg);
63
+ if (!ev)
64
+ return;
65
+ if (ev.turnId && turnId && ev.turnId !== turnId)
66
+ return;
67
+ if (ev.kind === 'agent_message_delta') {
68
+ accumulatedResponse += ev.delta;
69
+ emitter.emit('delta', ev.delta);
70
+ }
71
+ else if (ev.kind === 'tool_started') {
72
+ const cmdName = Array.isArray(ev.exec?.command) ? ev.exec.command.join(' ') : 'command';
73
+ emitter.emit('tool', {
74
+ name: cmdName,
75
+ status: 'running',
76
+ });
77
+ }
78
+ else if (ev.kind === 'tool_completed') {
79
+ const cmdName = Array.isArray(ev.exec?.command) ? ev.exec.command.join(' ') : 'command';
80
+ emitter.emit('tool', {
81
+ name: cmdName,
82
+ status: 'completed',
83
+ });
84
+ }
85
+ else if (ev.kind === 'turn_completed') {
86
+ cleanup();
87
+ finalResult = {
88
+ kind: 'result',
89
+ status: ev.state === 'interrupted' ? 'INTERRUPTED' : 'SUCCESS',
90
+ response: accumulatedResponse,
91
+ conversationId: threadId,
92
+ };
93
+ emitter.emit('result', finalResult);
94
+ resolveWaiters(finalResult);
95
+ }
96
+ }
97
+ catch {
98
+ // Ignore parsing errors for unrelated notifications
99
+ }
100
+ };
101
+ const cleanup = () => {
102
+ this.client.off('notification', onNotification);
103
+ };
104
+ this.client.on('notification', onNotification);
105
+ const effectiveModel = request.model && request.model !== 'default'
106
+ ? request.model
107
+ : (this.defaultModel && this.defaultModel !== 'default' ? this.defaultModel : null);
108
+ const run = async () => {
109
+ try {
110
+ if (!threadId) {
111
+ const session = await this.client.startThread({
112
+ cwd: request.cwd,
113
+ model: effectiveModel,
114
+ approvalPolicy: this.defaultApprovalPolicy,
115
+ sandboxMode: this.defaultSandboxMode,
116
+ });
117
+ threadId = session.thread.threadId;
118
+ }
119
+ else {
120
+ try {
121
+ await this.client.resumeThread({
122
+ threadId,
123
+ cwd: request.cwd,
124
+ approvalPolicy: this.defaultApprovalPolicy,
125
+ });
126
+ }
127
+ catch {
128
+ // thread might already be active on server
129
+ }
130
+ }
131
+ if (cancelled) {
132
+ cleanup();
133
+ return;
134
+ }
135
+ if (!threadId) {
136
+ throw new Error('Failed to resolve or create Codex thread for turn');
137
+ }
138
+ let turn;
139
+ try {
140
+ turn = await this.client.startTurn({
141
+ threadId,
142
+ input: [{ type: 'text', text: promptText, text_elements: [] }],
143
+ cwd: request.cwd,
144
+ model: effectiveModel,
145
+ effort: request.effort ?? null,
146
+ serviceTier: request.serviceTier ?? undefined,
147
+ collaborationMode: null,
148
+ approvalPolicy: this.defaultApprovalPolicy,
149
+ sandboxMode: this.defaultSandboxMode,
150
+ });
151
+ }
152
+ catch (turnErr) {
153
+ const errMsg = String(turnErr);
154
+ if (errMsg.includes('not found') ||
155
+ errMsg.includes('No thread') ||
156
+ errMsg.includes('stale') ||
157
+ errMsg.includes('invalid thread')) {
158
+ const session = await this.client.startThread({
159
+ cwd: request.cwd,
160
+ model: effectiveModel,
161
+ approvalPolicy: this.defaultApprovalPolicy,
162
+ sandboxMode: this.defaultSandboxMode,
163
+ });
164
+ threadId = session.thread.threadId;
165
+ turn = await this.client.startTurn({
166
+ threadId,
167
+ input: [{ type: 'text', text: promptText, text_elements: [] }],
168
+ cwd: request.cwd,
169
+ model: effectiveModel,
170
+ effort: request.effort ?? null,
171
+ serviceTier: request.serviceTier ?? undefined,
172
+ collaborationMode: null,
173
+ approvalPolicy: this.defaultApprovalPolicy,
174
+ sandboxMode: this.defaultSandboxMode,
175
+ });
176
+ }
177
+ else {
178
+ throw turnErr;
179
+ }
180
+ }
181
+ turnId = turn.id;
182
+ }
183
+ catch (err) {
184
+ cleanup();
185
+ turnError = err instanceof Error ? err : new Error(String(err));
186
+ emitter.emit('error', turnError);
187
+ resolveWaiters(null);
188
+ }
189
+ };
190
+ void run();
191
+ return {
192
+ turnId: turnId ?? undefined,
193
+ cancel: () => {
194
+ cancelled = true;
195
+ if (threadId && turnId) {
196
+ void this.client.interruptTurn(threadId, turnId).catch(() => { });
197
+ }
198
+ },
199
+ waitForResult: async () => {
200
+ if (finalResult)
201
+ return finalResult;
202
+ if (turnError)
203
+ return null;
204
+ return new Promise((resolve) => {
205
+ resultWaiters.push(resolve);
206
+ });
207
+ },
208
+ on: (event, listener) => {
209
+ emitter.on(event, listener);
210
+ },
211
+ };
212
+ }
213
+ }
@@ -49,7 +49,7 @@ interface StartTurnOptions {
49
49
  serviceTier?: string | null | undefined;
50
50
  collaborationMode: CodexCollaborationMode | null;
51
51
  }
52
- interface CodexAppServerRuntimeStatus {
52
+ export interface CodexAppServerRuntimeStatus {
53
53
  pid: number | null;
54
54
  port: number | null;
55
55
  running: boolean;
@@ -78,6 +78,7 @@ export declare class CodexAppClient extends EventEmitter {
78
78
  private port;
79
79
  private connected;
80
80
  private userAgent;
81
+ private readonly requestTimeoutMs;
81
82
  constructor(codexCliBin: string, launchCommand: string, autolaunch: boolean, serverStatePath: string, serverLogPath: string, logger: Logger, childEnv?: NodeJS.ProcessEnv | null, appServerConfigOverrides?: readonly string[]);
82
83
  isConnected(): boolean;
83
84
  getUserAgent(): string | null;
@@ -33,6 +33,7 @@ export class CodexAppClient extends EventEmitter {
33
33
  port = null;
34
34
  connected = false;
35
35
  userAgent = null;
36
+ requestTimeoutMs = 30_000;
36
37
  constructor(codexCliBin, launchCommand, autolaunch, serverStatePath, serverLogPath, logger, childEnv = null, appServerConfigOverrides = []) {
37
38
  super();
38
39
  this.codexCliBin = codexCliBin;
@@ -283,6 +284,18 @@ export class CodexAppClient extends EventEmitter {
283
284
  models.push(...rows.map(mapModel));
284
285
  cursor = typeof result.nextCursor === 'string' ? result.nextCursor : null;
285
286
  } while (cursor);
287
+ if (!models.some((m) => m.id === 'gpt-6-sol' || m.model === 'gpt-6-sol')) {
288
+ models.push({
289
+ id: 'gpt-6-sol',
290
+ model: 'gpt-6-sol',
291
+ displayName: 'GPT-6-Sol',
292
+ description: 'Next-generation flagship model with deep reasoning',
293
+ isDefault: false,
294
+ supportedReasoningEfforts: ['low', 'medium', 'high', 'xhigh', 'max'],
295
+ defaultReasoningEffort: 'medium',
296
+ serviceTiers: [],
297
+ });
298
+ }
286
299
  return models;
287
300
  }
288
301
  async readAccount(refreshToken = false) {
@@ -577,6 +590,9 @@ export class CodexAppClient extends EventEmitter {
577
590
  port: state.port,
578
591
  error: error instanceof Error ? error.message : String(error),
579
592
  });
593
+ if (isProcessAlive(state.pid)) {
594
+ throw new Error(`Managed Codex app-server pid ${state.pid} is alive but unreachable; retry the connection or explicitly restart it.`, { cause: error });
595
+ }
580
596
  this.clearServerStateForPid(state.pid);
581
597
  return false;
582
598
  }
@@ -588,11 +604,18 @@ export class CodexAppClient extends EventEmitter {
588
604
  try {
589
605
  await new Promise((resolve, reject) => {
590
606
  const ws = new WebSocket(url);
607
+ const timer = setTimeout(() => {
608
+ ws.close();
609
+ reject(new Error('WebSocket handshake timed out'));
610
+ }, 2000);
591
611
  const onError = (event) => {
612
+ clearTimeout(timer);
592
613
  ws.close();
593
614
  reject(new Error(`WebSocket connect failed: ${String(event.type)}`));
594
615
  };
595
616
  ws.addEventListener('open', () => {
617
+ clearTimeout(timer);
618
+ ws.removeEventListener('error', onError);
596
619
  this.socket = ws;
597
620
  this.connected = true;
598
621
  ws.addEventListener('message', message => this.handleMessage(String(message.data)));
@@ -608,7 +631,12 @@ export class CodexAppClient extends EventEmitter {
608
631
  });
609
632
  });
610
633
  ws.addEventListener('error', err => {
611
- this.logger.warn('codex.ws.error', String(err.message ?? 'unknown'));
634
+ this.logger.warn('codex.ws.error', { message: err.message || 'WebSocket transport error', port: this.port });
635
+ if (this.socket === ws) {
636
+ this.socket = null;
637
+ ws.close();
638
+ this.handleDisconnect({ source: 'websocket-error', port: this.port });
639
+ }
612
640
  });
613
641
  resolve();
614
642
  }, { once: true });
@@ -649,8 +677,22 @@ export class CodexAppClient extends EventEmitter {
649
677
  }
650
678
  const id = String(++this.requestId);
651
679
  return new Promise((resolve, reject) => {
652
- this.pending.set(id, { resolve, reject });
653
- this.send({ jsonrpc: '2.0', id, method, params });
680
+ const timer = setTimeout(() => {
681
+ this.pending.delete(id);
682
+ this.logger.warn('codex.request_timeout', { method, timeoutMs: this.requestTimeoutMs });
683
+ reject(new Error(`Codex request timed out: ${method}. The result is unknown; check /status or use foxclaw resume before retrying.`));
684
+ }, this.requestTimeoutMs);
685
+ this.pending.set(id, {
686
+ resolve: (value) => { clearTimeout(timer); resolve(value); },
687
+ reject: (error) => { clearTimeout(timer); reject(error); },
688
+ });
689
+ try {
690
+ this.send({ jsonrpc: '2.0', id, method, params });
691
+ }
692
+ catch (error) {
693
+ this.pending.get(id)?.reject(error);
694
+ this.pending.delete(id);
695
+ }
654
696
  });
655
697
  }
656
698
  send(payload) {
@@ -690,6 +732,7 @@ export class CodexAppClient extends EventEmitter {
690
732
  }
691
733
  }
692
734
  handleDisconnect(meta) {
735
+ this.logger.warn('codex.disconnected', meta);
693
736
  if (this.connected) {
694
737
  this.connected = false;
695
738
  }
@@ -711,7 +754,7 @@ export class CodexAppClient extends EventEmitter {
711
754
  this.reconnectTimer = setTimeout(async () => {
712
755
  this.reconnectTimer = null;
713
756
  try {
714
- await this.startServer();
757
+ await this.start();
715
758
  }
716
759
  catch (error) {
717
760
  this.logger.error('codex.reconnect_failed', { error: String(error) });
@@ -0,0 +1,12 @@
1
+ export interface ExternalWriterIdentity {
2
+ pid: number;
3
+ startTime: string;
4
+ exe: string;
5
+ lockDevice: string;
6
+ lockInode: string;
7
+ cwd: string;
8
+ }
9
+ export declare const externalWriterControl: {
10
+ inspect(home: string, threadId: string): Promise<ExternalWriterIdentity>;
11
+ stop(home: string, threadId: string, expected: ExternalWriterIdentity): Promise<void>;
12
+ };
@@ -0,0 +1,27 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { promisify } from 'node:util';
4
+ const execFileAsync = promisify(execFile);
5
+ const helper = fileURLToPath(new URL('../../scripts/force-takeover.py', import.meta.url));
6
+ async function runHelper(home, threadId, expected) {
7
+ if (process.platform !== 'linux')
8
+ throw new Error('Force takeover requires Linux/WSL with pidfd support');
9
+ const args = [helper, home, threadId];
10
+ if (expected)
11
+ args.push(JSON.stringify(expected));
12
+ const { stdout } = await execFileAsync('python3', args, { timeout: 12_000, maxBuffer: 16_384 });
13
+ const response = JSON.parse(stdout);
14
+ if (!response.ok)
15
+ throw new Error(response.error ?? 'External writer inspection failed');
16
+ return response.result;
17
+ }
18
+ export const externalWriterControl = {
19
+ async inspect(home, threadId) {
20
+ return await runHelper(home, threadId);
21
+ },
22
+ async stop(home, threadId, expected) {
23
+ const result = await runHelper(home, threadId, expected);
24
+ if (result.stopped !== true)
25
+ throw new Error('CLI stop was not confirmed');
26
+ },
27
+ };
package/dist/config.d.ts CHANGED
@@ -9,7 +9,9 @@ export declare const DEFAULT_CODEX_APP_SERVER_STATE_PATH: string;
9
9
  export declare const DEFAULT_CODEX_APP_SERVER_LOG_PATH: string;
10
10
  export declare const DEFAULT_OPENCODE_SERVER_STATE_PATH: string;
11
11
  export declare const DEFAULT_OPENCODE_SERVER_LOG_PATH: string;
12
+ export declare const DEFAULT_ANTIGRAVITY_AUTH_DIR: string;
12
13
  export declare const DEFAULT_CODEX_TELEGRAM_HOME: string;
14
+ export declare const DEFAULT_ANTIGRAVITY_TELEGRAM_HOME: string;
13
15
  export declare const DEFAULT_AUTH_SYNC_STATE_PATH: string;
14
16
  export declare const DEFAULT_AUTH_SYNC_TEMP_DIR: string;
15
17
  export declare const DEFAULT_ENV_PATH: string;
@@ -42,6 +44,14 @@ export interface AppConfig {
42
44
  opencodeServerPassword: string | null;
43
45
  opencodeServerStatePath: string;
44
46
  opencodeServerLogPath: string;
47
+ antigravityBotToken: string | null;
48
+ antigravityBotTokens: string[];
49
+ antigravityMultiBotMode: boolean;
50
+ antigravityDefaultRuntimeBotToken: string | null;
51
+ antigravityHome: string | null;
52
+ antigravityCliBin: string;
53
+ antigravityAuthDir: string;
54
+ antigravityDefaultModel: string;
45
55
  storePath: string;
46
56
  logLevel: LogLevel;
47
57
  defaultCwd: string;
@@ -101,6 +111,8 @@ export interface CodexApiProviderConfig {
101
111
  export declare function loadConfig(): AppConfig;
102
112
  export declare function buildCodexApiProviderOverrides(providers: readonly CodexApiProviderConfig[], defaultProviderId?: string | null): string[];
103
113
  export declare function validateOpencodeBotToken(token: string | null, codexTokens: readonly string[]): void;
114
+ export declare function validateAntigravityBotTokens(tokens: readonly string[], codexTokens: readonly string[], opencodeToken: string | null): void;
115
+ export declare function validateAntigravityBotToken(token: string | null, codexTokens: readonly string[], opencodeToken: string | null): void;
104
116
  export declare function parseCodexApiProviders(raw: string | undefined): CodexApiProviderConfig[];
105
117
  export declare function selectDefaultRuntimeBotToken(configuredTokens: string[], legacyToken: string | null): string | null;
106
118
  export declare function ensureAppDirs(config: AppConfig): void;
package/dist/config.js CHANGED
@@ -12,7 +12,9 @@ export const DEFAULT_CODEX_APP_SERVER_STATE_PATH = path.join(APP_HOME, 'runtime'
12
12
  export const DEFAULT_CODEX_APP_SERVER_LOG_PATH = path.join(APP_HOME, 'logs', 'codex-app-server.log');
13
13
  export const DEFAULT_OPENCODE_SERVER_STATE_PATH = path.join(APP_HOME, 'runtime', 'opencode-server.json');
14
14
  export const DEFAULT_OPENCODE_SERVER_LOG_PATH = path.join(APP_HOME, 'logs', 'opencode-server.log');
15
+ export const DEFAULT_ANTIGRAVITY_AUTH_DIR = path.join(os.homedir(), '.gemini', 'antigravity-cli');
15
16
  export const DEFAULT_CODEX_TELEGRAM_HOME = path.join(APP_HOME, 'codex', 'telegram');
17
+ export const DEFAULT_ANTIGRAVITY_TELEGRAM_HOME = path.join(APP_HOME, 'antigravity', 'telegram');
16
18
  export const DEFAULT_AUTH_SYNC_STATE_PATH = path.join(APP_HOME, 'runtime', 'auth-sync.json');
17
19
  export const DEFAULT_AUTH_SYNC_TEMP_DIR = path.join(APP_HOME, 'runtime', 'auth-sync');
18
20
  export const DEFAULT_ENV_PATH = path.join(APP_HOME, '.env');
@@ -55,6 +57,16 @@ export function loadConfig() {
55
57
  }
56
58
  const opencodeBotToken = optional('OPENCODE_BOT_TOKEN');
57
59
  validateOpencodeBotToken(opencodeBotToken, tgBotTokens);
60
+ const configuredAntigravityTokens = parseCommaSeparatedIds(process.env.ANTIGRAVITY_BOT_TOKENS);
61
+ const legacyAntigravityToken = optional('ANTIGRAVITY_BOT_TOKEN');
62
+ const antigravityDefaultRuntimeBotToken = selectDefaultRuntimeBotToken(configuredAntigravityTokens, legacyAntigravityToken);
63
+ const antigravityBotTokens = configuredAntigravityTokens.length > 0
64
+ ? configuredAntigravityTokens
65
+ : legacyAntigravityToken
66
+ ? [legacyAntigravityToken]
67
+ : [];
68
+ const antigravityBotToken = antigravityBotTokens[0] || null;
69
+ validateAntigravityBotTokens(antigravityBotTokens, tgBotTokens, opencodeBotToken);
58
70
  const config = {
59
71
  tgBotToken: tgBotTokens[0],
60
72
  tgBotTokens,
@@ -81,6 +93,14 @@ export function loadConfig() {
81
93
  opencodeServerPassword: optional('OPENCODE_SERVER_PASSWORD'),
82
94
  opencodeServerStatePath: process.env.OPENCODE_SERVER_STATE_PATH || DEFAULT_OPENCODE_SERVER_STATE_PATH,
83
95
  opencodeServerLogPath: process.env.OPENCODE_SERVER_LOG_PATH || DEFAULT_OPENCODE_SERVER_LOG_PATH,
96
+ antigravityBotToken,
97
+ antigravityBotTokens,
98
+ antigravityMultiBotMode: configuredAntigravityTokens.length > 0,
99
+ antigravityDefaultRuntimeBotToken,
100
+ antigravityHome: process.env.ANTIGRAVITY_HOME?.trim() || null,
101
+ antigravityCliBin: process.env.ANTIGRAVITY_CLI_BIN || resolveCommand('agy') || '/home/wuya/.local/bin/agy',
102
+ antigravityAuthDir: process.env.ANTIGRAVITY_AUTH_DIR?.trim() || DEFAULT_ANTIGRAVITY_AUTH_DIR,
103
+ antigravityDefaultModel: process.env.ANTIGRAVITY_DEFAULT_MODEL?.trim() || 'gemini-3.8-flash-high',
84
104
  storePath: process.env.STORE_PATH || DEFAULT_STORE_PATH,
85
105
  logLevel: parseLogLevel(process.env.LOG_LEVEL || 'info'),
86
106
  defaultCwd: process.env.DEFAULT_CWD || process.cwd(),
@@ -156,6 +176,28 @@ export function validateOpencodeBotToken(token, codexTokens) {
156
176
  throw new Error('OPENCODE_BOT_TOKEN must use a different bot from TG_BOT_TOKENS/TG_BOT_TOKEN');
157
177
  }
158
178
  }
179
+ export function validateAntigravityBotTokens(tokens, codexTokens, opencodeToken) {
180
+ if (tokens.length === 0)
181
+ return;
182
+ const seen = new Set();
183
+ for (const token of tokens) {
184
+ if (seen.has(token)) {
185
+ throw new Error(`ANTIGRAVITY_BOT_TOKENS contains duplicate Telegram bot token: ${token}`);
186
+ }
187
+ seen.add(token);
188
+ if (codexTokens.includes(token)) {
189
+ throw new Error('ANTIGRAVITY_BOT_TOKENS/ANTIGRAVITY_BOT_TOKEN must use a different bot from TG_BOT_TOKENS/TG_BOT_TOKEN');
190
+ }
191
+ if (opencodeToken && token === opencodeToken) {
192
+ throw new Error('ANTIGRAVITY_BOT_TOKENS/ANTIGRAVITY_BOT_TOKEN must use a different bot from OPENCODE_BOT_TOKEN');
193
+ }
194
+ }
195
+ }
196
+ export function validateAntigravityBotToken(token, codexTokens, opencodeToken) {
197
+ if (!token)
198
+ return;
199
+ validateAntigravityBotTokens([token], codexTokens, opencodeToken);
200
+ }
159
201
  export function parseCodexApiProviders(raw) {
160
202
  if (!raw?.trim())
161
203
  return [];
@@ -2,8 +2,8 @@ import type { AppConfig } from '../config.js';
2
2
  import type { Logger } from '../logger.js';
3
3
  import type { BridgeStore } from '../store/database.js';
4
4
  import { type AuthSyncClusterAuditResult } from '../auth/cross_node_sync.js';
5
- import type { RuntimeStatus } from '../types.js';
6
- import type { TelegramGateway, TelegramTextEvent } from '../telegram/gateway.js';
5
+ import type { AppLocale, RuntimeStatus } from '../types.js';
6
+ import type { TelegramGateway, TelegramTextEvent, TelegramCallbackEvent } from '../telegram/gateway.js';
7
7
  import { BridgeMessagingRouter } from '../channels/bridge_messaging_router.js';
8
8
  import type { CodexAppClient } from '../codex_app/client.js';
9
9
  import type { SelfUpdateRuntime, SelfUpdateStatus } from '../update.js';
@@ -66,6 +66,9 @@ export declare class BridgeSessionCore {
66
66
  private pendingUserInputs;
67
67
  private pendingMcpElicitations;
68
68
  private pendingLoginsByScope;
69
+ private externalWriterControl;
70
+ private forceTakeoversInProgress;
71
+ private pendingForceTakeovers;
69
72
  private pendingLoginScopesById;
70
73
  private pendingAuthAddsByLoginId;
71
74
  private latestTurnDiffs;
@@ -116,6 +119,7 @@ export declare class BridgeSessionCore {
116
119
  * (used by the Weixin adapter).
117
120
  */
118
121
  dispatchInboundLikeTelegramText(event: TelegramTextEvent): void;
122
+ hasPendingInteraction(scopeId: string): boolean;
119
123
  /** Start Codex app-server transport and attach RPC listeners. */
120
124
  startCodexApp(): Promise<void>;
121
125
  /** Begin Telegram Bot API long-polling after handlers and Codex are ready. */
@@ -132,7 +136,7 @@ export declare class BridgeSessionCore {
132
136
  private handleWatchCommand;
133
137
  private handleThreadArchiveIndexCommand;
134
138
  private handleThreadRenameIndexCommand;
135
- private handleCallback;
139
+ handleCallback(event: TelegramCallbackEvent): Promise<void>;
136
140
  private handleApprovalTextCommand;
137
141
  private handleNotification;
138
142
  private handleServerRequest;
@@ -287,6 +291,8 @@ export declare class BridgeSessionCore {
287
291
  private applyObservedSessionEvents;
288
292
  private applyObservedTurnSnapshot;
289
293
  private handleTakeoverCommand;
294
+ private prepareForceTakeover;
295
+ private handleForceTakeoverCallback;
290
296
  private handleQueueCommand;
291
297
  private handleActiveTurnInboundMessage;
292
298
  private queueObservedThreadMessage;
@@ -322,7 +328,7 @@ export declare class BridgeSessionCore {
322
328
  private clearProactiveAuthRefreshTimer;
323
329
  private runProactiveAuthRefresh;
324
330
  private recordProactiveAuthRefreshStatus;
325
- private handleAuthCommand;
331
+ handleAuthCommand(scopeId: string, locale: AppLocale, args: string[]): Promise<void>;
326
332
  private handleAuthSyncCommand;
327
333
  private runAuthSafeSyncAll;
328
334
  private runAuthClusterAudit;
@@ -341,6 +347,7 @@ export declare class BridgeSessionCore {
341
347
  private handleVoiceCallback;
342
348
  private sendVoiceForText;
343
349
  private handleLoginDeviceCommand;
350
+ private loginCancelKeyboard;
344
351
  private handleLoginCancelCommand;
345
352
  private handleLogoutCommand;
346
353
  private handleSteerCommand;
@@ -377,6 +384,7 @@ export declare class BridgeSessionCore {
377
384
  private requireReadyBinding;
378
385
  private handleAuthPanelActionCallback;
379
386
  private handleAuthListViewCallback;
387
+ private refreshRepairedAuthChoice;
380
388
  private handleAuthRepairMenuCallback;
381
389
  private handleAuthRepairActionCallback;
382
390
  private handleAuthToggleCallback;
@@ -437,7 +445,9 @@ export declare class BridgeSessionCore {
437
445
  private handleNavigationCallback;
438
446
  private showWherePanel;
439
447
  private handleThreadListNavigationCallback;
440
- private showThreadsPanel;
448
+ showThreadsPanel(scopeId: string, messageId?: number, searchTerm?: string | null, locale?: AppLocale, options?: {
449
+ offset?: number;
450
+ }, archived?: boolean): Promise<void>;
441
451
  private showModelSettingsPanel;
442
452
  private showSetupPanel;
443
453
  private showAccessSettingsPanel;