@foxden-app/foxclaw 0.3.17 → 0.3.19

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 (42) hide show
  1. package/.env.example +5 -2
  2. package/README.md +14 -4
  3. package/README_EN.md +14 -4
  4. package/dist/auth/mirror.d.ts +25 -0
  5. package/dist/auth/mirror.js +199 -0
  6. package/dist/codex_app/client.d.ts +2 -1
  7. package/dist/codex_app/client.js +10 -2
  8. package/dist/codex_app/local_usage.d.ts +7 -6
  9. package/dist/codex_app/local_usage.js +55 -66
  10. package/dist/config.d.ts +7 -0
  11. package/dist/config.js +18 -1
  12. package/dist/controller/controller.d.ts +14 -2
  13. package/dist/controller/controller.js +110 -34
  14. package/dist/core/bridge_scope.d.ts +5 -2
  15. package/dist/core/bridge_scope.js +7 -3
  16. package/dist/i18n.d.ts +4 -4
  17. package/dist/i18n.js +4 -4
  18. package/dist/main.js +144 -9
  19. package/dist/store/database.d.ts +4 -2
  20. package/dist/store/database.js +42 -6
  21. package/dist/telegram/addressing.d.ts +1 -0
  22. package/dist/telegram/addressing.js +3 -0
  23. package/dist/telegram/gateway.d.ts +4 -1
  24. package/dist/telegram/gateway.js +23 -5
  25. package/dist/types.d.ts +7 -0
  26. package/dist/update.d.ts +5 -0
  27. package/dist/update.js +93 -5
  28. package/docs/agent-assisted-install.md +4 -4
  29. package/docs/install-for-beginners.md +3 -3
  30. package/docs/troubleshooting.md +1 -1
  31. package/docs/user-manual.md +11 -11
  32. package/docs/zh/agent-assisted-install.md +4 -4
  33. package/docs/zh/foxclaw-skill.md +1 -1
  34. package/docs/zh/install-for-beginners.md +3 -3
  35. package/docs/zh/troubleshooting.md +1 -1
  36. package/docs/zh/user-manual.md +11 -11
  37. package/package.json +1 -1
  38. package/skills/foxclaw/SKILL.md +26 -19
  39. package/skills/foxclaw/references/telegram-setup.md +5 -4
  40. package/skills/foxclaw/scripts/bootstrap_host.py +11 -8
  41. package/skills/foxclaw/scripts/bootstrap_remote.py +8 -4
  42. package/skills/npm-publish/SKILL.md +3 -0
package/dist/main.js CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs';
3
+ import os from 'node:os';
3
4
  import path from 'node:path';
4
5
  import process from 'node:process';
5
6
  import { createInterface } from 'node:readline/promises';
6
7
  import { spawnSync } from 'node:child_process';
7
8
  import { fileURLToPath } from 'node:url';
8
- import { APP_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, getLoadedEnvPath, loadConfig, loadEnv, } from './config.js';
9
+ import { APP_HOME, DEFAULT_CODEX_TELEGRAM_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, getLoadedEnvPath, loadConfig, loadEnv, } from './config.js';
9
10
  import { acquireProcessLock, LockHeldError } from './lock.js';
10
11
  import { readRuntimeStatus, writeRuntimeStatus } from './runtime.js';
11
12
  import { refreshFoxclawExecStartDropIns, removeFoxclawExecStartDropIns } from './systemd.js';
@@ -61,6 +62,9 @@ async function main() {
61
62
  entryPoint,
62
63
  nodePath: process.execPath,
63
64
  version: readPackageVersion(),
65
+ ...(process.env.CODEX_CLI_BIN || resolveCommand('codex')
66
+ ? { codexCliBin: process.env.CODEX_CLI_BIN || resolveCommand('codex') }
67
+ : {}),
64
68
  ...(notificationFile ? { notificationFile } : {}),
65
69
  };
66
70
  const outcome = performSelfUpdate(options);
@@ -137,7 +141,7 @@ Usage:
137
141
  foxclaw --help`);
138
142
  }
139
143
  async function runServeCli() {
140
- const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter },] = await Promise.all([
144
+ const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter }, { AuthCandidateMirror },] = await Promise.all([
141
145
  import('./channels/bridge_messaging_router.js'),
142
146
  import('./channels/telegram/telegram_messaging_port.js'),
143
147
  import('./channels/weixin/weixin_channel_adapter.js'),
@@ -150,6 +154,7 @@ async function runServeCli() {
150
154
  import('./codex_app/client.js'),
151
155
  import('./controller/controller.js'),
152
156
  import('./channels/telegram/telegram_channel_adapter.js'),
157
+ import('./auth/mirror.js'),
153
158
  ]);
154
159
  const config = loadConfig();
155
160
  const logger = new Logger(config.logLevel, config.logPath);
@@ -157,8 +162,132 @@ async function runServeCli() {
157
162
  const processLock = acquireProcessLock(config.lockPath);
158
163
  let store = null;
159
164
  let weixinAdapter = null;
165
+ let activeTelegramAdapters = [];
166
+ let managedApps = [];
167
+ let activeAuthMirror = null;
160
168
  try {
161
169
  store = new BridgeStore(config.storePath);
170
+ if (config.tgMultiBotMode) {
171
+ const seeds = [];
172
+ for (const token of config.tgBotTokens) {
173
+ const bot = new TelegramGateway(token, config.tgAllowedUserId, config.tgAllowedChatId, config.telegramPollIntervalMs, store, logger, true);
174
+ const id = await bot.initializeIdentity();
175
+ if (seeds.some((runtime) => runtime.id === id)) {
176
+ throw new Error(`TG_BOT_TOKENS contains duplicate Telegram bot identity: ${id}`);
177
+ }
178
+ const home = path.join(DEFAULT_CODEX_TELEGRAM_HOME, id, 'home');
179
+ fs.mkdirSync(home, { recursive: true, mode: 0o700 });
180
+ const runtimeConfig = {
181
+ ...config,
182
+ tgBotToken: token,
183
+ tgBotTokens: [token],
184
+ tgScopeBotId: id,
185
+ codexAuthDir: home,
186
+ codexHome: home,
187
+ codexAppServerStatePath: path.join(APP_HOME, 'runtime', `codex-app-server-${id}.json`),
188
+ codexAppServerLogPath: path.join(APP_HOME, 'logs', `codex-app-server-${id}.log`),
189
+ };
190
+ const app = new CodexAppClient(runtimeConfig.codexCliBin, runtimeConfig.codexAppLaunchCmd, runtimeConfig.codexAppAutolaunch, runtimeConfig.codexAppServerStatePath, runtimeConfig.codexAppServerLogPath, logger, { CODEX_HOME: home });
191
+ seeds.push({ id, home, config: runtimeConfig, bot, app });
192
+ }
193
+ const canonicalAuthDir = config.codexAuthDir ?? config.codexHome ?? path.join(os.homedir(), '.codex');
194
+ const mirror = new AuthCandidateMirror(canonicalAuthDir, seeds.map((runtime) => ({
195
+ id: runtime.id,
196
+ authDir: runtime.home,
197
+ notify: async (message) => {
198
+ const chatId = store.getTelegramPrivateChatId(runtime.id);
199
+ if (chatId) {
200
+ await runtime.bot.sendMessage(chatId, message);
201
+ }
202
+ },
203
+ })), logger);
204
+ await mirror.initialize();
205
+ mirror.start();
206
+ activeAuthMirror = mirror;
207
+ managedApps = seeds.map((runtime) => runtime.app);
208
+ const selfUpdater = createSelfUpdateRuntime({
209
+ entryPoint,
210
+ nodePath: process.execPath,
211
+ version: readPackageVersion(),
212
+ statusPath: config.statusPath,
213
+ logPath: path.join(APP_HOME, 'logs', 'update.log'),
214
+ codexCliBin: config.codexCliBin,
215
+ });
216
+ const runtimes = [];
217
+ const writeAggregateStatus = (running = true) => {
218
+ const statuses = runtimes.map((runtime) => runtime.core.getRuntimeStatus());
219
+ const first = statuses[0] ?? null;
220
+ writeRuntimeStatus(config.statusPath, {
221
+ running,
222
+ connected: running && statuses.every((status) => status.connected),
223
+ userAgent: first?.userAgent ?? null,
224
+ ...(first?.codexAppServer ? { codexAppServer: first.codexAppServer } : {}),
225
+ botUsername: first?.botUsername ?? null,
226
+ currentBindings: store.countBindings(),
227
+ pendingApprovals: store.countPendingApprovals(),
228
+ pendingUserInputs: store.countPendingUserInputs(),
229
+ activeTurns: statuses.reduce((sum, status) => sum + status.activeTurns, 0),
230
+ lastError: statuses.find((status) => status.lastError)?.lastError ?? null,
231
+ updatedAt: new Date().toISOString(),
232
+ channels: { telegram: running, weixin: running && config.wxEnabled },
233
+ bots: runtimes.map((runtime, index) => ({
234
+ id: runtime.id,
235
+ username: statuses[index]?.botUsername ?? runtime.bot.username,
236
+ connected: running && Boolean(statuses[index]?.connected),
237
+ activeTurns: running ? (statuses[index]?.activeTurns ?? 0) : 0,
238
+ ...(statuses[index]?.codexAppServer ? { codexAppServer: statuses[index].codexAppServer } : {}),
239
+ })),
240
+ });
241
+ };
242
+ const coordinator = {
243
+ canSelfUpdate: () => runtimes.every((runtime) => runtime.core.isIdleForServiceUpdate()),
244
+ authCandidateUpdated: (runtimeId, candidateName) => mirror.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined),
245
+ statusUpdated: () => writeAggregateStatus(),
246
+ };
247
+ for (const [index, seed] of seeds.entries()) {
248
+ const telegramMessaging = new TelegramMessagingPort(seed.bot);
249
+ const weixinMessaging = index === 0 && config.wxEnabled
250
+ ? new WeixinMessagingPort(store, (id) => loadWeixinAccount(config.weixinAccountsDir, id))
251
+ : null;
252
+ const outbound = new BridgeMessagingRouter(telegramMessaging, weixinMessaging);
253
+ const core = new BridgeSessionCore(seed.config, store, logger, seed.bot, seed.app, outbound, selfUpdater, coordinator);
254
+ runtimes.push({ ...seed, core, telegram: new TelegramChannelAdapter(core) });
255
+ }
256
+ if (config.wxEnabled) {
257
+ weixinAdapter = new WeixinChannelAdapter(runtimes[0].core, store, runtimes[0].config, logger);
258
+ }
259
+ activeTelegramAdapters = runtimes.map((runtime) => runtime.telegram);
260
+ process.on('unhandledRejection', (error) => {
261
+ logger.error('process.unhandled_rejection', { error: serializeError(error) });
262
+ });
263
+ process.on('uncaughtException', (error) => {
264
+ logger.error('process.uncaught_exception', { error: serializeError(error) });
265
+ });
266
+ for (const runtime of runtimes) {
267
+ await runtime.telegram.start();
268
+ }
269
+ if (weixinAdapter) {
270
+ await weixinAdapter.start();
271
+ }
272
+ writeAggregateStatus();
273
+ logger.info('bridge.started', { bots: runtimes.map((runtime) => runtime.id) });
274
+ const shutdown = async (signal) => {
275
+ logger.info('bridge.shutting_down', { signal });
276
+ mirror.stop();
277
+ await weixinAdapter?.stop();
278
+ await Promise.all(runtimes.map((runtime) => runtime.telegram.stop()));
279
+ writeAggregateStatus(false);
280
+ await Promise.all(runtimes.map((runtime) => runtime.app.stop({ terminateServer: true }).catch((error) => {
281
+ logger.warn('codex.app-server.stop_failed', { runtimeId: runtime.id, error: serializeError(error) });
282
+ })));
283
+ store?.close();
284
+ processLock.release();
285
+ process.exit(0);
286
+ };
287
+ process.on('SIGINT', () => void shutdown('SIGINT'));
288
+ process.on('SIGTERM', () => void shutdown('SIGTERM'));
289
+ return;
290
+ }
162
291
  const bot = new TelegramGateway(config.tgBotToken, config.tgAllowedUserId, config.tgAllowedChatId, config.telegramPollIntervalMs, store, logger);
163
292
  const app = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger);
164
293
  const telegramMessaging = new TelegramMessagingPort(bot);
@@ -172,9 +301,12 @@ async function runServeCli() {
172
301
  version: readPackageVersion(),
173
302
  statusPath: config.statusPath,
174
303
  logPath: path.join(APP_HOME, 'logs', 'update.log'),
304
+ codexCliBin: config.codexCliBin,
175
305
  });
176
306
  const core = new BridgeSessionCore(config, store, logger, bot, app, outbound, selfUpdater);
177
307
  const telegram = new TelegramChannelAdapter(core);
308
+ managedApps = [app];
309
+ activeTelegramAdapters = [telegram];
178
310
  if (config.wxEnabled) {
179
311
  weixinAdapter = new WeixinChannelAdapter(core, store, config, logger);
180
312
  }
@@ -218,7 +350,10 @@ async function runServeCli() {
218
350
  process.on('SIGTERM', () => void shutdown('SIGTERM'));
219
351
  }
220
352
  catch (error) {
353
+ activeAuthMirror?.stop();
221
354
  await weixinAdapter?.stop().catch(() => { });
355
+ await Promise.allSettled(activeTelegramAdapters.map((adapter) => adapter.stop()));
356
+ await Promise.allSettled(managedApps.map((app) => app.stop({ terminateServer: true })));
222
357
  store?.close();
223
358
  processLock.release();
224
359
  throw error;
@@ -264,15 +399,15 @@ async function configureEnvInteractively(envPath, existed) {
264
399
  const skipped = [];
265
400
  const warnings = [];
266
401
  Object.assign(updates, await maybeSaveProxyEnvFromShell(rl, envPath));
267
- const token = sanitizeEnvInput(await rl.question('Telegram bot token (TG_BOT_TOKEN): '));
268
- if (token) {
269
- updates.TG_BOT_TOKEN = token;
270
- if (!/^\d+:[A-Za-z0-9_-]+$/.test(token)) {
271
- warnings.push('TG_BOT_TOKEN does not look like a standard Telegram bot token.');
402
+ const tokens = sanitizeEnvInput(await rl.question('Telegram bot token(s), comma-separated (TG_BOT_TOKENS): '));
403
+ if (tokens) {
404
+ updates.TG_BOT_TOKENS = tokens;
405
+ if (tokens.split(',').map((token) => token.trim()).some((token) => !/^\d+:[A-Za-z0-9_-]+$/.test(token))) {
406
+ warnings.push('One or more TG_BOT_TOKENS values do not look like standard Telegram bot tokens.');
272
407
  }
273
408
  }
274
409
  else {
275
- skipped.push('TG_BOT_TOKEN');
410
+ skipped.push('TG_BOT_TOKENS');
276
411
  }
277
412
  const userId = sanitizeEnvInput(await rl.question('Telegram numeric user ID (TG_ALLOWED_USER_ID): '));
278
413
  if (userId) {
@@ -486,7 +621,7 @@ function runDoctorChecks() {
486
621
  const checks = [
487
622
  ['node >= 24', Number(process.versions.node.split('.')[0]) >= 24],
488
623
  ['codex cli available', hasConfiguredCodexBin(configuredCodexBin) || hasCommand('codex')],
489
- ['telegram bot token configured', Boolean(process.env.TG_BOT_TOKEN)],
624
+ ['telegram bot token(s) configured', Boolean(process.env.TG_BOT_TOKENS?.trim() || process.env.TG_BOT_TOKEN?.trim())],
490
625
  ['telegram allowed user configured', Boolean(process.env.TG_ALLOWED_USER_ID)],
491
626
  ];
492
627
  if (process.env.WX_ENABLED === 'true' || process.env.WX_ENABLED === '1') {
@@ -29,6 +29,8 @@ export declare class BridgeStore {
29
29
  constructor(dbPath: string);
30
30
  getTelegramOffset(botKey: string): number;
31
31
  setTelegramOffset(botKey: string, updateId: number): void;
32
+ rememberTelegramPrivateScope(botId: string, scopeId: string, chatId: string): void;
33
+ getTelegramPrivateChatId(botId: string): string | null;
32
34
  getBinding(chatId: string): ThreadBinding | null;
33
35
  setBinding(chatId: string, threadId: string, cwd: string | null): void;
34
36
  clearBinding(chatId: string): void;
@@ -73,7 +75,7 @@ export declare class BridgeStore {
73
75
  private writeChatSettings;
74
76
  getWeixinContextToken(scopeId: string): string | null;
75
77
  setWeixinContextToken(scopeId: string, contextToken: string): void;
76
- listDisabledCodexAuthCandidateNames(): Set<string>;
77
- setCodexAuthCandidateDisabled(name: string, disabled: boolean): void;
78
+ listDisabledCodexAuthCandidateNames(runtimeId?: string): Set<string>;
79
+ setCodexAuthCandidateDisabled(name: string, disabled: boolean, runtimeId?: string): void;
78
80
  private ensureColumn;
79
81
  }
@@ -8,10 +8,16 @@ export class BridgeStore {
8
8
  fs.mkdirSync(path.dirname(dbPath), { recursive: true });
9
9
  this.db = new DatabaseSync(dbPath);
10
10
  this.db.exec(`
11
- CREATE TABLE IF NOT EXISTS telegram_offsets (
12
- bot_key TEXT PRIMARY KEY,
13
- update_id INTEGER NOT NULL
14
- );
11
+ CREATE TABLE IF NOT EXISTS telegram_offsets (
12
+ bot_key TEXT PRIMARY KEY,
13
+ update_id INTEGER NOT NULL
14
+ );
15
+ CREATE TABLE IF NOT EXISTS telegram_private_scopes (
16
+ bot_id TEXT PRIMARY KEY,
17
+ scope_id TEXT NOT NULL,
18
+ chat_id TEXT NOT NULL,
19
+ updated_at INTEGER NOT NULL
20
+ );
15
21
  CREATE TABLE IF NOT EXISTS chat_bindings (
16
22
  chat_id TEXT PRIMARY KEY,
17
23
  thread_id TEXT NOT NULL,
@@ -110,6 +116,13 @@ export class BridgeStore {
110
116
  disabled INTEGER NOT NULL DEFAULT 0,
111
117
  updated_at INTEGER NOT NULL
112
118
  );
119
+ CREATE TABLE IF NOT EXISTS codex_auth_candidate_runtime (
120
+ runtime_id TEXT NOT NULL,
121
+ name TEXT NOT NULL,
122
+ disabled INTEGER NOT NULL DEFAULT 0,
123
+ updated_at INTEGER NOT NULL,
124
+ PRIMARY KEY (runtime_id, name)
125
+ );
113
126
  `);
114
127
  this.ensureColumn('thread_cache', 'name', 'TEXT');
115
128
  this.ensureColumn('thread_cache', 'model_provider', 'TEXT');
@@ -136,6 +149,17 @@ export class BridgeStore {
136
149
  ON CONFLICT(bot_key) DO UPDATE SET update_id = excluded.update_id
137
150
  `).run(botKey, updateId);
138
151
  }
152
+ rememberTelegramPrivateScope(botId, scopeId, chatId) {
153
+ this.db.prepare(`
154
+ INSERT INTO telegram_private_scopes (bot_id, scope_id, chat_id, updated_at)
155
+ VALUES (?, ?, ?, ?)
156
+ ON CONFLICT(bot_id) DO UPDATE SET scope_id = excluded.scope_id, chat_id = excluded.chat_id, updated_at = excluded.updated_at
157
+ `).run(botId, scopeId, chatId, Date.now());
158
+ }
159
+ getTelegramPrivateChatId(botId) {
160
+ const row = this.db.prepare('SELECT chat_id FROM telegram_private_scopes WHERE bot_id = ?').get(botId);
161
+ return row ? String(row.chat_id) : null;
162
+ }
139
163
  getBinding(chatId) {
140
164
  const row = this.db.prepare('SELECT chat_id, thread_id, cwd, updated_at FROM chat_bindings WHERE chat_id = ?').get(chatId);
141
165
  if (!row)
@@ -462,11 +486,23 @@ export class BridgeStore {
462
486
  ON CONFLICT(scope_id) DO UPDATE SET context_token = excluded.context_token, updated_at = excluded.updated_at
463
487
  `).run(scopeId, contextToken, Date.now());
464
488
  }
465
- listDisabledCodexAuthCandidateNames() {
489
+ listDisabledCodexAuthCandidateNames(runtimeId = 'default') {
490
+ if (runtimeId !== 'default') {
491
+ const runtimeRows = this.db.prepare('SELECT name FROM codex_auth_candidate_runtime WHERE runtime_id = ? AND disabled = 1').all(runtimeId);
492
+ return new Set(runtimeRows.map(row => String(row.name)));
493
+ }
466
494
  const rows = this.db.prepare('SELECT name FROM codex_auth_candidates WHERE disabled = 1').all();
467
495
  return new Set(rows.map(row => String(row.name)));
468
496
  }
469
- setCodexAuthCandidateDisabled(name, disabled) {
497
+ setCodexAuthCandidateDisabled(name, disabled, runtimeId = 'default') {
498
+ if (runtimeId !== 'default') {
499
+ this.db.prepare(`
500
+ INSERT INTO codex_auth_candidate_runtime (runtime_id, name, disabled, updated_at)
501
+ VALUES (?, ?, ?, ?)
502
+ ON CONFLICT(runtime_id, name) DO UPDATE SET disabled = excluded.disabled, updated_at = excluded.updated_at
503
+ `).run(runtimeId, name, disabled ? 1 : 0, Date.now());
504
+ return;
505
+ }
470
506
  this.db.prepare(`
471
507
  INSERT INTO codex_auth_candidates (name, disabled, updated_at)
472
508
  VALUES (?, ?, ?)
@@ -27,6 +27,7 @@ interface DefaultScopeParams {
27
27
  allowedChatId: string | null;
28
28
  allowedTopicId: number | null;
29
29
  topicId: number | null;
30
+ requireExplicitGroupAddressing?: boolean;
30
31
  }
31
32
  export declare function resolveTelegramAddressing(params: ResolveTelegramAddressingParams): TelegramAddressingDecision;
32
33
  export declare function isDefaultTelegramScope(params: DefaultScopeParams): boolean;
@@ -21,6 +21,9 @@ export function isDefaultTelegramScope(params) {
21
21
  if (params.chatType === 'private') {
22
22
  return true;
23
23
  }
24
+ if (params.requireExplicitGroupAddressing) {
25
+ return false;
26
+ }
24
27
  if (params.allowedChatId === null) {
25
28
  return false;
26
29
  }
@@ -34,12 +34,15 @@ export declare class TelegramGateway extends EventEmitter {
34
34
  private readonly pollIntervalMs;
35
35
  private readonly store;
36
36
  private readonly logger;
37
+ private readonly namespacedScopes;
37
38
  private running;
38
39
  private botKey;
39
40
  private botUsername;
40
41
  private botUserId;
41
- constructor(botToken: string, allowedUserId: string, allowedChatId: string | null, pollIntervalMs: number, store: BridgeStore, logger: Logger);
42
+ constructor(botToken: string, allowedUserId: string, allowedChatId: string | null, pollIntervalMs: number, store: BridgeStore, logger: Logger, namespacedScopes?: boolean);
42
43
  get username(): string | null;
44
+ get identity(): string | null;
45
+ initializeIdentity(): Promise<string>;
43
46
  start(): Promise<void>;
44
47
  stop(): void;
45
48
  sendMessage(chatId: string, text: string, inlineKeyboard?: Array<Array<{
@@ -11,11 +11,12 @@ export class TelegramGateway extends EventEmitter {
11
11
  pollIntervalMs;
12
12
  store;
13
13
  logger;
14
+ namespacedScopes;
14
15
  running = false;
15
16
  botKey;
16
17
  botUsername = null;
17
18
  botUserId = null;
18
- constructor(botToken, allowedUserId, allowedChatId, pollIntervalMs, store, logger) {
19
+ constructor(botToken, allowedUserId, allowedChatId, pollIntervalMs, store, logger, namespacedScopes = false) {
19
20
  super();
20
21
  this.botToken = botToken;
21
22
  this.allowedUserId = allowedUserId;
@@ -23,16 +24,26 @@ export class TelegramGateway extends EventEmitter {
23
24
  this.pollIntervalMs = pollIntervalMs;
24
25
  this.store = store;
25
26
  this.logger = logger;
27
+ this.namespacedScopes = namespacedScopes;
26
28
  this.botKey = `telegram:${crypto.createHash('sha256').update(this.botToken).digest('hex').slice(0, 8)}`;
27
29
  }
28
30
  get username() {
29
31
  return this.botUsername;
30
32
  }
33
+ get identity() {
34
+ return this.botUserId === null ? null : `bot${this.botUserId}`;
35
+ }
36
+ async initializeIdentity() {
37
+ await this.resolveBotIdentity(true);
38
+ return this.identity;
39
+ }
31
40
  async start() {
32
41
  if (this.running)
33
42
  return;
34
43
  this.running = true;
35
- await this.resolveBotIdentity();
44
+ if (this.botUserId === null) {
45
+ await this.resolveBotIdentity(this.namespacedScopes);
46
+ }
36
47
  await this.registerCommands();
37
48
  void this.pollLoop();
38
49
  }
@@ -134,12 +145,16 @@ export class TelegramGateway extends EventEmitter {
134
145
  async downloadResolvedFile(remoteFilePath, destinationPath) {
135
146
  return downloadTelegramFile(this.botToken, remoteFilePath, destinationPath);
136
147
  }
137
- async resolveBotIdentity() {
148
+ async resolveBotIdentity(required = false) {
138
149
  const result = await callTelegramApi(this.botToken, 'getMe', {});
139
150
  if (result.ok && result.result) {
140
151
  this.botKey = `telegram:bot${result.result.id}`;
141
152
  this.botUserId = result.result.id;
142
153
  this.botUsername = result.result.username ?? null;
154
+ return;
155
+ }
156
+ if (required) {
157
+ throw new Error(result.description || 'Failed to resolve Telegram bot identity');
143
158
  }
144
159
  }
145
160
  async registerCommands() {
@@ -187,7 +202,7 @@ export class TelegramGateway extends EventEmitter {
187
202
  const attachments = extractAttachments(update.message);
188
203
  const text = update.message.text ?? update.message.caption ?? '';
189
204
  const topicId = update.message.message_thread_id ?? null;
190
- const scopeId = toTelegramBridgeScopeId(createTelegramScopeId(String(update.message.chat.id), topicId));
205
+ const scopeId = toTelegramBridgeScopeId(createTelegramScopeId(String(update.message.chat.id), topicId), this.namespacedScopes ? this.identity : null);
191
206
  const entities = update.message.text ? (update.message.entities ?? []) : (update.message.caption_entities ?? []);
192
207
  const replyToBot = this.botUserId !== null && update.message.reply_to_message?.from?.id === this.botUserId;
193
208
  if (text || attachments.length > 0) {
@@ -204,6 +219,9 @@ export class TelegramGateway extends EventEmitter {
204
219
  replyToBot,
205
220
  ...(update.message.from.language_code ? { languageCode: update.message.from.language_code } : {}),
206
221
  });
222
+ if (update.message.chat.type === 'private' && this.identity) {
223
+ this.store.rememberTelegramPrivateScope(this.identity, scopeId, String(update.message.chat.id));
224
+ }
207
225
  return;
208
226
  }
209
227
  }
@@ -216,7 +234,7 @@ export class TelegramGateway extends EventEmitter {
216
234
  this.emit('callback', {
217
235
  chatId: String(update.callback_query.message.chat.id),
218
236
  topicId,
219
- scopeId: toTelegramBridgeScopeId(createTelegramScopeId(String(update.callback_query.message.chat.id), topicId)),
237
+ scopeId: toTelegramBridgeScopeId(createTelegramScopeId(String(update.callback_query.message.chat.id), topicId), this.namespacedScopes ? this.identity : null),
220
238
  userId: String(update.callback_query.from.id),
221
239
  data: update.callback_query.data,
222
240
  callbackQueryId: update.callback_query.id,
package/dist/types.d.ts CHANGED
@@ -340,4 +340,11 @@ export interface RuntimeStatus {
340
340
  telegram: boolean;
341
341
  weixin: boolean;
342
342
  };
343
+ bots?: Array<{
344
+ id: string;
345
+ username: string | null;
346
+ connected: boolean;
347
+ activeTurns: number;
348
+ codexAppServer?: RuntimeStatus['codexAppServer'];
349
+ }>;
343
350
  }
package/dist/update.d.ts CHANGED
@@ -6,6 +6,7 @@ export interface SelfUpdateStatus {
6
6
  locale: AppLocale;
7
7
  fromVersion: string;
8
8
  toVersion: string | null;
9
+ codexUpdate?: string | null;
9
10
  error: string | null;
10
11
  updatedAt: string;
11
12
  }
@@ -19,6 +20,7 @@ export interface SelfUpdateInstaller {
19
20
  command: string;
20
21
  installArgs: string[];
21
22
  rootArgs: string[];
23
+ pnpmHome?: string;
22
24
  }
23
25
  interface CreateSelfUpdateRuntimeOptions {
24
26
  entryPoint: string;
@@ -26,12 +28,14 @@ interface CreateSelfUpdateRuntimeOptions {
26
28
  version: string;
27
29
  statusPath: string;
28
30
  logPath: string;
31
+ codexCliBin?: string;
29
32
  }
30
33
  interface PerformSelfUpdateOptions {
31
34
  entryPoint: string;
32
35
  nodePath: string;
33
36
  version: string;
34
37
  notificationFile?: string;
38
+ codexCliBin?: string;
35
39
  env?: NodeJS.ProcessEnv;
36
40
  }
37
41
  export interface SelfUpdateOutcome {
@@ -43,6 +47,7 @@ export interface SelfUpdateOutcome {
43
47
  export declare function selfUpdateStatusPath(statusPath: string): string;
44
48
  export declare function inferPnpmHomeFromEntryPoint(entryPoint: string): string | null;
45
49
  export declare function resolveSelfUpdateInstaller(entryPoint: string, nodePath?: string, exists?: (target: string) => boolean, env?: NodeJS.ProcessEnv): SelfUpdateInstaller;
50
+ export declare function resolveCodexUpdateInstaller(codexCliBin: string, nodePath?: string, exists?: (target: string) => boolean, env?: NodeJS.ProcessEnv, realpath?: (target: string) => string, readText?: (target: string) => string): SelfUpdateInstaller | null;
46
51
  export declare function readSelfUpdateStatus(statusFile: string): SelfUpdateStatus | null;
47
52
  export declare function writeSelfUpdateStatus(statusFile: string, status: SelfUpdateStatus): void;
48
53
  export declare function createSelfUpdateRuntime(options: CreateSelfUpdateRuntimeOptions): SelfUpdateRuntime;