@foxden-app/foxclaw 0.10.0 → 0.11.1

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.
@@ -64,6 +64,7 @@ export class OpencodeEngineAdapter {
64
64
  throw new Error(`Failed to create OpenCode session: ${formatSdkError(createRes.error)}`);
65
65
  }
66
66
  sessionId = createRes.data.id;
67
+ emitter.emit('conversation', sessionId);
67
68
  }
68
69
  if (cancelled)
69
70
  return;
@@ -720,7 +720,7 @@ export class OpencodeBridgeCore {
720
720
  const model = index >= 0 ? models[index] : models.find((item) => `${item.providerId}/${item.modelId}` === normalized || item.modelId === normalized || storeModel(item.providerId, item.modelId) === normalized);
721
721
  if (!model)
722
722
  throw new Error(localize(locale, `未知模型:${raw}`, `Unknown model: ${raw}`));
723
- this.store.setChatSettings(scopeId, storeModel(model.providerId, model.modelId), null);
723
+ this.store.setChatModel(scopeId, storeModel(model.providerId, model.modelId));
724
724
  const prefs = readPrefs(this.store.getChatSettings(scopeId));
725
725
  if (prefs.variant && !model.variants.includes(prefs.variant))
726
726
  this.writePrefs(scopeId, { ...prefs, variant: null });
@@ -1792,7 +1792,7 @@ export class OpencodeBridgeCore {
1792
1792
  }
1793
1793
  else {
1794
1794
  const parsed = parseStoredModel(action.value);
1795
- this.store.setChatSettings(event.scopeId, action.value, null);
1795
+ this.store.setChatModel(event.scopeId, action.value);
1796
1796
  this.writePrefs(event.scopeId, { ...readPrefs(this.store.getChatSettings(event.scopeId)), variant: null });
1797
1797
  answer = `${parsed.providerId}/${parsed.modelId}`;
1798
1798
  }
@@ -59,6 +59,7 @@ export declare class BridgeStore {
59
59
  scopeId: string;
60
60
  chatId: string;
61
61
  } | null;
62
+ migrateTelegramUnnamespacedScope(botId: string, chatId: string): void;
62
63
  getBinding(chatId: string): ThreadBinding | null;
63
64
  setBinding(chatId: string, threadId: string, cwd: string | null): void;
64
65
  listBindings(): ThreadBinding[];
@@ -71,6 +72,8 @@ export declare class BridgeStore {
71
72
  }>;
72
73
  getChatSettings(chatId: string): ChatSessionSettings | null;
73
74
  setChatSettings(chatId: string, model: string | null, reasoningEffort: ReasoningEffortValue | null, locale?: AppLocale | null): void;
75
+ setChatModel(chatId: string, model: string | null): void;
76
+ setChatEffort(chatId: string, effort: ReasoningEffortValue | null): void;
74
77
  setChatLocale(chatId: string, locale: AppLocale): void;
75
78
  setChatAccessPreset(chatId: string, accessPreset: AccessPresetValue | null): void;
76
79
  setChatCollaborationMode(chatId: string, collaborationMode: CollaborationModeValue | null): void;
@@ -298,6 +298,31 @@ export class BridgeStore {
298
298
  const row = this.db.prepare('SELECT scope_id, chat_id FROM telegram_private_scopes WHERE bot_id = ?').get(botId);
299
299
  return row ? { scopeId: String(row.scope_id), chatId: String(row.chat_id) } : null;
300
300
  }
301
+ migrateTelegramUnnamespacedScope(botId, chatId) {
302
+ const legacyScope = `telegram:${chatId}::root`;
303
+ const namespacedScope = `telegram:${botId}:${chatId}::root`;
304
+ const existing = this.db.prepare('SELECT chat_id, thread_id, cwd, updated_at FROM chat_bindings WHERE chat_id = ?').get(legacyScope);
305
+ if (existing) {
306
+ const target = this.db.prepare('SELECT chat_id, thread_id, cwd, updated_at FROM chat_bindings WHERE chat_id = ?').get(namespacedScope);
307
+ if (!target) {
308
+ this.db.prepare('INSERT INTO chat_bindings (chat_id, thread_id, cwd, updated_at) VALUES (?, ?, ?, ?)').run(namespacedScope, existing.thread_id, existing.cwd, existing.updated_at);
309
+ }
310
+ else if (Number(existing.updated_at) > Number(target.updated_at)) {
311
+ this.db.prepare('UPDATE chat_bindings SET thread_id = ?, cwd = ?, updated_at = ? WHERE chat_id = ?').run(existing.thread_id, existing.cwd, existing.updated_at, namespacedScope);
312
+ }
313
+ }
314
+ const legacyBackends = this.db.prepare('SELECT * FROM scope_backend_bindings WHERE scope_id = ?').all(legacyScope);
315
+ for (const row of legacyBackends) {
316
+ const backendId = String(row.backend_id);
317
+ const targetRow = this.db.prepare('SELECT updated_at FROM scope_backend_bindings WHERE scope_id = ? AND backend_id = ?').get(namespacedScope, backendId);
318
+ if (!targetRow) {
319
+ this.db.prepare('INSERT INTO scope_backend_bindings (scope_id, backend_id, thread_id, cwd, updated_at, model, reasoning_effort, active_turn_message_mode, service_tier, access_preset) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').run(namespacedScope, backendId, row.thread_id, row.cwd, row.updated_at, row.model, row.reasoning_effort, row.active_turn_message_mode, row.service_tier, row.access_preset);
320
+ }
321
+ else if (Number(row.updated_at) > Number(targetRow.updated_at)) {
322
+ this.db.prepare('UPDATE scope_backend_bindings SET thread_id = ?, cwd = ?, updated_at = ?, model = ?, reasoning_effort = ?, active_turn_message_mode = ?, service_tier = ?, access_preset = ? WHERE scope_id = ? AND backend_id = ?').run(row.thread_id, row.cwd, row.updated_at, row.model, row.reasoning_effort, row.active_turn_message_mode, row.service_tier, row.access_preset, namespacedScope, backendId);
323
+ }
324
+ }
325
+ }
301
326
  getBinding(chatId) {
302
327
  const row = this.db.prepare('SELECT chat_id, thread_id, cwd, updated_at FROM chat_bindings WHERE chat_id = ?').get(chatId);
303
328
  if (!row)
@@ -370,6 +395,14 @@ export class BridgeStore {
370
395
  const nextLocale = locale === undefined ? current?.locale ?? null : locale;
371
396
  this.writeChatSettings(chatId, model, reasoningEffort, nextLocale, current?.accessPreset ?? null, current?.collaborationMode ?? null, current?.serviceTier ?? null, current?.activeTurnMessageMode ?? null, current?.activeBackendId ?? null);
372
397
  }
398
+ setChatModel(chatId, model) {
399
+ const current = this.getChatSettings(chatId);
400
+ this.setChatSettings(chatId, model, current?.reasoningEffort ?? null);
401
+ }
402
+ setChatEffort(chatId, effort) {
403
+ const current = this.getChatSettings(chatId);
404
+ this.setChatSettings(chatId, current?.model ?? null, effort);
405
+ }
373
406
  setChatLocale(chatId, locale) {
374
407
  const current = this.getChatSettings(chatId);
375
408
  this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, locale, current?.accessPreset ?? null, current?.collaborationMode ?? null, current?.serviceTier ?? null, current?.activeTurnMessageMode ?? null, current?.activeBackendId ?? null);
@@ -9,6 +9,7 @@ export interface TelegramRemoteFile {
9
9
  file_size?: number;
10
10
  file_path?: string;
11
11
  }
12
+ export declare function isTransientNetworkError(err: unknown): boolean;
12
13
  export declare function callTelegramApi<T>(botToken: string, method: string, body: Record<string, unknown>): Promise<TelegramApiResult<T>>;
13
14
  export declare function callTelegramMultipartApi<T>(botToken: string, method: string, fields: Record<string, string>, files: Array<{
14
15
  fieldName: string;
@@ -4,16 +4,28 @@ import https from 'node:https';
4
4
  import path from 'node:path';
5
5
  import { pipeline } from 'node:stream/promises';
6
6
  const DEFAULT_API_BASE_URL = 'https://api.telegram.org';
7
- function isTransientNetworkError(err) {
7
+ export function isTransientNetworkError(err) {
8
8
  if (!err)
9
9
  return false;
10
- const msg = String(err.message || err);
11
- return (msg.includes('Client network socket disconnected') ||
12
- msg.includes('ECONNRESET') ||
13
- msg.includes('ETIMEDOUT') ||
14
- msg.includes('EAI_AGAIN') ||
15
- msg.includes('ENOTFOUND') ||
16
- msg.includes('socket hang up'));
10
+ const msg = String(err.message || err).toLowerCase();
11
+ return (msg.includes('client network socket disconnected') ||
12
+ msg.includes('econnreset') ||
13
+ msg.includes('etimedout') ||
14
+ msg.includes('eai_again') ||
15
+ msg.includes('enotfound') ||
16
+ msg.includes('econnrefused') ||
17
+ msg.includes('socket hang up') ||
18
+ msg.includes('timed out') ||
19
+ msg.includes('timeout') ||
20
+ msg.includes('network socket disconnected') ||
21
+ msg.includes('network error') ||
22
+ msg.includes('fetch failed') ||
23
+ msg.includes('und_err_') ||
24
+ msg.includes('read tcp') ||
25
+ msg.includes('connection timed out') ||
26
+ msg.includes('broken pipe') ||
27
+ msg.includes('ehostunreach') ||
28
+ msg.includes('enetunreach'));
17
29
  }
18
30
  function callTelegramApiOnce(botToken, method, body) {
19
31
  const payload = JSON.stringify(body);
@@ -51,7 +63,7 @@ function callTelegramApiOnce(botToken, method, body) {
51
63
  });
52
64
  }
53
65
  export async function callTelegramApi(botToken, method, body) {
54
- const maxAttempts = method === 'getUpdates' ? 1 : 3;
66
+ const maxAttempts = method === 'getUpdates' ? 1 : 4;
55
67
  let lastError;
56
68
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
57
69
  try {
@@ -60,7 +72,7 @@ export async function callTelegramApi(botToken, method, body) {
60
72
  catch (err) {
61
73
  lastError = err;
62
74
  if (attempt < maxAttempts && isTransientNetworkError(err)) {
63
- await new Promise((r) => setTimeout(r, 400 * attempt));
75
+ await new Promise((r) => setTimeout(r, Math.min(3000, 500 * Math.pow(2, attempt - 1))));
64
76
  continue;
65
77
  }
66
78
  throw err;
@@ -68,7 +80,7 @@ export async function callTelegramApi(botToken, method, body) {
68
80
  }
69
81
  throw lastError;
70
82
  }
71
- export async function callTelegramMultipartApi(botToken, method, fields, files) {
83
+ function callTelegramMultipartApiOnce(botToken, method, fields, files) {
72
84
  const boundary = `foxclaw-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
73
85
  const parts = [];
74
86
  for (const [name, value] of Object.entries(fields)) {
@@ -114,6 +126,24 @@ export async function callTelegramMultipartApi(botToken, method, fields, files)
114
126
  request.end();
115
127
  });
116
128
  }
129
+ export async function callTelegramMultipartApi(botToken, method, fields, files) {
130
+ const maxAttempts = 3;
131
+ let lastError;
132
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
133
+ try {
134
+ return await callTelegramMultipartApiOnce(botToken, method, fields, files);
135
+ }
136
+ catch (err) {
137
+ lastError = err;
138
+ if (attempt < maxAttempts && isTransientNetworkError(err)) {
139
+ await new Promise((r) => setTimeout(r, Math.min(3000, 500 * Math.pow(2, attempt - 1))));
140
+ continue;
141
+ }
142
+ throw err;
143
+ }
144
+ }
145
+ throw lastError;
146
+ }
117
147
  export async function getTelegramFile(botToken, fileId) {
118
148
  const result = await callTelegramApi(botToken, 'getFile', { file_id: fileId });
119
149
  if (!result.ok || !result.result) {
@@ -121,7 +151,7 @@ export async function getTelegramFile(botToken, fileId) {
121
151
  }
122
152
  return result.result;
123
153
  }
124
- export async function downloadTelegramFile(botToken, remoteFilePath, destinationPath) {
154
+ async function downloadTelegramFileOnce(botToken, remoteFilePath, destinationPath) {
125
155
  await fs.promises.mkdir(path.dirname(destinationPath), { recursive: true });
126
156
  const tempPath = `${destinationPath}.tmp-${process.pid}-${Date.now()}`;
127
157
  let response = null;
@@ -158,6 +188,24 @@ export async function downloadTelegramFile(botToken, remoteFilePath, destination
158
188
  response?.destroy();
159
189
  }
160
190
  }
191
+ export async function downloadTelegramFile(botToken, remoteFilePath, destinationPath) {
192
+ const maxAttempts = 3;
193
+ let lastError;
194
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
195
+ try {
196
+ return await downloadTelegramFileOnce(botToken, remoteFilePath, destinationPath);
197
+ }
198
+ catch (err) {
199
+ lastError = err;
200
+ if (attempt < maxAttempts && isTransientNetworkError(err)) {
201
+ await new Promise((r) => setTimeout(r, Math.min(3000, 500 * Math.pow(2, attempt - 1))));
202
+ continue;
203
+ }
204
+ throw err;
205
+ }
206
+ }
207
+ throw lastError;
208
+ }
161
209
  function escapeMultipartName(value) {
162
210
  return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('\r', '').replaceAll('\n', '');
163
211
  }
@@ -5,16 +5,17 @@ const ID_FILE = '.foxclaw-bot.json';
5
5
  export function prepareTelegramBotHome(baseDir, botId, username, sharedHome = null) {
6
6
  if (!/^bot\d+$/.test(botId))
7
7
  throw new Error('Invalid Telegram bot identity');
8
- if (username !== null && !/^[A-Za-z0-9_]{1,64}$/.test(username))
8
+ const normalizedUsername = username ? username.replace(/^@/, '') : null;
9
+ if (normalizedUsername !== null && !/^[A-Za-z0-9_]{1,64}$/.test(normalizedUsername))
9
10
  throw new Error('Invalid Telegram username for directory');
10
- const base = path.resolve(baseDir);
11
- fs.mkdirSync(base, { recursive: true, mode: 0o700 });
11
+ fs.mkdirSync(baseDir, { recursive: true, mode: 0o700 });
12
+ const base = fs.realpathSync(path.resolve(baseDir));
12
13
  const legacy = path.join(base, botId);
13
14
  const legacyStat = lstat(legacy);
14
15
  const source = legacyStat?.isSymbolicLink() ? fs.realpathSync(legacy) : legacy;
15
16
  if (path.dirname(source) !== base)
16
17
  throw new Error(`Bot directory points outside its storage root: ${legacy}`);
17
- const target = username ? path.join(base, `@${username}`) : source;
18
+ const target = normalizedUsername ? path.join(base, `@${normalizedUsername}`) : source;
18
19
  const sourceStat = lstat(source);
19
20
  const targetStat = lstat(target);
20
21
  if (sourceStat && !sourceStat.isDirectory())
package/dist/types.d.ts CHANGED
@@ -403,7 +403,9 @@ export interface RuntimeStatus {
403
403
  activeTurns: number;
404
404
  runtimeKind?: 'default' | 'isolated';
405
405
  currentAuth?: string | null;
406
+ defaultBackend?: 'codex' | 'antigravity';
406
407
  codexHome?: string | null;
408
+ antigravityHome?: string | null;
407
409
  codexAppServer?: RuntimeStatus['codexAppServer'];
408
410
  }>;
409
411
  weixinRuntime?: {
package/dist/update.js CHANGED
@@ -33,7 +33,23 @@ export function inferPnpmFallbackPackageFromEntryPoint(entryPoint) {
33
33
  return 'pnpm@latest';
34
34
  }
35
35
  export function resolveSelfUpdateInstaller(entryPoint, nodePath = process.execPath, exists = fs.existsSync, env = process.env) {
36
- const pnpmHome = inferPnpmHomeFromEntryPoint(entryPoint);
36
+ let pnpmHome = inferPnpmHomeFromEntryPoint(entryPoint);
37
+ const normalizedEntryPoint = entryPoint.replace(/\\/g, '/');
38
+ const isExplicitNpm = normalizedEntryPoint.includes('/node_modules/@foxden-app/foxclaw/')
39
+ && !normalizedEntryPoint.includes('/.pnpm/')
40
+ && !normalizedEntryPoint.includes('/global/');
41
+ if (!pnpmHome && !isExplicitNpm) {
42
+ const candidatePnpmHome = env.PNPM_HOME?.trim() || (process.platform === 'win32'
43
+ ? (env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, 'pnpm') : null)
44
+ : path.join(os.homedir(), '.local', 'share', 'pnpm'));
45
+ if (candidatePnpmHome && exists(candidatePnpmHome)) {
46
+ const foxclawBin = path.join(candidatePnpmHome, process.platform === 'win32' ? 'foxclaw.cmd' : 'foxclaw');
47
+ const globalDir = path.join(candidatePnpmHome, 'global');
48
+ if (exists(foxclawBin) || exists(globalDir)) {
49
+ pnpmHome = candidatePnpmHome;
50
+ }
51
+ }
52
+ }
37
53
  if (pnpmHome) {
38
54
  const npmCommandName = process.platform === 'win32' ? 'npm.cmd' : 'npm';
39
55
  const npmCommand = executableCandidates(npmCommandName, nodePath, env)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "Foxden local execution claw for controlling Codex, OpenCode, and Antigravity from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",