@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.
- package/.env.example +14 -0
- package/CHANGELOG.md +129 -0
- package/README.md +22 -2
- package/README_EN.md +22 -2
- 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/auth/cross_node_sync.js +2 -2
- 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 +2 -1
- package/dist/codex_app/client.js +47 -4
- package/dist/codex_app/force_takeover.d.ts +12 -0
- package/dist/codex_app/force_takeover.js +27 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +42 -0
- package/dist/controller/controller.d.ts +15 -5
- package/dist/controller/controller.js +252 -20
- 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 +36 -6
- package/dist/i18n.js +81 -6
- package/dist/main.js +190 -20
- 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 +34 -1
- package/dist/telegram/bot_home.d.ts +3 -0
- package/dist/telegram/bot_home.js +107 -0
- package/dist/telegram/gateway.d.ts +5 -0
- package/dist/telegram/gateway.js +21 -0
- package/dist/types.d.ts +1 -0
- package/dist/update.d.ts +6 -0
- package/dist/update.js +78 -6
- package/dist/voice/target.js +2 -1
- 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/docs/user-manual.md +11 -1
- package/docs/zh/2026-09-05-bot-home-migration.md +26 -0
- package/docs/zh/2026-09-05-reliability-review.md +53 -0
- package/docs/zh/troubleshooting.md +12 -0
- package/docs/zh/user-manual.md +13 -1
- package/package.json +2 -2
- package/scripts/force-takeover.py +118 -0
- package/scripts/force-takeover.test.py +122 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Logger } from '../logger.js';
|
|
2
|
+
import type { BridgeStore } from '../store/database.js';
|
|
3
|
+
import type { AppLocale } from '../types.js';
|
|
4
|
+
export interface AntigravityConversation {
|
|
5
|
+
conversationId: string;
|
|
6
|
+
title: string;
|
|
7
|
+
preview: string;
|
|
8
|
+
stepCount: number;
|
|
9
|
+
lastModifiedTime: string;
|
|
10
|
+
updatedAt: number;
|
|
11
|
+
workspaceUris: string[];
|
|
12
|
+
workspaceDir: string | null;
|
|
13
|
+
status: string;
|
|
14
|
+
killed: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function parseWorkspaceDir(urisJson: string | null | undefined): string | null;
|
|
17
|
+
export declare function formatAge(timestamp: number, locale?: AppLocale): string;
|
|
18
|
+
export declare class AntigravityConversationManager {
|
|
19
|
+
readonly dbPath: string;
|
|
20
|
+
private readonly logger;
|
|
21
|
+
constructor(appDataDir?: string, logger?: Logger);
|
|
22
|
+
listConversations(limit?: number, search?: string): AntigravityConversation[];
|
|
23
|
+
renameConversation(conversationId: string, newTitle: string): boolean;
|
|
24
|
+
archiveConversation(conversationId: string): boolean;
|
|
25
|
+
getConversation(conversationId: string): AntigravityConversation | null;
|
|
26
|
+
resolveConversation(raw: string, scopeId: string, store: BridgeStore): AntigravityConversation | null;
|
|
27
|
+
getTranscriptPath(conversationId: string): string;
|
|
28
|
+
hasTranscript(conversationId: string): boolean;
|
|
29
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
5
|
+
export function parseWorkspaceDir(urisJson) {
|
|
6
|
+
if (!urisJson)
|
|
7
|
+
return null;
|
|
8
|
+
try {
|
|
9
|
+
const list = JSON.parse(urisJson);
|
|
10
|
+
if (Array.isArray(list) && list.length > 0 && typeof list[0] === 'string') {
|
|
11
|
+
const uri = list[0];
|
|
12
|
+
if (uri.startsWith('file://')) {
|
|
13
|
+
return uri.replace(/^file:\/\//, '');
|
|
14
|
+
}
|
|
15
|
+
return uri;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// If it's a plain string
|
|
20
|
+
if (typeof urisJson === 'string' && urisJson.startsWith('/')) {
|
|
21
|
+
return urisJson;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
export function formatAge(timestamp, locale = 'zh') {
|
|
27
|
+
if (!timestamp)
|
|
28
|
+
return '—';
|
|
29
|
+
const seconds = Math.max(0, Math.round((Date.now() - timestamp) / 1000));
|
|
30
|
+
if (seconds < 60)
|
|
31
|
+
return locale === 'zh' ? `${seconds} 秒前` : `${seconds}s ago`;
|
|
32
|
+
const minutes = Math.round(seconds / 60);
|
|
33
|
+
if (minutes < 60)
|
|
34
|
+
return locale === 'zh' ? `${minutes} 分钟前` : `${minutes}m ago`;
|
|
35
|
+
const hours = Math.round(minutes / 60);
|
|
36
|
+
if (hours < 48)
|
|
37
|
+
return locale === 'zh' ? `${hours} 小时前` : `${hours}h ago`;
|
|
38
|
+
const days = Math.round(hours / 24);
|
|
39
|
+
return locale === 'zh' ? `${days} 天前` : `${days}d ago`;
|
|
40
|
+
}
|
|
41
|
+
export class AntigravityConversationManager {
|
|
42
|
+
dbPath;
|
|
43
|
+
logger;
|
|
44
|
+
constructor(appDataDir, logger) {
|
|
45
|
+
const baseDir = appDataDir || path.join(os.homedir(), '.gemini', 'antigravity-cli');
|
|
46
|
+
this.dbPath = path.join(baseDir, 'conversation_summaries.db');
|
|
47
|
+
this.logger = logger;
|
|
48
|
+
}
|
|
49
|
+
listConversations(limit = 15, search) {
|
|
50
|
+
try {
|
|
51
|
+
if (!fs.existsSync(this.dbPath)) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
const db = new DatabaseSync(this.dbPath, { readOnly: true });
|
|
55
|
+
let rows = [];
|
|
56
|
+
try {
|
|
57
|
+
db.exec('PRAGMA busy_timeout = 3000;');
|
|
58
|
+
let query = `
|
|
59
|
+
SELECT conversation_id, title, preview, step_count, last_modified_time, workspace_uris, status, killed
|
|
60
|
+
FROM conversation_summaries
|
|
61
|
+
WHERE killed = 0
|
|
62
|
+
`;
|
|
63
|
+
const params = [];
|
|
64
|
+
if (search && search.trim()) {
|
|
65
|
+
query += ` AND (title LIKE ? OR preview LIKE ? OR workspace_uris LIKE ?)`;
|
|
66
|
+
const term = `%${search.trim()}%`;
|
|
67
|
+
params.push(term, term, term);
|
|
68
|
+
}
|
|
69
|
+
query += ` ORDER BY last_modified_time DESC LIMIT ?`;
|
|
70
|
+
params.push(limit);
|
|
71
|
+
rows = db.prepare(query).all(...params);
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
db.close();
|
|
75
|
+
}
|
|
76
|
+
return rows.map((r) => {
|
|
77
|
+
const id = String(r.conversation_id);
|
|
78
|
+
const title = String(r.title || r.preview || 'Untitled');
|
|
79
|
+
const preview = String(r.preview || '');
|
|
80
|
+
const stepCount = Number(r.step_count || 0);
|
|
81
|
+
const lastModifiedTime = String(r.last_modified_time || '');
|
|
82
|
+
const timeMs = lastModifiedTime ? new Date(lastModifiedTime).getTime() : 0;
|
|
83
|
+
const workspaceDir = parseWorkspaceDir(r.workspace_uris);
|
|
84
|
+
return {
|
|
85
|
+
conversationId: id,
|
|
86
|
+
title,
|
|
87
|
+
preview,
|
|
88
|
+
stepCount,
|
|
89
|
+
lastModifiedTime,
|
|
90
|
+
updatedAt: Number.isNaN(timeMs) ? 0 : timeMs,
|
|
91
|
+
workspaceUris: r.workspace_uris ? [String(r.workspace_uris)] : [],
|
|
92
|
+
workspaceDir,
|
|
93
|
+
status: String(r.status || ''),
|
|
94
|
+
killed: Boolean(r.killed),
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
this.logger?.warn('antigravity.conversations.list_error', {
|
|
100
|
+
error: err instanceof Error ? err.message : String(err),
|
|
101
|
+
});
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
renameConversation(conversationId, newTitle) {
|
|
106
|
+
try {
|
|
107
|
+
if (!fs.existsSync(this.dbPath))
|
|
108
|
+
return false;
|
|
109
|
+
const db = new DatabaseSync(this.dbPath);
|
|
110
|
+
try {
|
|
111
|
+
db.exec('PRAGMA busy_timeout = 3000;');
|
|
112
|
+
db.prepare('UPDATE conversation_summaries SET title = ? WHERE conversation_id = ?').run(newTitle, conversationId);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
db.close();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
this.logger?.warn('antigravity.conversations.rename_error', { error: String(err) });
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
archiveConversation(conversationId) {
|
|
125
|
+
try {
|
|
126
|
+
if (!fs.existsSync(this.dbPath))
|
|
127
|
+
return false;
|
|
128
|
+
const db = new DatabaseSync(this.dbPath);
|
|
129
|
+
try {
|
|
130
|
+
db.exec('PRAGMA busy_timeout = 3000;');
|
|
131
|
+
db.prepare('UPDATE conversation_summaries SET killed = 1 WHERE conversation_id = ?').run(conversationId);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
db.close();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
this.logger?.warn('antigravity.conversations.archive_error', { error: String(err) });
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
getConversation(conversationId) {
|
|
144
|
+
try {
|
|
145
|
+
if (!fs.existsSync(this.dbPath))
|
|
146
|
+
return null;
|
|
147
|
+
const db = new DatabaseSync(this.dbPath, { readOnly: true });
|
|
148
|
+
let row;
|
|
149
|
+
try {
|
|
150
|
+
db.exec('PRAGMA busy_timeout = 3000;');
|
|
151
|
+
row = db.prepare(`
|
|
152
|
+
SELECT conversation_id, title, preview, step_count, last_modified_time, workspace_uris, status, killed
|
|
153
|
+
FROM conversation_summaries
|
|
154
|
+
WHERE conversation_id = ?
|
|
155
|
+
`).get(conversationId);
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
db.close();
|
|
159
|
+
}
|
|
160
|
+
if (!row)
|
|
161
|
+
return null;
|
|
162
|
+
const timeMs = row.last_modified_time ? new Date(String(row.last_modified_time)).getTime() : 0;
|
|
163
|
+
return {
|
|
164
|
+
conversationId: String(row.conversation_id),
|
|
165
|
+
title: String(row.title || row.preview || 'Untitled'),
|
|
166
|
+
preview: String(row.preview || ''),
|
|
167
|
+
stepCount: Number(row.step_count || 0),
|
|
168
|
+
lastModifiedTime: String(row.last_modified_time || ''),
|
|
169
|
+
updatedAt: Number.isNaN(timeMs) ? 0 : timeMs,
|
|
170
|
+
workspaceUris: row.workspace_uris ? [String(row.workspace_uris)] : [],
|
|
171
|
+
workspaceDir: parseWorkspaceDir(row.workspace_uris),
|
|
172
|
+
status: String(row.status || ''),
|
|
173
|
+
killed: Boolean(row.killed),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
this.logger?.warn('antigravity.conversations.get_error', {
|
|
178
|
+
error: err instanceof Error ? err.message : String(err),
|
|
179
|
+
});
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
resolveConversation(raw, scopeId, store) {
|
|
184
|
+
const value = raw.trim();
|
|
185
|
+
if (!value)
|
|
186
|
+
return null;
|
|
187
|
+
// 1. Check numeric index in store cache
|
|
188
|
+
if (/^\d+$/.test(value)) {
|
|
189
|
+
const index = Number.parseInt(value, 10);
|
|
190
|
+
if (index > 0) {
|
|
191
|
+
const cached = store.getCachedThread(scopeId, index);
|
|
192
|
+
if (cached) {
|
|
193
|
+
const direct = this.getConversation(cached.threadId);
|
|
194
|
+
if (direct)
|
|
195
|
+
return direct;
|
|
196
|
+
return {
|
|
197
|
+
conversationId: cached.threadId,
|
|
198
|
+
title: cached.name || cached.preview || cached.threadId.slice(0, 8),
|
|
199
|
+
preview: cached.preview,
|
|
200
|
+
stepCount: 0,
|
|
201
|
+
lastModifiedTime: new Date(cached.updatedAt).toISOString(),
|
|
202
|
+
updatedAt: cached.updatedAt,
|
|
203
|
+
workspaceUris: cached.cwd ? [cached.cwd] : [],
|
|
204
|
+
workspaceDir: cached.cwd,
|
|
205
|
+
status: 'idle',
|
|
206
|
+
killed: false,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// 2. Check store cached threads by exact ID or prefix
|
|
212
|
+
const cachedList = store.listCachedThreads(scopeId);
|
|
213
|
+
const cachedMatch = cachedList.find((c) => c.threadId === value || c.threadId.startsWith(value));
|
|
214
|
+
if (cachedMatch) {
|
|
215
|
+
const direct = this.getConversation(cachedMatch.threadId);
|
|
216
|
+
if (direct)
|
|
217
|
+
return direct;
|
|
218
|
+
return {
|
|
219
|
+
conversationId: cachedMatch.threadId,
|
|
220
|
+
title: cachedMatch.name || cachedMatch.preview || cachedMatch.threadId.slice(0, 8),
|
|
221
|
+
preview: cachedMatch.preview,
|
|
222
|
+
stepCount: 0,
|
|
223
|
+
lastModifiedTime: new Date(cachedMatch.updatedAt).toISOString(),
|
|
224
|
+
updatedAt: cachedMatch.updatedAt,
|
|
225
|
+
workspaceUris: cachedMatch.cwd ? [cachedMatch.cwd] : [],
|
|
226
|
+
workspaceDir: cachedMatch.cwd,
|
|
227
|
+
status: 'idle',
|
|
228
|
+
killed: false,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
// 3. Direct exact or prefix lookup in SQLite
|
|
232
|
+
const direct = this.getConversation(value);
|
|
233
|
+
if (direct)
|
|
234
|
+
return direct;
|
|
235
|
+
const list = this.listConversations(50);
|
|
236
|
+
const matches = list.filter((c) => c.conversationId === value || c.conversationId.startsWith(value));
|
|
237
|
+
if (matches.length === 1) {
|
|
238
|
+
return matches[0];
|
|
239
|
+
}
|
|
240
|
+
// 4. Fallback to on-disk brain directory & transcript.jsonl
|
|
241
|
+
if (this.hasTranscript(value)) {
|
|
242
|
+
return {
|
|
243
|
+
conversationId: value,
|
|
244
|
+
title: value.slice(0, 8),
|
|
245
|
+
preview: '',
|
|
246
|
+
stepCount: 0,
|
|
247
|
+
lastModifiedTime: new Date().toISOString(),
|
|
248
|
+
updatedAt: Date.now(),
|
|
249
|
+
workspaceUris: [],
|
|
250
|
+
workspaceDir: null,
|
|
251
|
+
status: 'idle',
|
|
252
|
+
killed: false,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const brainDir = path.join(path.dirname(this.dbPath), 'brain');
|
|
256
|
+
if (fs.existsSync(brainDir)) {
|
|
257
|
+
try {
|
|
258
|
+
const entries = fs.readdirSync(brainDir);
|
|
259
|
+
const brainMatches = entries.filter((e) => e === value || e.startsWith(value));
|
|
260
|
+
if (brainMatches.length === 1) {
|
|
261
|
+
const matchedId = brainMatches[0];
|
|
262
|
+
return {
|
|
263
|
+
conversationId: matchedId,
|
|
264
|
+
title: matchedId.slice(0, 8),
|
|
265
|
+
preview: '',
|
|
266
|
+
stepCount: 0,
|
|
267
|
+
lastModifiedTime: new Date().toISOString(),
|
|
268
|
+
updatedAt: Date.now(),
|
|
269
|
+
workspaceUris: [],
|
|
270
|
+
workspaceDir: null,
|
|
271
|
+
status: 'idle',
|
|
272
|
+
killed: false,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
/* ignore */
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
getTranscriptPath(conversationId) {
|
|
283
|
+
const baseDir = path.dirname(this.dbPath);
|
|
284
|
+
return path.join(baseDir, 'brain', conversationId, '.system_generated', 'logs', 'transcript.jsonl');
|
|
285
|
+
}
|
|
286
|
+
hasTranscript(conversationId) {
|
|
287
|
+
return fs.existsSync(this.getTranscriptPath(conversationId));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export interface AgInitPayload {
|
|
2
|
+
cwd: string;
|
|
3
|
+
tools?: string[];
|
|
4
|
+
permission_mode?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface AgInitEvent {
|
|
7
|
+
event: 'init';
|
|
8
|
+
conversation_id: string;
|
|
9
|
+
init: AgInitPayload;
|
|
10
|
+
}
|
|
11
|
+
export interface AgStepUsage {
|
|
12
|
+
input_tokens: number;
|
|
13
|
+
output_tokens: number;
|
|
14
|
+
thinking_tokens: number;
|
|
15
|
+
cache_read_tokens: number;
|
|
16
|
+
total_tokens: number;
|
|
17
|
+
}
|
|
18
|
+
export interface AgToolInfo {
|
|
19
|
+
name: string;
|
|
20
|
+
parameters: Record<string, unknown>;
|
|
21
|
+
output?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface AgStepUpdatePayload {
|
|
24
|
+
conversation_id: string;
|
|
25
|
+
step_index: number;
|
|
26
|
+
state: 'ACTIVE' | 'DONE' | 'ERROR';
|
|
27
|
+
step_type: 'user_input' | 'system_message' | 'agent_response' | 'tool';
|
|
28
|
+
text_delta?: string;
|
|
29
|
+
tool_name?: string;
|
|
30
|
+
tool_info?: AgToolInfo;
|
|
31
|
+
duration_seconds?: number;
|
|
32
|
+
usage?: AgStepUsage;
|
|
33
|
+
}
|
|
34
|
+
export interface AgStepUpdateEvent {
|
|
35
|
+
event: 'step_update';
|
|
36
|
+
step_update: AgStepUpdatePayload;
|
|
37
|
+
}
|
|
38
|
+
export interface AgResultPayload {
|
|
39
|
+
conversation_id: string;
|
|
40
|
+
status: 'SUCCESS' | 'ERROR';
|
|
41
|
+
response: string;
|
|
42
|
+
error?: string;
|
|
43
|
+
duration_seconds: number;
|
|
44
|
+
num_turns: number;
|
|
45
|
+
usage?: AgStepUsage;
|
|
46
|
+
}
|
|
47
|
+
export interface AgResultEvent {
|
|
48
|
+
event: 'result';
|
|
49
|
+
result: AgResultPayload;
|
|
50
|
+
}
|
|
51
|
+
export type AgRawEvent = AgInitEvent | AgStepUpdateEvent | AgResultEvent;
|
|
52
|
+
export interface AgInitBridgeEvent {
|
|
53
|
+
kind: 'init';
|
|
54
|
+
conversationId: string;
|
|
55
|
+
cwd: string;
|
|
56
|
+
tools: string[];
|
|
57
|
+
}
|
|
58
|
+
export interface AgTextEvent {
|
|
59
|
+
kind: 'text';
|
|
60
|
+
conversationId: string;
|
|
61
|
+
stepIndex: number;
|
|
62
|
+
delta: string;
|
|
63
|
+
accumulatedText: string;
|
|
64
|
+
}
|
|
65
|
+
export interface AgToolEvent {
|
|
66
|
+
kind: 'tool';
|
|
67
|
+
conversationId: string;
|
|
68
|
+
stepIndex: number;
|
|
69
|
+
state: 'ACTIVE' | 'DONE' | 'ERROR';
|
|
70
|
+
toolName: string;
|
|
71
|
+
parameters: Record<string, unknown>;
|
|
72
|
+
output?: string | undefined;
|
|
73
|
+
}
|
|
74
|
+
export interface AgResultBridgeEvent {
|
|
75
|
+
kind: 'result';
|
|
76
|
+
conversationId: string;
|
|
77
|
+
status: 'SUCCESS' | 'ERROR';
|
|
78
|
+
response: string;
|
|
79
|
+
error?: string | undefined;
|
|
80
|
+
usage?: AgStepUsage | undefined;
|
|
81
|
+
durationSeconds: number;
|
|
82
|
+
}
|
|
83
|
+
export interface AgErrorBridgeEvent {
|
|
84
|
+
kind: 'error';
|
|
85
|
+
conversationId: string | null;
|
|
86
|
+
message: string;
|
|
87
|
+
isQuotaError: boolean;
|
|
88
|
+
}
|
|
89
|
+
export type AntigravityBridgeEvent = AgInitBridgeEvent | AgTextEvent | AgToolEvent | AgResultBridgeEvent | AgErrorBridgeEvent;
|
|
90
|
+
export declare function isVerificationError(errorMessage: string): boolean;
|
|
91
|
+
export declare function isQuotaOrAuthError(errorMessage: string): boolean;
|
|
92
|
+
export declare function isCapacityOrUnavailableError(errorMessage: string): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Normalizes raw NDJSON stream events from `agy` into clean bridge events.
|
|
95
|
+
*/
|
|
96
|
+
export declare class AntigravityEventNormalizer {
|
|
97
|
+
private accumulatedTextByStep;
|
|
98
|
+
private lastSeenUsage;
|
|
99
|
+
reset(): void;
|
|
100
|
+
accept(raw: AgRawEvent): AntigravityBridgeEvent[];
|
|
101
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export function isVerificationError(errorMessage) {
|
|
2
|
+
const lower = errorMessage.toLowerCase();
|
|
3
|
+
return (lower.includes('verification required') ||
|
|
4
|
+
lower.includes('verification. please complete') ||
|
|
5
|
+
lower.includes('eligibility check failed'));
|
|
6
|
+
}
|
|
7
|
+
export function isQuotaOrAuthError(errorMessage) {
|
|
8
|
+
const lower = errorMessage.toLowerCase();
|
|
9
|
+
return (lower.includes('resourceexhausted') ||
|
|
10
|
+
lower.includes('resource_exhausted') ||
|
|
11
|
+
lower.includes('429') ||
|
|
12
|
+
lower.includes('quota') ||
|
|
13
|
+
lower.includes('rate limit') ||
|
|
14
|
+
lower.includes('token expired') ||
|
|
15
|
+
lower.includes('not authenticated') ||
|
|
16
|
+
lower.includes('unauthenticated') ||
|
|
17
|
+
lower.includes('invalid_grant') ||
|
|
18
|
+
lower.includes('verification required') ||
|
|
19
|
+
lower.includes('verification. please complete') ||
|
|
20
|
+
lower.includes('eligibility check failed') ||
|
|
21
|
+
lower.includes('precondition required'));
|
|
22
|
+
}
|
|
23
|
+
export function isCapacityOrUnavailableError(errorMessage) {
|
|
24
|
+
const lower = errorMessage.toLowerCase();
|
|
25
|
+
return (lower.includes('unavailable') ||
|
|
26
|
+
lower.includes('503') ||
|
|
27
|
+
lower.includes('no capacity available') ||
|
|
28
|
+
lower.includes('overloaded') ||
|
|
29
|
+
lower.includes('high load'));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Normalizes raw NDJSON stream events from `agy` into clean bridge events.
|
|
33
|
+
*/
|
|
34
|
+
export class AntigravityEventNormalizer {
|
|
35
|
+
accumulatedTextByStep = new Map();
|
|
36
|
+
lastSeenUsage;
|
|
37
|
+
reset() {
|
|
38
|
+
this.accumulatedTextByStep.clear();
|
|
39
|
+
this.lastSeenUsage = undefined;
|
|
40
|
+
}
|
|
41
|
+
accept(raw) {
|
|
42
|
+
switch (raw.event) {
|
|
43
|
+
case 'init':
|
|
44
|
+
return [
|
|
45
|
+
{
|
|
46
|
+
kind: 'init',
|
|
47
|
+
conversationId: raw.conversation_id,
|
|
48
|
+
cwd: raw.init.cwd,
|
|
49
|
+
tools: raw.init.tools ?? [],
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
case 'step_update': {
|
|
53
|
+
const payload = raw.step_update;
|
|
54
|
+
if (payload.usage) {
|
|
55
|
+
this.lastSeenUsage = payload.usage;
|
|
56
|
+
}
|
|
57
|
+
if (payload.step_type === 'agent_response' && payload.text_delta) {
|
|
58
|
+
const current = this.accumulatedTextByStep.get(payload.step_index) ?? '';
|
|
59
|
+
const next = current + payload.text_delta;
|
|
60
|
+
this.accumulatedTextByStep.set(payload.step_index, next);
|
|
61
|
+
return [
|
|
62
|
+
{
|
|
63
|
+
kind: 'text',
|
|
64
|
+
conversationId: payload.conversation_id,
|
|
65
|
+
stepIndex: payload.step_index,
|
|
66
|
+
delta: payload.text_delta,
|
|
67
|
+
accumulatedText: next,
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
if (payload.step_type === 'tool' && payload.tool_name) {
|
|
72
|
+
return [
|
|
73
|
+
{
|
|
74
|
+
kind: 'tool',
|
|
75
|
+
conversationId: payload.conversation_id,
|
|
76
|
+
stepIndex: payload.step_index,
|
|
77
|
+
state: payload.state,
|
|
78
|
+
toolName: payload.tool_name,
|
|
79
|
+
parameters: payload.tool_info?.parameters ?? {},
|
|
80
|
+
output: payload.tool_info?.output,
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
case 'result': {
|
|
87
|
+
const res = raw.result;
|
|
88
|
+
const usage = res.usage ?? this.lastSeenUsage;
|
|
89
|
+
const events = [
|
|
90
|
+
{
|
|
91
|
+
kind: 'result',
|
|
92
|
+
conversationId: res.conversation_id,
|
|
93
|
+
status: res.status,
|
|
94
|
+
response: res.response,
|
|
95
|
+
error: res.error,
|
|
96
|
+
usage,
|
|
97
|
+
durationSeconds: res.duration_seconds,
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
if (res.status === 'ERROR' && res.error) {
|
|
101
|
+
events.push({
|
|
102
|
+
kind: 'error',
|
|
103
|
+
conversationId: res.conversation_id,
|
|
104
|
+
message: res.error,
|
|
105
|
+
isQuotaError: isQuotaOrAuthError(res.error),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return events;
|
|
109
|
+
}
|
|
110
|
+
default:
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AppConfig } from '../config.js';
|
|
2
|
+
import type { Logger } from '../logger.js';
|
|
3
|
+
import type { BridgeStore } from '../store/database.js';
|
|
4
|
+
import { TelegramGateway } from '../telegram/gateway.js';
|
|
5
|
+
import { AntigravityAppClient } from './client.js';
|
|
6
|
+
import { AntigravityAuthManager } from './auth.js';
|
|
7
|
+
import { AntigravityBridgeCore } from './controller.js';
|
|
8
|
+
import type { CodexAppClient } from '../codex_app/client.js';
|
|
9
|
+
export interface AntigravityRuntimeOptions {
|
|
10
|
+
botToken?: string | undefined;
|
|
11
|
+
botId?: string | undefined;
|
|
12
|
+
botUsername?: string | undefined;
|
|
13
|
+
sharedDefaultRuntime?: boolean | undefined;
|
|
14
|
+
home?: string | undefined;
|
|
15
|
+
authDir?: string | undefined;
|
|
16
|
+
codexApp?: CodexAppClient | undefined;
|
|
17
|
+
app?: AntigravityAppClient | undefined;
|
|
18
|
+
auth?: AntigravityAuthManager | undefined;
|
|
19
|
+
}
|
|
20
|
+
/** Keeps the optional Antigravity Telegram bot lifecycle decoupled from Codex and OpenCode runtimes. */
|
|
21
|
+
export declare class AntigravityTelegramRuntime {
|
|
22
|
+
private readonly bot;
|
|
23
|
+
private readonly app;
|
|
24
|
+
private readonly auth;
|
|
25
|
+
private readonly core;
|
|
26
|
+
private readonly botId;
|
|
27
|
+
private readonly configuredUsername;
|
|
28
|
+
private readonly sharedDefaultRuntime;
|
|
29
|
+
private readonly home;
|
|
30
|
+
private readonly authDir;
|
|
31
|
+
constructor(config: AppConfig, store: BridgeStore, logger: Logger, options?: AntigravityRuntimeOptions);
|
|
32
|
+
get id(): string;
|
|
33
|
+
get username(): string | null;
|
|
34
|
+
get isSharedDefaultRuntime(): boolean;
|
|
35
|
+
get botHome(): string;
|
|
36
|
+
get authDirectory(): string;
|
|
37
|
+
start(): Promise<void>;
|
|
38
|
+
stop(): Promise<void>;
|
|
39
|
+
getRuntimeStatus(): ReturnType<AntigravityBridgeCore['getRuntimeStatus']> & {
|
|
40
|
+
id?: string;
|
|
41
|
+
botHome?: string;
|
|
42
|
+
authDir?: string;
|
|
43
|
+
sharedDefaultRuntime?: boolean;
|
|
44
|
+
};
|
|
45
|
+
get botGateway(): TelegramGateway;
|
|
46
|
+
get bridgeCore(): AntigravityBridgeCore;
|
|
47
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import { TelegramMessagingPort } from '../channels/telegram/telegram_messaging_port.js';
|
|
3
|
+
import { getAntigravityTelegramCommands } from '../i18n.js';
|
|
4
|
+
import { TelegramGateway } from '../telegram/gateway.js';
|
|
5
|
+
import { AntigravityAppClient } from './client.js';
|
|
6
|
+
import { AntigravityAuthManager } from './auth.js';
|
|
7
|
+
import { AntigravityBridgeCore } from './controller.js';
|
|
8
|
+
/** Keeps the optional Antigravity Telegram bot lifecycle decoupled from Codex and OpenCode runtimes. */
|
|
9
|
+
export class AntigravityTelegramRuntime {
|
|
10
|
+
bot;
|
|
11
|
+
app;
|
|
12
|
+
auth;
|
|
13
|
+
core;
|
|
14
|
+
botId;
|
|
15
|
+
configuredUsername;
|
|
16
|
+
sharedDefaultRuntime;
|
|
17
|
+
home;
|
|
18
|
+
authDir;
|
|
19
|
+
constructor(config, store, logger, options) {
|
|
20
|
+
const token = options?.botToken ?? config.antigravityBotToken;
|
|
21
|
+
if (!token) {
|
|
22
|
+
throw new Error('ANTIGRAVITY_BOT_TOKEN or ANTIGRAVITY_BOT_TOKENS is required for the Antigravity runtime');
|
|
23
|
+
}
|
|
24
|
+
this.bot = new TelegramGateway(token, config.tgAllowedUserId, config.tgAllowedChatId, config.telegramPollIntervalMs, store, logger, true, getAntigravityTelegramCommands);
|
|
25
|
+
this.botId = options?.botId ?? null;
|
|
26
|
+
this.configuredUsername = options?.botUsername ?? null;
|
|
27
|
+
this.home = options?.home ?? os.homedir();
|
|
28
|
+
this.authDir = options?.authDir ?? config.antigravityAuthDir;
|
|
29
|
+
this.sharedDefaultRuntime =
|
|
30
|
+
options?.sharedDefaultRuntime ??
|
|
31
|
+
(config.antigravityDefaultRuntimeBotToken
|
|
32
|
+
? config.antigravityDefaultRuntimeBotToken === token
|
|
33
|
+
: true);
|
|
34
|
+
this.auth = options?.auth ?? new AntigravityAuthManager(this.authDir, logger);
|
|
35
|
+
const childEnv = options?.home && !this.sharedDefaultRuntime ? { HOME: options.home } : null;
|
|
36
|
+
this.app = options?.app ?? new AntigravityAppClient(config.antigravityCliBin, logger, childEnv);
|
|
37
|
+
this.core = new AntigravityBridgeCore(config, store, logger, this.bot, this.app, this.auth, new TelegramMessagingPort(this.bot), {
|
|
38
|
+
...options,
|
|
39
|
+
defaultBackendId: 'antigravity',
|
|
40
|
+
});
|
|
41
|
+
this.core.registerInboundHandlers();
|
|
42
|
+
}
|
|
43
|
+
get id() {
|
|
44
|
+
return this.botId ?? this.bot.identity ?? 'unknown';
|
|
45
|
+
}
|
|
46
|
+
get username() {
|
|
47
|
+
return this.bot.username ?? this.configuredUsername;
|
|
48
|
+
}
|
|
49
|
+
get isSharedDefaultRuntime() {
|
|
50
|
+
return this.sharedDefaultRuntime;
|
|
51
|
+
}
|
|
52
|
+
get botHome() {
|
|
53
|
+
return this.home;
|
|
54
|
+
}
|
|
55
|
+
get authDirectory() {
|
|
56
|
+
return this.authDir;
|
|
57
|
+
}
|
|
58
|
+
async start() {
|
|
59
|
+
await this.bot.initializeIdentity().catch(() => { });
|
|
60
|
+
await this.bot.resolveUsername().catch(() => { });
|
|
61
|
+
await this.core.start();
|
|
62
|
+
}
|
|
63
|
+
async stop() {
|
|
64
|
+
await this.core.stop();
|
|
65
|
+
}
|
|
66
|
+
getRuntimeStatus() {
|
|
67
|
+
const status = this.core.getRuntimeStatus();
|
|
68
|
+
return {
|
|
69
|
+
...status,
|
|
70
|
+
id: this.id,
|
|
71
|
+
botHome: this.home,
|
|
72
|
+
authDir: this.authDir,
|
|
73
|
+
sharedDefaultRuntime: this.sharedDefaultRuntime,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
get botGateway() {
|
|
77
|
+
return this.bot;
|
|
78
|
+
}
|
|
79
|
+
get bridgeCore() {
|
|
80
|
+
return this.core;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -776,11 +776,11 @@ export class CrossNodeAuthSync {
|
|
|
776
776
|
this.recordEvent({
|
|
777
777
|
direction: 'local',
|
|
778
778
|
kind: 'audit.state',
|
|
779
|
-
stage: message.state,
|
|
779
|
+
stage: applied ? message.state : 'skipped',
|
|
780
780
|
peer: null,
|
|
781
781
|
requestId: message.requestId,
|
|
782
782
|
candidateName: message.candidateName,
|
|
783
|
-
detail: null,
|
|
783
|
+
detail: applied ? null : 'local candidate did not match the audited identity or refresh timestamp',
|
|
784
784
|
});
|
|
785
785
|
}
|
|
786
786
|
async buildAuditReport() {
|