@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.
- package/.env.example +13 -0
- package/CHANGELOG.md +109 -0
- package/README.md +21 -1
- package/README_EN.md +21 -1
- package/dist/antigravity/adapter.d.ts +17 -0
- package/dist/antigravity/adapter.js +256 -0
- package/dist/antigravity/auth.d.ts +121 -0
- package/dist/antigravity/auth.js +896 -0
- package/dist/antigravity/client.d.ts +32 -0
- package/dist/antigravity/client.js +238 -0
- package/dist/antigravity/controller.d.ts +96 -0
- package/dist/antigravity/controller.js +1922 -0
- package/dist/antigravity/conversations.d.ts +29 -0
- package/dist/antigravity/conversations.js +289 -0
- package/dist/antigravity/events.d.ts +101 -0
- package/dist/antigravity/events.js +114 -0
- package/dist/antigravity/runtime.d.ts +47 -0
- package/dist/antigravity/runtime.js +82 -0
- package/dist/channels/telegram/telegram_channel_adapter.d.ts +10 -3
- package/dist/channels/telegram/telegram_channel_adapter.js +2 -1
- package/dist/channels/weixin/weixin_channel_adapter.d.ts +5 -1
- package/dist/codex_app/adapter.d.ts +20 -0
- package/dist/codex_app/adapter.js +213 -0
- package/dist/codex_app/client.d.ts +1 -1
- package/dist/codex_app/client.js +12 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +42 -0
- package/dist/controller/controller.d.ts +8 -5
- package/dist/controller/controller.js +38 -8
- package/dist/core/attachments.d.ts +11 -0
- package/dist/core/attachments.js +56 -0
- package/dist/core/engine_spi.d.ts +76 -0
- package/dist/core/engine_spi.js +1 -0
- package/dist/core/orchestrator.d.ts +101 -0
- package/dist/core/orchestrator.js +1404 -0
- package/dist/core/stream_preview.d.ts +29 -0
- package/dist/core/stream_preview.js +146 -0
- package/dist/core/turn_queue.d.ts +19 -0
- package/dist/core/turn_queue.js +41 -0
- package/dist/i18n.d.ts +6 -0
- package/dist/i18n.js +49 -0
- package/dist/main.js +146 -12
- package/dist/opencode/adapter.d.ts +11 -0
- package/dist/opencode/adapter.js +168 -0
- package/dist/store/database.d.ts +41 -0
- package/dist/store/database.js +242 -43
- package/dist/store/token_usage.d.ts +23 -0
- package/dist/store/token_usage.js +61 -0
- package/dist/telegram/api.js +30 -1
- package/dist/telegram/gateway.d.ts +4 -0
- package/dist/telegram/gateway.js +11 -0
- package/dist/types.d.ts +1 -0
- package/dist/update.d.ts +6 -0
- package/dist/update.js +78 -6
- package/dist/voice/tts.js +4 -2
- 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
- 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
- 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;
|
package/dist/codex_app/client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
7053
|
-
input:
|
|
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:
|
|
7056
|
-
cached:
|
|
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
|
+
}
|