@llblab/pi-telegram 0.19.3 → 0.20.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.
@@ -99,31 +99,55 @@ export function getTelegramBusTransportRetryPolicy(input: {
99
99
  };
100
100
  }
101
101
 
102
+ function normalizeTelegramBusEndpointScope(value: string): string {
103
+ return value.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 80);
104
+ }
105
+
102
106
  export function getTelegramBusLeaderEndpoint(input: {
103
107
  agentDir: string;
104
108
  platform: NodeJS.Platform | string;
109
+ profileName?: string;
105
110
  }): string {
111
+ const profileScope = input.profileName
112
+ ? normalizeTelegramBusEndpointScope(input.profileName)
113
+ : undefined;
106
114
  return input.platform === "win32"
107
- ? getTelegramBusPipePath({ agentDir: input.agentDir, scope: "bus" })
108
- : join(input.agentDir, "tmp", "telegram", "bus.sock");
115
+ ? getTelegramBusPipePath({
116
+ agentDir: input.agentDir,
117
+ scope: profileScope ? `bus-${profileScope}` : "bus",
118
+ })
119
+ : join(
120
+ input.agentDir,
121
+ "tmp",
122
+ "telegram",
123
+ profileScope ? `bus.${profileScope}.sock` : "bus.sock",
124
+ );
109
125
  }
110
126
 
111
127
  export function getTelegramBusFollowerEndpoint(input: {
112
128
  agentDir: string;
113
129
  platform: NodeJS.Platform | string;
114
130
  instanceId: string;
131
+ profileName?: string;
115
132
  }): string {
133
+ const instanceScope = normalizeTelegramBusEndpointScope(input.instanceId);
134
+ const profileScope = input.profileName
135
+ ? normalizeTelegramBusEndpointScope(input.profileName)
136
+ : undefined;
116
137
  return input.platform === "win32"
117
138
  ? getTelegramBusPipePath({
118
139
  agentDir: input.agentDir,
119
- scope: `follower-${input.instanceId}`,
140
+ scope: profileScope
141
+ ? `follower-${profileScope}-${instanceScope}`
142
+ : `follower-${instanceScope}`,
120
143
  })
121
144
  : join(
122
145
  input.agentDir,
123
146
  "tmp",
124
147
  "telegram",
125
148
  "followers",
126
- `${input.instanceId.replace(/[^a-zA-Z0-9_.-]/g, "_")}.sock`,
149
+ ...(profileScope ? [profileScope] : []),
150
+ `${instanceScope}.sock`,
127
151
  );
128
152
  }
129
153
 
package/lib/bus.ts CHANGED
@@ -13,8 +13,8 @@ import {
13
13
  type Server,
14
14
  type Socket,
15
15
  } from "node:net";
16
- import { homedir, platform as getPlatform } from "node:os";
17
- import { dirname, join, resolve } from "node:path";
16
+ import { platform as getPlatform } from "node:os";
17
+ import { dirname } from "node:path";
18
18
 
19
19
  import {
20
20
  classifyTelegramBusTransportError,
@@ -30,32 +30,34 @@ import {
30
30
  type TelegramBusTransportRetryPolicy,
31
31
  } from "./bus-transport.ts";
32
32
  import type { TelegramTarget } from "./target.ts";
33
+ import { resolveAgentDir } from "./paths.ts";
33
34
 
34
35
  export type TelegramBusRole = "leader" | "follower";
35
36
 
36
- function getAgentDir(): string {
37
- return process.env.PI_CODING_AGENT_DIR
38
- ? resolve(process.env.PI_CODING_AGENT_DIR)
39
- : join(homedir(), ".pi", "agent");
40
- }
41
-
42
37
  export function createTelegramBusAuthSecret(): string {
43
38
  return randomBytes(32).toString("base64url");
44
39
  }
45
40
 
46
41
  export function getTelegramBusSocketPath(
47
- agentDir = getAgentDir(),
42
+ agentDir = resolveAgentDir(),
48
43
  platform = getPlatform(),
44
+ profileName?: string,
49
45
  ): string {
50
- return getTelegramBusLeaderEndpoint({ agentDir, platform });
46
+ return getTelegramBusLeaderEndpoint({ agentDir, platform, profileName });
51
47
  }
52
48
 
53
49
  export function getTelegramBusFollowerSocketPath(
54
50
  instanceId: string,
55
- agentDir = getAgentDir(),
51
+ agentDir = resolveAgentDir(),
56
52
  platform = getPlatform(),
53
+ profileName?: string,
57
54
  ): string {
58
- return getTelegramBusFollowerEndpoint({ agentDir, platform, instanceId });
55
+ return getTelegramBusFollowerEndpoint({
56
+ agentDir,
57
+ platform,
58
+ instanceId,
59
+ profileName,
60
+ });
59
61
  }
60
62
 
61
63
  export interface TelegramBusInstanceRegistration {
@@ -155,7 +157,9 @@ export function isTelegramFollowerApiCallAllowed(input: {
155
157
  const messageId = (body as Record<string, unknown>).message_id;
156
158
  const parsedMessageId =
157
159
  typeof messageId === "number" ? messageId : Number(messageId);
158
- return Number.isInteger(parsedMessageId) && matchesId(messageId, parsedMessageId);
160
+ return (
161
+ Number.isInteger(parsedMessageId) && matchesId(messageId, parsedMessageId)
162
+ );
159
163
  };
160
164
  const isBotCommandRegistration = (body: unknown): boolean => {
161
165
  if (!body || typeof body !== "object" || Array.isArray(body)) return false;
@@ -185,7 +189,8 @@ export function isTelegramFollowerApiCallAllowed(input: {
185
189
  if (apiMethod === "getMe") return true;
186
190
  if (apiMethod === "setMyCommands")
187
191
  return isBotCommandRegistration(input.args[1]);
188
- if (apiMethod === "sendChatAction") return isTargetChatScoped(input.args[1]);
192
+ if (apiMethod === "sendChatAction")
193
+ return isTargetChatScoped(input.args[1]);
189
194
  if (apiMethod === "deleteMessage" || apiMethod === "editMessageText") {
190
195
  return isTargetMessageScoped(input.args[1]);
191
196
  }
@@ -346,14 +351,20 @@ export interface TelegramBusLocalServer {
346
351
  stop: () => Promise<void>;
347
352
  }
348
353
 
354
+ export type TelegramBusSocketPathSource = string | (() => string);
355
+
356
+ export function resolveTelegramBusSocketPath(
357
+ source: TelegramBusSocketPathSource,
358
+ ): string {
359
+ return typeof source === "function" ? source() : source;
360
+ }
361
+
349
362
  export interface TelegramBusLocalServerDeps {
350
- socketPath: string;
363
+ socketPath: TelegramBusSocketPathSource;
351
364
  handleEnvelope: (
352
365
  envelope: TelegramBusEnvelope,
353
366
  ) =>
354
- | Promise<TelegramBusEnvelope | undefined>
355
- | TelegramBusEnvelope
356
- | undefined;
367
+ Promise<TelegramBusEnvelope | undefined> | TelegramBusEnvelope | undefined;
357
368
  recordTransportEvent?: TelegramBusTransportEventRecorder;
358
369
  }
359
370
 
@@ -366,7 +377,7 @@ export interface TelegramBusLocalClientOptions {
366
377
  }
367
378
 
368
379
  export interface TelegramBusForeignOwnedForwarderDeps {
369
- socketPath: string;
380
+ socketPath: TelegramBusSocketPathSource;
370
381
  createRequestId: () => string;
371
382
  getNowMs?: () => number;
372
383
  timeoutMs?: number;
@@ -405,12 +416,13 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
405
416
  const getNowMs = deps.getNowMs ?? Date.now;
406
417
  const send = async (envelope: TelegramBusEnvelope): Promise<boolean> => {
407
418
  if (deps.getAuthSecret) envelope.auth = deps.getAuthSecret();
419
+ const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
408
420
  const response = await sendTelegramBusLocalEnvelope({
409
- socketPath: deps.socketPath,
421
+ socketPath,
410
422
  envelope,
411
423
  timeoutMs: deps.timeoutMs,
412
424
  retry: getTelegramBusTransportRetryPolicy({
413
- endpoint: deps.socketPath,
425
+ endpoint: socketPath,
414
426
  operation: "operation",
415
427
  }),
416
428
  });
@@ -454,7 +466,9 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
454
466
 
455
467
  export interface TelegramBusFollowerThreadRestoreHandlerDeps {
456
468
  followerRegistry: Pick<TelegramBusFollowerRegistry, "get" | "register">;
457
- followerTargetController: ReturnType<typeof createTelegramBusFollowerTargetController>;
469
+ followerTargetController: ReturnType<
470
+ typeof createTelegramBusFollowerTargetController
471
+ >;
458
472
  onRestored?: () => void;
459
473
  }
460
474
 
@@ -517,11 +531,7 @@ export function createTelegramBusFollowerThreadRestoreHandler(
517
531
  target: TelegramTarget & { threadId: number };
518
532
  oldTarget?: TelegramTarget & { threadId: number };
519
533
  }) => Promise<boolean> {
520
- return async ({
521
- record,
522
- target,
523
- oldTarget,
524
- }) => {
534
+ return async ({ record, target, oldTarget }) => {
525
535
  if (!record.instanceId) return false;
526
536
  const follower = deps.followerRegistry.get(record.instanceId);
527
537
  if (!follower) return false;
@@ -564,6 +574,7 @@ export function createTelegramBusLocalServer(
564
574
  deps: TelegramBusLocalServerDeps,
565
575
  ): TelegramBusLocalServer {
566
576
  let server: Server | undefined;
577
+ let activeSocketPath: string | undefined;
567
578
  const sockets = new Set<Socket>();
568
579
  const closeSocket = (socket: Socket) => {
569
580
  sockets.delete(socket);
@@ -572,16 +583,18 @@ export function createTelegramBusLocalServer(
572
583
  return {
573
584
  start: async () => {
574
585
  if (server) return;
575
- const usesWindowsPipe = isTelegramBusPipePath(deps.socketPath);
586
+ const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
587
+ activeSocketPath = socketPath;
588
+ const usesWindowsPipe = isTelegramBusPipePath(socketPath);
576
589
  deps.recordTransportEvent?.(
577
590
  "server-start",
578
- getTelegramBusEndpointDiagnostics(deps.socketPath),
591
+ getTelegramBusEndpointDiagnostics(socketPath),
579
592
  );
580
593
  if (!usesWindowsPipe) {
581
- const socketDir = dirname(deps.socketPath);
594
+ const socketDir = dirname(socketPath);
582
595
  mkdirSync(socketDir, { recursive: true, mode: 0o700 });
583
596
  chmodSync(socketDir, 0o700);
584
- if (existsSync(deps.socketPath)) unlinkSync(deps.socketPath);
597
+ if (existsSync(socketPath)) unlinkSync(socketPath);
585
598
  }
586
599
  server = createServer((socket) => {
587
600
  sockets.add(socket);
@@ -597,14 +610,14 @@ export function createTelegramBusLocalServer(
597
610
  socket,
598
611
  deps.handleEnvelope,
599
612
  deps.recordTransportEvent,
600
- deps.socketPath,
613
+ socketPath,
601
614
  );
602
615
  }
603
616
  });
604
617
  socket.on("close", () => sockets.delete(socket));
605
618
  socket.on("error", (error) => {
606
619
  deps.recordTransportEvent?.("server-socket-error", {
607
- ...getTelegramBusEndpointDiagnostics(deps.socketPath),
620
+ ...getTelegramBusEndpointDiagnostics(socketPath),
608
621
  ...classifyTelegramBusTransportError(error),
609
622
  });
610
623
  closeSocket(socket);
@@ -613,37 +626,47 @@ export function createTelegramBusLocalServer(
613
626
  try {
614
627
  await new Promise<void>((resolve, reject) => {
615
628
  server?.once("error", reject);
616
- server?.listen(deps.socketPath, resolve);
629
+ server?.listen(socketPath, resolve);
617
630
  });
618
631
  deps.recordTransportEvent?.(
619
632
  "server-started",
620
- getTelegramBusEndpointDiagnostics(deps.socketPath),
633
+ getTelegramBusEndpointDiagnostics(socketPath),
621
634
  );
622
635
  } catch (error) {
636
+ server = undefined;
637
+ activeSocketPath = undefined;
623
638
  deps.recordTransportEvent?.("server-start-failed", {
624
- ...getTelegramBusEndpointDiagnostics(deps.socketPath),
639
+ ...getTelegramBusEndpointDiagnostics(socketPath),
625
640
  ...classifyTelegramBusTransportError(error),
626
641
  });
627
642
  throw error;
628
643
  }
629
- if (!usesWindowsPipe) chmodSync(deps.socketPath, 0o600);
644
+ if (!usesWindowsPipe) chmodSync(socketPath, 0o600);
630
645
  },
631
646
  stop: async () => {
632
647
  const activeServer = server;
648
+ const socketPath = activeSocketPath;
633
649
  server = undefined;
650
+ activeSocketPath = undefined;
634
651
  for (const socket of sockets) closeSocket(socket);
635
652
  if (activeServer) {
636
653
  await new Promise<void>((resolve) =>
637
654
  activeServer.close(() => resolve()),
638
655
  );
639
656
  }
640
- if (!isTelegramBusPipePath(deps.socketPath) && existsSync(deps.socketPath)) {
641
- unlinkSync(deps.socketPath);
657
+ if (
658
+ socketPath &&
659
+ !isTelegramBusPipePath(socketPath) &&
660
+ existsSync(socketPath)
661
+ ) {
662
+ unlinkSync(socketPath);
663
+ }
664
+ if (socketPath) {
665
+ deps.recordTransportEvent?.(
666
+ "server-stopped",
667
+ getTelegramBusEndpointDiagnostics(socketPath),
668
+ );
642
669
  }
643
- deps.recordTransportEvent?.(
644
- "server-stopped",
645
- getTelegramBusEndpointDiagnostics(deps.socketPath),
646
- );
647
670
  },
648
671
  };
649
672
  }
package/lib/commands.ts CHANGED
@@ -295,7 +295,7 @@ export interface TelegramBridgeCommandStartPollingResult {
295
295
  }
296
296
 
297
297
  export interface TelegramBridgeCommandRegistrationDeps {
298
- promptForConfig: (ctx: ExtensionCommandContext) => Promise<void>;
298
+ promptForConfig: (ctx: ExtensionCommandContext, profileName?: string) => Promise<void>;
299
299
  getStatusLines: (options?: TelegramBridgeStatusLineOptions) => string[];
300
300
  reloadConfig: () => Promise<void>;
301
301
  hasBotToken: () => boolean;
@@ -308,6 +308,19 @@ export interface TelegramBridgeCommandRegistrationDeps {
308
308
  | TelegramBridgeCommandStartPollingResult;
309
309
  stopPolling: () => Promise<void | string>;
310
310
  updateStatus: (ctx: ExtensionCommandContext) => void;
311
+ getProfileNames?: () => string[];
312
+ activateDefaultProfileConfig?: (ctx: ExtensionCommandContext) => Promise<void>;
313
+ activateProfileConfig?: (
314
+ ctx: ExtensionCommandContext,
315
+ profileName: string,
316
+ ) => Promise<boolean>;
317
+ }
318
+
319
+ function parseTelegramProfileArg(args: string): string | undefined {
320
+ const word = args.trim().split(/\s+/)[0];
321
+ if (!word || word.length === 0) return undefined;
322
+ if (word.startsWith("-")) return undefined;
323
+ return word;
311
324
  }
312
325
 
313
326
  function formatTelegramTakeoverTitle(ctx: ExtensionCommandContext): string {
@@ -331,9 +344,9 @@ export function registerTelegramBridgeCommands(
331
344
  deps: TelegramBridgeCommandRegistrationDeps,
332
345
  ): void {
333
346
  pi.registerCommand("telegram-setup", {
334
- description: "Configure Telegram bot token",
335
- handler: async (_args, ctx) => {
336
- await deps.promptForConfig(ctx);
347
+ description: "Configure Telegram bot token. Use /telegram-setup <name> for named profiles.",
348
+ handler: async (args, ctx) => {
349
+ await deps.promptForConfig(ctx, parseTelegramProfileArg(args));
337
350
  },
338
351
  });
339
352
  pi.registerCommand("telegram-status", {
@@ -346,11 +359,31 @@ export function registerTelegramBridgeCommands(
346
359
  },
347
360
  });
348
361
  pi.registerCommand("telegram-connect", {
349
- description: "Start the Telegram bridge in this Pi session",
350
- handler: async (_args, ctx) => {
351
- await deps.reloadConfig();
362
+ description: "Start the Telegram bridge. Use /telegram-connect <name> for named profiles.",
363
+ handler: async (args, ctx) => {
364
+ const profileName = parseTelegramProfileArg(args);
365
+ if (profileName && deps.activateProfileConfig) {
366
+ const ok = await deps.activateProfileConfig(ctx, profileName);
367
+ if (!ok) {
368
+ ctx.ui.notify(`Profile "${profileName}" not found.`, "error");
369
+ deps.updateStatus(ctx);
370
+ return;
371
+ }
372
+ ctx.ui.notify(`Activated profile "${profileName}".`, "info");
373
+ } else {
374
+ await (deps.activateDefaultProfileConfig?.(ctx) ?? deps.reloadConfig());
375
+ }
352
376
  if (!deps.hasBotToken()) {
353
- await deps.promptForConfig(ctx);
377
+ const profileNames = deps.getProfileNames?.() ?? [];
378
+ if (!profileName && profileNames.length > 0) {
379
+ ctx.ui.notify(
380
+ `No default Telegram profile configured. Available profiles: ${profileNames.join(", ")}. Use /telegram-connect <profileName> or /telegram-setup to create a default profile.`,
381
+ "info",
382
+ );
383
+ deps.updateStatus(ctx);
384
+ return;
385
+ }
386
+ await deps.promptForConfig(ctx, profileName);
354
387
  return;
355
388
  }
356
389
  let result = await deps.startPolling(ctx, {
package/lib/config.ts CHANGED
@@ -6,27 +6,19 @@
6
6
 
7
7
  import { existsSync } from "node:fs";
8
8
  import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
9
- import { homedir } from "node:os";
10
- import { join, resolve } from "node:path";
9
+ import { resolveAgentDir, resolveTelegramConfigPath } from "./paths.ts";
11
10
 
12
11
  import type { TelegramInboundHandlerConfig } from "./inbound.ts";
13
12
  import type { CommandTemplateObjectConfig } from "./command-templates.ts";
14
13
 
15
14
  const CONFIG_RUNTIME_KEY = "__piTelegramConfigRuntime__";
16
15
 
17
- function getAgentDir(): string {
18
- return process.env.PI_CODING_AGENT_DIR
19
- ? resolve(process.env.PI_CODING_AGENT_DIR)
20
- : join(homedir(), ".pi", "agent");
21
- }
22
-
23
16
  function getConfigPath(): string {
24
- return join(getAgentDir(), "telegram.json");
17
+ return resolveTelegramConfigPath();
25
18
  }
26
19
 
27
20
  export type TelegramOutboundCommandTemplateConfig =
28
- | string
29
- | CommandTemplateObjectConfig;
21
+ string | CommandTemplateObjectConfig;
30
22
  export interface TelegramOutboundHandlerConfig extends CommandTemplateObjectConfig {
31
23
  type?: string;
32
24
  match?: string | string[];
@@ -75,12 +67,88 @@ export interface TelegramConfig {
75
67
  sendTranscript?: boolean;
76
68
  };
77
69
  time?: TelegramTimeConfig;
70
+ /** Named bot/session profiles (e.g. "work", "omp"). */
71
+ profiles?: Record<string, TelegramBotProfile>;
72
+ }
73
+
74
+ /**
75
+ * Per-profile bot/session identity fields.
76
+ * Stored under `profiles.<name>` in telegram.json.
77
+ * Shared bridge settings (inboundHandlers, outboundHandlers, voice, time,
78
+ * assistant, proactivePush) stay at the top level.
79
+ */
80
+ export interface TelegramBotProfile {
81
+ botToken: string;
82
+ botUsername?: string;
83
+ botId?: number;
84
+ allowedUserId?: number;
85
+ lastUpdateId?: number;
86
+ }
87
+
88
+ /** Profile names must be lowercase letters, digits, hyphens, underscores; max 32 chars. */
89
+ const TELEGRAM_PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,31}$/;
90
+ const TELEGRAM_RESERVED_PROFILE_NAMES: ReadonlySet<string> = new Set([
91
+ "default",
92
+ "main",
93
+ "active",
94
+ ]);
95
+
96
+ export function isValidTelegramProfileName(name: string): boolean {
97
+ return (
98
+ TELEGRAM_PROFILE_NAME_PATTERN.test(name) &&
99
+ !TELEGRAM_RESERVED_PROFILE_NAMES.has(name)
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Resolve the effective config for a named (or default) profile.
105
+ * Returns bot/session fields from the named profile, falling back to
106
+ * top-level fields for the default profile. Shared bridge settings
107
+ * always come from the top level.
108
+ */
109
+ export function resolveTelegramActiveProfile(
110
+ config: TelegramConfig,
111
+ profileName?: string,
112
+ ): {
113
+ botToken?: string;
114
+ botUsername?: string;
115
+ botId?: number;
116
+ allowedUserId?: number;
117
+ lastUpdateId?: number;
118
+ } {
119
+ if (!profileName || !config.profiles?.[profileName]) {
120
+ return {
121
+ botToken: config.botToken,
122
+ botUsername: config.botUsername,
123
+ botId: config.botId,
124
+ allowedUserId: config.allowedUserId,
125
+ lastUpdateId: config.lastUpdateId,
126
+ };
127
+ }
128
+ const profile = config.profiles[profileName];
129
+ return {
130
+ botToken: profile.botToken,
131
+ botUsername: profile.botUsername,
132
+ botId: profile.botId,
133
+ allowedUserId: profile.allowedUserId,
134
+ lastUpdateId: profile.lastUpdateId,
135
+ };
136
+ }
137
+
138
+ /** List defined profile names. */
139
+ export function getTelegramProfileNames(
140
+ config: TelegramConfig,
141
+ ): string[] {
142
+ return Object.keys(config.profiles ?? {}).sort();
78
143
  }
79
144
 
80
145
  export interface TelegramConfigStore {
81
146
  get: () => TelegramConfig;
147
+ getStoredConfig: () => TelegramConfig;
82
148
  set: (config: TelegramConfig) => void;
83
149
  update: (mutate: (config: TelegramConfig) => void) => void;
150
+ activateProfile: (profileName: string | undefined) => boolean;
151
+ getActiveProfileName: () => string | undefined;
84
152
  getBotToken: () => string | undefined;
85
153
  hasBotToken: () => boolean;
86
154
  getAllowedUserId: () => number | undefined;
@@ -211,23 +279,85 @@ export async function writeTelegramConfig(
211
279
  await chmod(configPath, 0o600);
212
280
  }
213
281
 
282
+ function getTelegramProfileFields(config: TelegramConfig): TelegramBotProfile | undefined {
283
+ const token = config.botToken?.trim();
284
+ if (!token) return undefined;
285
+ return {
286
+ botToken: token,
287
+ botUsername: config.botUsername,
288
+ botId: config.botId,
289
+ allowedUserId: config.allowedUserId,
290
+ lastUpdateId: config.lastUpdateId,
291
+ };
292
+ }
293
+
294
+ function applyTelegramProfile(
295
+ config: TelegramConfig,
296
+ profileName: string | undefined,
297
+ ): TelegramConfig {
298
+ if (!profileName) return config;
299
+ const profile = config.profiles?.[profileName];
300
+ if (!profile) return config;
301
+ return {
302
+ ...config,
303
+ botToken: profile.botToken,
304
+ botUsername: profile.botUsername,
305
+ botId: profile.botId,
306
+ allowedUserId: profile.allowedUserId,
307
+ lastUpdateId: profile.lastUpdateId,
308
+ };
309
+ }
310
+
311
+ function storeTelegramEffectiveConfig(
312
+ baseConfig: TelegramConfig,
313
+ nextConfig: TelegramConfig,
314
+ profileName: string | undefined,
315
+ ): TelegramConfig {
316
+ if (!profileName) return nextConfig;
317
+ const profile = getTelegramProfileFields(nextConfig);
318
+ const profiles = { ...(baseConfig.profiles ?? {}) };
319
+ if (profile) profiles[profileName] = profile;
320
+ else delete profiles[profileName];
321
+ return {
322
+ ...nextConfig,
323
+ botToken: baseConfig.botToken,
324
+ botUsername: baseConfig.botUsername,
325
+ botId: baseConfig.botId,
326
+ allowedUserId: baseConfig.allowedUserId,
327
+ lastUpdateId: baseConfig.lastUpdateId,
328
+ profiles: Object.keys(profiles).length > 0 ? profiles : undefined,
329
+ };
330
+ }
331
+
214
332
  export function createTelegramConfigStore(
215
333
  options: TelegramConfigStoreOptions = {},
216
334
  ): TelegramConfigStore {
217
335
  let config: TelegramConfig = options.initialConfig ?? {};
218
- const agentDir = options.agentDir ?? getAgentDir();
336
+ let activeProfileName: string | undefined;
337
+ const agentDir = options.agentDir ?? resolveAgentDir();
219
338
  const configPath = options.configPath ?? getConfigPath();
339
+ const getEffectiveConfig = () => applyTelegramProfile(config, activeProfileName);
340
+ const setEffectiveConfig = (nextConfig: TelegramConfig) => {
341
+ config = storeTelegramEffectiveConfig(config, nextConfig, activeProfileName);
342
+ };
220
343
  return {
221
- get: () => config,
222
- set: (nextConfig) => {
223
- config = nextConfig;
224
- },
344
+ get: getEffectiveConfig,
345
+ getStoredConfig: () => config,
346
+ set: setEffectiveConfig,
225
347
  update: (mutate) => {
226
- mutate(config);
348
+ const nextConfig = getEffectiveConfig();
349
+ mutate(nextConfig);
350
+ setEffectiveConfig(nextConfig);
351
+ },
352
+ activateProfile: (profileName) => {
353
+ if (profileName && !config.profiles?.[profileName]) return false;
354
+ activeProfileName = profileName;
355
+ return true;
227
356
  },
228
- getBotToken: () => config.botToken,
229
- hasBotToken: () => !!config.botToken,
230
- getAllowedUserId: () => config.allowedUserId,
357
+ getActiveProfileName: () => activeProfileName,
358
+ getBotToken: () => getEffectiveConfig().botToken,
359
+ hasBotToken: () => !!getEffectiveConfig().botToken,
360
+ getAllowedUserId: () => getEffectiveConfig().allowedUserId,
231
361
  getInboundHandlers: () => [
232
362
  ...(config.inboundHandlers ?? []),
233
363
  ...(config.attachmentHandlers ?? []),
@@ -235,7 +365,9 @@ export function createTelegramConfigStore(
235
365
  getAttachmentHandlers: () => config.attachmentHandlers,
236
366
  getOutboundHandlers: () => config.outboundHandlers,
237
367
  setAllowedUserId: (userId) => {
238
- config.allowedUserId = userId;
368
+ const nextConfig = getEffectiveConfig();
369
+ nextConfig.allowedUserId = userId;
370
+ setEffectiveConfig(nextConfig);
239
371
  },
240
372
  load: async () => {
241
373
  config = await readTelegramConfig(configPath, {
@@ -247,9 +379,18 @@ export function createTelegramConfigStore(
247
379
  });
248
380
  },
249
381
  });
382
+ if (activeProfileName && !config.profiles?.[activeProfileName]) {
383
+ activeProfileName = undefined;
384
+ }
250
385
  },
251
- persist: async (nextConfig = config) => {
252
- await writeTelegramConfig(agentDir, configPath, nextConfig);
386
+ persist: async (nextConfig = getEffectiveConfig()) => {
387
+ const storedConfig = storeTelegramEffectiveConfig(
388
+ config,
389
+ nextConfig,
390
+ activeProfileName,
391
+ );
392
+ config = storedConfig;
393
+ await writeTelegramConfig(agentDir, configPath, storedConfig);
253
394
  },
254
395
  };
255
396
  }
@@ -481,9 +622,7 @@ export function createTelegramConfigControls(
481
622
  }
482
623
 
483
624
  export type TelegramAuthorizationState =
484
- | { kind: "pair"; userId: number }
485
- | { kind: "allow" }
486
- | { kind: "deny" };
625
+ { kind: "pair"; userId: number } | { kind: "allow" } | { kind: "deny" };
487
626
 
488
627
  export interface TelegramUserPairingDeps<TContext> {
489
628
  allowedUserId?: number;