@foxden-app/foxclaw 0.7.3 → 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 (58) hide show
  1. package/.env.example +13 -0
  2. package/CHANGELOG.md +109 -0
  3. package/README.md +21 -1
  4. package/README_EN.md +21 -1
  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/channels/telegram/telegram_channel_adapter.d.ts +10 -3
  20. package/dist/channels/telegram/telegram_channel_adapter.js +2 -1
  21. package/dist/channels/weixin/weixin_channel_adapter.d.ts +5 -1
  22. package/dist/codex_app/adapter.d.ts +20 -0
  23. package/dist/codex_app/adapter.js +213 -0
  24. package/dist/codex_app/client.d.ts +1 -1
  25. package/dist/codex_app/client.js +12 -0
  26. package/dist/config.d.ts +12 -0
  27. package/dist/config.js +42 -0
  28. package/dist/controller/controller.d.ts +8 -5
  29. package/dist/controller/controller.js +38 -8
  30. package/dist/core/attachments.d.ts +11 -0
  31. package/dist/core/attachments.js +56 -0
  32. package/dist/core/engine_spi.d.ts +76 -0
  33. package/dist/core/engine_spi.js +1 -0
  34. package/dist/core/orchestrator.d.ts +101 -0
  35. package/dist/core/orchestrator.js +1404 -0
  36. package/dist/core/stream_preview.d.ts +29 -0
  37. package/dist/core/stream_preview.js +146 -0
  38. package/dist/core/turn_queue.d.ts +19 -0
  39. package/dist/core/turn_queue.js +41 -0
  40. package/dist/i18n.d.ts +6 -0
  41. package/dist/i18n.js +49 -0
  42. package/dist/main.js +146 -12
  43. package/dist/opencode/adapter.d.ts +11 -0
  44. package/dist/opencode/adapter.js +168 -0
  45. package/dist/store/database.d.ts +41 -0
  46. package/dist/store/database.js +242 -43
  47. package/dist/store/token_usage.d.ts +23 -0
  48. package/dist/store/token_usage.js +61 -0
  49. package/dist/telegram/api.js +30 -1
  50. package/dist/telegram/gateway.d.ts +4 -0
  51. package/dist/telegram/gateway.js +11 -0
  52. package/dist/types.d.ts +1 -0
  53. package/dist/update.d.ts +6 -0
  54. package/dist/update.js +78 -6
  55. package/dist/voice/tts.js +4 -2
  56. 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
  57. 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
  58. package/package.json +2 -2
@@ -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;
@@ -284,6 +284,18 @@ export class CodexAppClient extends EventEmitter {
284
284
  models.push(...rows.map(mapModel));
285
285
  cursor = typeof result.nextCursor === 'string' ? result.nextCursor : null;
286
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
+ }
287
299
  return models;
288
300
  }
289
301
  async readAccount(refreshToken = false) {
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';
@@ -119,6 +119,7 @@ export declare class BridgeSessionCore {
119
119
  * (used by the Weixin adapter).
120
120
  */
121
121
  dispatchInboundLikeTelegramText(event: TelegramTextEvent): void;
122
+ hasPendingInteraction(scopeId: string): boolean;
122
123
  /** Start Codex app-server transport and attach RPC listeners. */
123
124
  startCodexApp(): Promise<void>;
124
125
  /** Begin Telegram Bot API long-polling after handlers and Codex are ready. */
@@ -135,7 +136,7 @@ export declare class BridgeSessionCore {
135
136
  private handleWatchCommand;
136
137
  private handleThreadArchiveIndexCommand;
137
138
  private handleThreadRenameIndexCommand;
138
- private handleCallback;
139
+ handleCallback(event: TelegramCallbackEvent): Promise<void>;
139
140
  private handleApprovalTextCommand;
140
141
  private handleNotification;
141
142
  private handleServerRequest;
@@ -327,7 +328,7 @@ export declare class BridgeSessionCore {
327
328
  private clearProactiveAuthRefreshTimer;
328
329
  private runProactiveAuthRefresh;
329
330
  private recordProactiveAuthRefreshStatus;
330
- private handleAuthCommand;
331
+ handleAuthCommand(scopeId: string, locale: AppLocale, args: string[]): Promise<void>;
331
332
  private handleAuthSyncCommand;
332
333
  private runAuthSafeSyncAll;
333
334
  private runAuthClusterAudit;
@@ -444,7 +445,9 @@ export declare class BridgeSessionCore {
444
445
  private handleNavigationCallback;
445
446
  private showWherePanel;
446
447
  private handleThreadListNavigationCallback;
447
- private showThreadsPanel;
448
+ showThreadsPanel(scopeId: string, messageId?: number, searchTerm?: string | null, locale?: AppLocale, options?: {
449
+ offset?: number;
450
+ }, archived?: boolean): Promise<void>;
448
451
  private showModelSettingsPanel;
449
452
  private showSetupPanel;
450
453
  private showAccessSettingsPanel;
@@ -26,6 +26,7 @@ import { normalizeAccessPreset, resolveAccessMode } from './access.js';
26
26
  import { diffObservedTurn, findLatestTurn, findLiveTurn } from './observer.js';
27
27
  import { applySessionLog, bootstrapSessionLog, splitJsonlChunk, } from './session_observer.js';
28
28
  import { renderActiveTurnStatus } from './status.js';
29
+ import { formatMetricTokenCount } from '../store/token_usage.js';
29
30
  import { writeRuntimeStatus } from '../runtime.js';
30
31
  const AUTH_DELETE_REASON_NEEDS_REPAIR = 'needs_repair';
31
32
  const RESTART_PREVIEW_RECOVERY_RETRY_DELAYS_MS = [3000, 7000, 15_000, 30_000, 45_000, 60_000, 60_000, 60_000];
@@ -212,6 +213,12 @@ export class BridgeSessionCore {
212
213
  void this.handleAsyncError('channel.text', error, event.scopeId);
213
214
  });
214
215
  }
216
+ hasPendingInteraction(scopeId) {
217
+ return this.pendingThreadRenames.has(scopeId)
218
+ || this.pendingThreadNewCwds.has(scopeId)
219
+ || this.pendingUserInputs.has(scopeId)
220
+ || this.pendingForceTakeovers.has(scopeId);
221
+ }
215
222
  /** Start Codex app-server transport and attach RPC listeners. */
216
223
  async startCodexApp() {
217
224
  this.stopping = false;
@@ -324,12 +331,13 @@ export class BridgeSessionCore {
324
331
  this.updateStatus();
325
332
  }
326
333
  getRuntimeStatus() {
334
+ const appServer = typeof this.app.getServerStatus === 'function' ? this.app.getServerStatus() : null;
327
335
  return {
328
336
  running: true,
329
- connected: this.app.isConnected(),
330
- userAgent: this.app.getUserAgent(),
337
+ connected: typeof this.app.isConnected === 'function' ? this.app.isConnected() : false,
338
+ userAgent: typeof this.app.getUserAgent === 'function' ? this.app.getUserAgent() : null,
331
339
  codexHome: this.config.codexHome ?? path.join(os.homedir(), '.codex'),
332
- codexAppServer: this.app.getServerStatus(),
340
+ ...(appServer ? { codexAppServer: appServer } : {}),
333
341
  botUsername: this.botUsername,
334
342
  currentBindings: this.store.countBindings(),
335
343
  pendingApprovals: this.store.countPendingApprovals(),
@@ -559,6 +567,9 @@ export class BridgeSessionCore {
559
567
  if (codexUpdateLine) {
560
568
  lines.push(t(locale, 'status_last_codex_update', { value: codexUpdateLine }));
561
569
  }
570
+ if (lastUpdate.agyUpdate) {
571
+ lines.push(locale === 'zh' ? `Antigravity CLI:${lastUpdate.agyUpdate}` : `Antigravity CLI: ${lastUpdate.agyUpdate}`);
572
+ }
562
573
  }
563
574
  else {
564
575
  lines.push(t(locale, 'status_last_update_none'));
@@ -3481,7 +3492,9 @@ export class BridgeSessionCore {
3481
3492
  this.coordinator.statusUpdated(status);
3482
3493
  return;
3483
3494
  }
3484
- writeRuntimeStatus(this.config.statusPath, status);
3495
+ if (this.config.statusPath) {
3496
+ writeRuntimeStatus(this.config.statusPath, status);
3497
+ }
3485
3498
  }
3486
3499
  async sendMessage(scopeId, text, inlineKeyboard) {
3487
3500
  return this.sendRichMarkdownMessage(scopeId, text, inlineKeyboard);
@@ -5044,6 +5057,9 @@ export class BridgeSessionCore {
5044
5057
  const rows = [
5045
5058
  ['FoxClaw', `${status.fromVersion} -> ${status.toVersion ?? t(status.locale, 'unknown')}`, status.locale === 'zh' ? '升级完成,服务已重启' : 'Updated; service restarted'],
5046
5059
  ['Codex CLI', formatSelfUpdateVersionTransition(status.codexFromVersion, status.codexToVersion, status.locale), formatCodexUpdateState(status)],
5060
+ ...(status.agyFromVersion || status.agyUpdate ? [
5061
+ ['Antigravity (AGY)', formatSelfUpdateVersionTransition(status.agyFromVersion ?? null, status.agyToVersion ?? null, status.locale), status.agyUpdate ?? (status.locale === 'zh' ? '已检查更新' : 'Checked')]
5062
+ ] : []),
5047
5063
  [status.locale === 'zh' ? '集群广播' : 'Cluster broadcast', status.toVersion ?? t(status.locale, 'unknown'), broadcastLine],
5048
5064
  ];
5049
5065
  const notes = status.releaseNotes?.filter(note => note.trim()) ?? [];
@@ -5061,6 +5077,7 @@ export class BridgeSessionCore {
5061
5077
  telegramBold(title),
5062
5078
  escapeTelegramHtml(foxclawResult),
5063
5079
  escapeTelegramHtml(codexUpdateLine ?? (status.locale === 'zh' ? 'Codex CLI:未执行升级。' : 'Codex CLI: not updated.')),
5080
+ ...(status.agyUpdate ? [escapeTelegramHtml(`Antigravity CLI:${status.agyUpdate}`)] : []),
5064
5081
  escapeTelegramHtml(broadcastLine),
5065
5082
  releaseNotes ? escapeTelegramHtml(releaseNotes) : '',
5066
5083
  ].filter(Boolean).join('\n');
@@ -7049,11 +7066,11 @@ export class BridgeSessionCore {
7049
7066
  events: formatTokenCount(stats.usageEvents),
7050
7067
  }),
7051
7068
  t(locale, 'status_codex_local_tokens', {
7052
- total: formatTokenCount(stats.totals.totalTokens),
7053
- input: formatTokenCount(stats.totals.inputTokens),
7069
+ total: formatCodexTokenCountWithMetric(stats.totals.totalTokens),
7070
+ input: formatCodexTokenCountWithMetric(stats.totals.inputTokens),
7054
7071
  visible: formatTokenCount(Math.max(0, stats.totals.outputTokens - stats.totals.reasoningOutputTokens)),
7055
- output: formatTokenCount(stats.totals.outputTokens),
7056
- cached: formatTokenCount(stats.totals.cachedInputTokens),
7072
+ output: formatCodexTokenCountWithMetric(stats.totals.outputTokens),
7073
+ cached: formatCodexTokenCountWithMetric(stats.totals.cachedInputTokens),
7057
7074
  reasoning: formatTokenCount(stats.totals.reasoningOutputTokens),
7058
7075
  }),
7059
7076
  ...this.formatCodexLocalResponseThroughputStatusLines(locale, stats),
@@ -7122,6 +7139,13 @@ export class BridgeSessionCore {
7122
7139
  this.localUsageCache = snapshot;
7123
7140
  this.localUsageCacheLoaded = true;
7124
7141
  await writeCodexLocalUsageSnapshot(this.codexLocalUsageSnapshotPath(), snapshot);
7142
+ this.store.setBackendCumulativeTokenUsage({
7143
+ inputTokens: stats.totals.inputTokens,
7144
+ outputTokens: stats.totals.outputTokens,
7145
+ cachedTokens: stats.totals.cachedInputTokens,
7146
+ totalTokens: stats.totals.totalTokens,
7147
+ turnsCount: stats.turns,
7148
+ }, 'codex');
7125
7149
  }
7126
7150
  codexLocalUsageSnapshotPath() {
7127
7151
  return path.join(path.dirname(this.config.statusPath), this.runtimeSnapshotFilename(CODEX_LOCAL_USAGE_SNAPSHOT_FILENAME));
@@ -12031,6 +12055,12 @@ function formatTokenCount(value) {
12031
12055
  }
12032
12056
  return Math.round(value).toLocaleString('en-US');
12033
12057
  }
12058
+ function formatCodexTokenCountWithMetric(value) {
12059
+ if (value >= 1_000_000) {
12060
+ return `${formatMetricTokenCount(value)} (${formatTokenCount(value)})`;
12061
+ }
12062
+ return formatTokenCount(value);
12063
+ }
12034
12064
  function formatLocalTimestamp(seconds) {
12035
12065
  const date = new Date(seconds * 1000);
12036
12066
  const pad = (value) => String(value).padStart(2, '0');
@@ -0,0 +1,11 @@
1
+ import type { Logger } from '../logger.js';
2
+ import { type StagedTelegramAttachment, type TelegramInboundAttachment } from '../telegram/media.js';
3
+ export interface TelegramFileDownloader {
4
+ getFile(fileId: string): Promise<{
5
+ file_id: string;
6
+ file_path?: string;
7
+ file_size?: number;
8
+ }>;
9
+ downloadResolvedFile(filePath: string, destination: string): Promise<unknown>;
10
+ }
11
+ export declare function stageInboundAttachments(downloader: TelegramFileDownloader, cwd: string, threadId: string, attachments: readonly TelegramInboundAttachment[], logger?: Logger): Promise<StagedTelegramAttachment[]>;
@@ -0,0 +1,56 @@
1
+ import fsPromises from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { planAttachmentStoragePath, isNativeImageAttachment, TELEGRAM_BOT_API_DOWNLOAD_LIMIT_BYTES, } from '../telegram/media.js';
4
+ export async function stageInboundAttachments(downloader, cwd, threadId, attachments, logger) {
5
+ const staged = [];
6
+ for (const attachment of attachments) {
7
+ try {
8
+ if (attachment.localPath) {
9
+ const planned = planAttachmentStoragePath(cwd, threadId, attachment, path.basename(attachment.localPath));
10
+ await fsPromises.mkdir(path.dirname(planned.localPath), { recursive: true });
11
+ await fsPromises.copyFile(attachment.localPath, planned.localPath);
12
+ const stat = await fsPromises.stat(planned.localPath);
13
+ const resolvedSize = stat.size;
14
+ staged.push({
15
+ ...attachment,
16
+ fileName: planned.fileName,
17
+ fileSize: resolvedSize,
18
+ localPath: planned.localPath,
19
+ relativePath: planned.relativePath,
20
+ nativeImage: isNativeImageAttachment(attachment),
21
+ });
22
+ continue;
23
+ }
24
+ const remoteFile = await downloader.getFile(attachment.fileId);
25
+ const resolvedSize = attachment.fileSize ?? remoteFile.file_size ?? null;
26
+ if (resolvedSize !== null && resolvedSize > TELEGRAM_BOT_API_DOWNLOAD_LIMIT_BYTES) {
27
+ logger?.warn('attachments.too_large', {
28
+ fileId: attachment.fileId,
29
+ size: resolvedSize,
30
+ });
31
+ continue;
32
+ }
33
+ if (!remoteFile.file_path) {
34
+ continue;
35
+ }
36
+ const planned = planAttachmentStoragePath(cwd, threadId, attachment, remoteFile.file_path);
37
+ await fsPromises.mkdir(path.dirname(planned.localPath), { recursive: true });
38
+ await downloader.downloadResolvedFile(remoteFile.file_path, planned.localPath);
39
+ staged.push({
40
+ ...attachment,
41
+ fileName: planned.fileName,
42
+ fileSize: resolvedSize,
43
+ localPath: planned.localPath,
44
+ relativePath: planned.relativePath,
45
+ nativeImage: isNativeImageAttachment(attachment),
46
+ });
47
+ }
48
+ catch (error) {
49
+ logger?.warn('attachments.download_failed', {
50
+ fileId: attachment.fileId,
51
+ error: error instanceof Error ? error.message : String(error),
52
+ });
53
+ }
54
+ }
55
+ return staged;
56
+ }