jeo-code 0.12.8 → 0.12.10

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/CHANGELOG.md CHANGED
@@ -7,7 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
9
9
 
10
- ## [Unreleased]
10
+ ## [0.12.10] - 2026-11-02
11
+
12
+ ### Added
13
+ - **Messenger computer use** — explicitly allow authorized messenger sessions to use the host computer tool, independently of the global computer setting. Host desktop and filesystem access are shared and not sandboxed.
14
+
15
+ ### Fixed
16
+ - **Literal worker tool isolation** — prevent environment-provided tool lists from bypassing a messenger worker's explicit allowlist.
17
+
18
+ ## [0.12.9] - 2026-11-02
19
+ ≔14c4+
20
+ - **Discord model policy** — optionally pin a Discord worker model and fall back on persistent usage or quota exhaustion before any tool execution; other platforms and global routing retain their own settings.
11
21
 
12
22
  ### Added
13
23
  - **Discord session files and control** — local opt-in enables authorized Discord prompts to use host session tools and exchange staged attachments; see the [Discord guide](docs/integrations/discord.md) for limits, public visibility, and revocation boundaries.
package/README.ja.md CHANGED
@@ -417,11 +417,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
417
417
  ## 変更履歴 (Changelog)
418
418
 
419
419
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
420
- - **[Unreleased]**
420
+ - **[0.12.10]** (2026-11-02)
421
+ - **[0.12.9]** (2026-11-02)
421
422
  - **[0.12.7]** (2026-10-18) — Repair desktop screenshot vision feedback, macOS scrolling, complete drag gestures, and keyboard normalization.
422
423
  - **[0.12.6]** (2026-10-17) — Preserve OpenAI subscription authentication behind configured proxies, isolate account catalogs, and give provider-specific recovery instructions while retaining Antigravity OAuth calls.
423
424
  - **[0.12.5]** (2026-10-17) — Antigravity and Kimi provider support in interactive setup, Kimi live model cache rehydration, immediate discovery caching on provider probing.
424
- - **[0.12.4]** (2026-10-17) — Refresh Antigravity provider (`antigravity/*`) account catalogs with `antigravity/hub/2.23.1` so entitled Gemini 4 Argon and Gemini 3.8 Flash tiers appear directly in `/model provider antigravity`.
425
425
 
426
426
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
427
427
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -418,11 +418,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
418
418
  ## 변경 이력 (Changelog)
419
419
 
420
420
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
421
- - **[Unreleased]**
421
+ - **[0.12.10]** (2026-11-02)
422
+ - **[0.12.9]** (2026-11-02)
422
423
  - **[0.12.7]** (2026-10-18) — Repair desktop screenshot vision feedback, macOS scrolling, complete drag gestures, and keyboard normalization.
423
424
  - **[0.12.6]** (2026-10-17) — Preserve OpenAI subscription authentication behind configured proxies, isolate account catalogs, and give provider-specific recovery instructions while retaining Antigravity OAuth calls.
424
425
  - **[0.12.5]** (2026-10-17) — Antigravity and Kimi provider support in interactive setup, Kimi live model cache rehydration, immediate discovery caching on provider probing.
425
- - **[0.12.4]** (2026-10-17) — Refresh Antigravity provider (`antigravity/*`) account catalogs with `antigravity/hub/2.23.1` so entitled Gemini 4 Argon and Gemini 3.8 Flash tiers appear directly in `/model provider antigravity`.
426
426
 
427
427
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
428
428
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -410,11 +410,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
410
410
  ## Changelog
411
411
 
412
412
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
413
- - **[Unreleased]**
413
+ - **[0.12.10]** (2026-11-02)
414
+ - **[0.12.9]** (2026-11-02)
414
415
  - **[0.12.7]** (2026-10-18) — Repair desktop screenshot vision feedback, macOS scrolling, complete drag gestures, and keyboard normalization.
415
416
  - **[0.12.6]** (2026-10-17) — Preserve OpenAI subscription authentication behind configured proxies, isolate account catalogs, and give provider-specific recovery instructions while retaining Antigravity OAuth calls.
416
417
  - **[0.12.5]** (2026-10-17) — Antigravity and Kimi provider support in interactive setup, Kimi live model cache rehydration, immediate discovery caching on provider probing.
417
- - **[0.12.4]** (2026-10-17) — Refresh Antigravity provider (`antigravity/*`) account catalogs with `antigravity/hub/2.23.1` so entitled Gemini 4 Argon and Gemini 3.8 Flash tiers appear directly in `/model provider antigravity`.
418
418
 
419
419
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
420
420
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -415,11 +415,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
415
415
  ## 更新日志 (Changelog)
416
416
 
417
417
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
418
- - **[Unreleased]**
418
+ - **[0.12.10]** (2026-11-02)
419
+ - **[0.12.9]** (2026-11-02)
419
420
  - **[0.12.7]** (2026-10-18) — Repair desktop screenshot vision feedback, macOS scrolling, complete drag gestures, and keyboard normalization.
420
421
  - **[0.12.6]** (2026-10-17) — Preserve OpenAI subscription authentication behind configured proxies, isolate account catalogs, and give provider-specific recovery instructions while retaining Antigravity OAuth calls.
421
422
  - **[0.12.5]** (2026-10-17) — Antigravity and Kimi provider support in interactive setup, Kimi live model cache rehydration, immediate discovery caching on provider probing.
422
- - **[0.12.4]** (2026-10-17) — Refresh Antigravity provider (`antigravity/*`) account catalogs with `antigravity/hub/2.23.1` so entitled Gemini 4 Argon and Gemini 3.8 Flash tiers appear directly in `/model provider antigravity`.
423
423
 
424
424
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
425
425
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.12.8",
3
+ "version": "0.12.10",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -53,6 +53,7 @@
53
53
  "changelog:sync": "bun scripts/sync-changelog.ts",
54
54
  "test": "bun test",
55
55
  "verify:browser": "bun scripts/verify-browser.ts",
56
+ "verify:discord": "bun scripts/verify-discord-integration.ts",
56
57
  "verify:tmux": "bash scripts/tmux-verify.sh",
57
58
  "dev:link": "bun scripts/dev-link.ts link",
58
59
  "dev:doctor": "bun scripts/dev-link.ts doctor"
@@ -1,10 +1,12 @@
1
1
  import { lstat, mkdir, open, realpath, rm } from "node:fs/promises";
2
2
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
3
3
 
4
- const MAX_FILES = 10;
4
+ /** Limit enforced on presented attachment entries, before validation or deduplication. */
5
+ export const MAX_ARTIFACTS = 10;
5
6
  const MAX_FILE_BYTES = 25 * 1024 * 1024;
6
7
  const MAX_TOTAL_BYTES = 50 * 1024 * 1024;
7
- const CDN_HOSTS = ["cdn.discordapp.com", "media.discordapp.net"];
8
+ export const DISCORD_ATTACHMENT_CDN_HOSTS = ["cdn.discordapp.com", "media.discordapp.net"] as const;
9
+ export const MAX_DISCORD_ATTACHMENT_REDIRECTS = 5;
8
10
 
9
11
  interface Attachment {
10
12
  filename: string;
@@ -23,12 +25,62 @@ export interface DiscordAttachmentStage {
23
25
  cleanup: () => Promise<void>;
24
26
  }
25
27
 
28
+ /**
29
+ * Parse and validate one Discord CDN URL without accepting normalization that
30
+ * could conceal an ambiguous authority. Host matching is exact after URL's
31
+ * ASCII/case normalization; Unicode, percent-encoded authorities, trailing
32
+ * dots, user-info, and explicit ports are rejected.
33
+ */
34
+ export function validateDiscordAttachmentUrl(value: string): URL {
35
+ if (typeof value !== "string") throw new Error("Untrusted Discord attachment URL");
36
+ const authority = value.match(/^https:\/\/([^/?#]*)/i)?.[1];
37
+ if (!authority || /[^\x00-\x7f]/.test(authority) || authority.includes("%") || authority.endsWith(".")) {
38
+ throw new Error("Untrusted Discord attachment URL");
39
+ }
40
+ if (authority.includes("@") || authority.includes(":")) throw new Error("Untrusted Discord attachment URL");
41
+ let url: URL;
42
+ try { url = new URL(value); } catch { throw new Error("Invalid Discord attachment URL"); }
43
+ if (url.protocol !== "https:" ||
44
+ !(DISCORD_ATTACHMENT_CDN_HOSTS as readonly string[]).includes(url.hostname) ||
45
+ url.username || url.password || url.port) {
46
+ throw new Error("Untrusted Discord attachment URL");
47
+ }
48
+ return url;
49
+ }
50
+
51
+ async function fetchTrustedAttachment(
52
+ initialUrl: string,
53
+ fetchImpl: typeof fetch,
54
+ signal: AbortSignal,
55
+ ): Promise<Response> {
56
+ let current = validateDiscordAttachmentUrl(initialUrl);
57
+ for (let redirects = 0; ; redirects++) {
58
+ const response = await fetchImpl(current.href, { redirect: "manual", signal });
59
+ if (response.status >= 300 && response.status < 400) {
60
+ const location = response.headers.get("location");
61
+ await response.body?.cancel().catch(() => {});
62
+ if (!location || redirects >= MAX_DISCORD_ATTACHMENT_REDIRECTS) throw new Error("Discord attachment redirect rejected");
63
+ // Validate an authority-bearing Location before URL resolution can erase
64
+ // percent-encoding, IDNA, case, or default-port evidence.
65
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(location)) validateDiscordAttachmentUrl(location);
66
+ else if (location.startsWith("//")) validateDiscordAttachmentUrl(`${current.protocol}${location}`);
67
+ current = validateDiscordAttachmentUrl(new URL(location, current).href);
68
+ continue;
69
+ }
70
+ // Native fetch exposes the final URL. Test doubles commonly leave it blank.
71
+ if (response.url) validateDiscordAttachmentUrl(response.url);
72
+ return response;
73
+ }
74
+ }
75
+
26
76
  /** Staged files are private and must be removed with cleanup after the request finishes. */
27
77
  export async function stageDiscordAttachments(
28
78
  attachments: unknown[],
29
79
  options: DiscordAttachmentOptions,
30
80
  ): Promise<DiscordAttachmentStage> {
31
- if (!Array.isArray(attachments) || attachments.length > MAX_FILES) throw new Error("Invalid Discord attachment count");
81
+ // Every presented entry consumes the limit, including duplicates and entries
82
+ // that would later fail metadata validation. Overflow rejects the whole input.
83
+ if (!Array.isArray(attachments) || attachments.length > MAX_ARTIFACTS) throw new Error("Invalid Discord attachment count");
32
84
  const validated: Attachment[] = [];
33
85
  let declaredTotal = 0;
34
86
  for (const value of attachments) {
@@ -38,11 +90,7 @@ export async function stageDiscordAttachments(
38
90
  !Number.isSafeInteger(item.size) || (item.size as number) < 0 || (item.size as number) > MAX_FILE_BYTES) {
39
91
  throw new Error("Invalid Discord attachment metadata");
40
92
  }
41
- let url: URL;
42
- try { url = new URL(item.url); } catch { throw new Error("Invalid Discord attachment URL"); }
43
- if (url.protocol !== "https:" || !CDN_HOSTS.includes(url.hostname) || url.username || url.password || url.port) {
44
- throw new Error("Untrusted Discord attachment URL");
45
- }
93
+ const url = validateDiscordAttachmentUrl(item.url);
46
94
  declaredTotal += item.size as number;
47
95
  if (declaredTotal > MAX_TOTAL_BYTES) throw new Error("Discord attachments exceed total size limit");
48
96
  validated.push({ filename: item.filename, url: url.href, size: item.size as number });
@@ -69,7 +117,7 @@ export async function stageDiscordAttachments(
69
117
  const signal = AbortSignal.any([...(options.signal ? [options.signal] : []), AbortSignal.timeout(30_000)]);
70
118
  let response: Response;
71
119
  try {
72
- response = await (options.fetchImpl ?? fetch)(item.url, { redirect: "error", signal });
120
+ response = await fetchTrustedAttachment(item.url, options.fetchImpl ?? fetch, signal);
73
121
  } catch {
74
122
  throw new Error("Discord attachment download failed or aborted");
75
123
  }
@@ -252,12 +252,13 @@ export const TOOL_DESCRIPTIONS: Record<string, string> = {
252
252
  find: "find {globPattern} — find files by name",
253
253
  search: "search {pattern, globPattern?, ignoreCase?, context?, maxMatches?} — grep (context: N lines around each match)",
254
254
  ls: "ls {dirPath} — list a directory's entries (dirs first)",
255
+ computer: "computer {action, x?, y?, text?, key?, deltaX?, deltaY?, duration?, actions?} — desktop automation (screenshot, click, double_click, move, drag, scroll, type, keypress, wait, batch)",
255
256
  };
256
257
 
257
258
  export function buildToolProtocol(allowedTools: Set<string>): string {
258
259
  const lines: string[] = ["You have these tools (call exactly ONE per step):"];
259
260
  let num = 1;
260
- for (const name of ["read", "write", "edit", "bash", "find", "search", "ls"]) {
261
+ for (const name of ["read", "write", "edit", "bash", "find", "search", "ls", "computer"]) {
261
262
  if (allowedTools.has(name)) {
262
263
  lines.push(`${num}. ${TOOL_DESCRIPTIONS[name]}`);
263
264
  num++;
@@ -67,6 +67,7 @@ import { routePrompt, deriveCacheSessionKey, warnOnce, tierModelPool, selectFrom
67
67
  import { RouteHistory } from "../agent/route-history";
68
68
 
69
69
  import { isRateLimitError, isUsageLimitError, isConnectionError } from "../util/retry";
70
+ import { messengerUsageFallback } from "../messenger/model-policy";
70
71
 
71
72
 
72
73
  import { listAliases } from "../ai/model-registry";
@@ -738,7 +739,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
738
739
  // never persisted to config.
739
740
  const routeHistory = new RouteHistory();
740
741
  // Computer-use session override: undefined = follow config.computer.enabled, true/false = /computer on|off wins this session.
741
- let sessionComputerOverride: boolean | undefined;
742
+ let sessionComputerOverride: boolean | undefined = flags.literalWorker && process.env.JEO_MESSENGER_COMPUTER_ENABLED === "1" ? true : undefined;
742
743
 
743
744
  // Cache of live, credential-validated models per provider (refreshed by live pickers).
744
745
  // Every successful discovery is also persisted (`writeModelCache`), so the NEXT launch
@@ -1241,7 +1242,8 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1241
1242
  // global default (rememberModelPatch), so re-reading turnConfig.defaultModel here let
1242
1243
  // that write silently switch THIS running session's model mid-run. Per-session model
1243
1244
  // selection must stay session-local: running sessions never influence each other.
1244
- const routingEnabled = sessionRouteOverride ?? !!turnConfig.routing?.enabled;
1245
+ const routingEnabled = flags.literalWorker && process.env.JEO_MESSENGER_FIXED_MODEL === "1"
1246
+ ? false : sessionRouteOverride ?? !!turnConfig.routing?.enabled;
1245
1247
  let routeCredentialNotice: string | undefined;
1246
1248
  let routeVetoNote: string | undefined;
1247
1249
  // Credential SCOPES that have shown an ACCOUNT-LEVEL failure (rate limit,
@@ -1968,6 +1970,15 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1968
1970
  return null;
1969
1971
  };
1970
1972
  result = await runLoopWithModel(activeModel, activeThinking, turnSessionKey);
1973
+ const messengerFallback = flags.literalWorker && process.env.JEO_MESSENGER_FIXED_MODEL === "1"
1974
+ ? messengerUsageFallback(process.env.JEO_MESSENGER_USAGE_FALLBACK_MODEL, activeModel, result)
1975
+ : undefined;
1976
+ if (messengerFallback) {
1977
+ activeModel = messengerFallback;
1978
+ turnSessionKey = sessionId ? deriveCacheSessionKey(sessionId, activeModel) : undefined;
1979
+ contextTokens = catalogMetadata(activeModel)?.contextTokens;
1980
+ result = await runLoopWithModel(activeModel, activeThinking, turnSessionKey);
1981
+ }
1971
1982
  const routeFallbackMaxRaw = Number(jeoEnv("ROUTE_FALLBACK_MAX_ATTEMPTS") ?? NaN);
1972
1983
  const routeFallbackMax = Number.isFinite(routeFallbackMaxRaw) && routeFallbackMaxRaw >= 0
1973
1984
  ? Math.floor(routeFallbackMaxRaw)
@@ -98,7 +98,9 @@ export function createMessengerCliReply(config: MessengerConfig, spawn: typeof B
98
98
  const sourceEntry = process.argv[1]?.endsWith(".ts") ? process.argv[1] : undefined;
99
99
  const tools = [...new Set(config.toolNames.map(name => name === "grep" ? "search" : name === "glob" ? "find" : name))];
100
100
  const toolFlags = tools.length ? ["--tools", tools.join(",")] : ["--no-tools"];
101
- const command = [executable, ...(sourceEntry ? ["--no-env-file", sourceEntry] : []), "launch", "--print", "--no-tui", "--no-skills", "--literal-worker", ...toolFlags, "--resume", id, "--", "worker"];
101
+ const platformConfig = config.platforms[message.platform];
102
+ const modelFlags = platformConfig?.model ? ["--model", platformConfig.model] : [];
103
+ const command = [executable, ...(sourceEntry ? ["--no-env-file", sourceEntry] : []), "launch", "--print", "--no-tui", "--no-skills", "--literal-worker", ...toolFlags, ...modelFlags, "--resume", id, "--", "worker"];
102
104
  const secretNames = new Set<string>();
103
105
  for (const platform of Object.values(config.platforms)) {
104
106
  if (!platform) continue;
@@ -112,6 +114,13 @@ export function createMessengerCliReply(config: MessengerConfig, spawn: typeof B
112
114
  const env = Object.fromEntries(Object.entries(process.env).filter(([name, value]) => value !== undefined && !secretNames.has(name))) as Record<string, string>;
113
115
  for (const name of secretNames) env[name] = "";
114
116
  env.JEO_SESSION_DIR = config.sessionDir;
117
+ // The explicit tool allowlist is the opt-in for this worker only.
118
+ env.JEO_MESSENGER_COMPUTER_ENABLED = tools.includes("computer") ? "1" : "0";
119
+ // Never inherit another platform's fallback policy from the gateway environment.
120
+ delete env.JEO_MESSENGER_USAGE_FALLBACK_MODEL;
121
+ if (platformConfig?.usageFallbackModel) env.JEO_MESSENGER_USAGE_FALLBACK_MODEL = platformConfig.usageFallbackModel;
122
+ delete env.JEO_MESSENGER_FIXED_MODEL;
123
+ if (platformConfig?.model) env.JEO_MESSENGER_FIXED_MODEL = "1";
115
124
  return pool.turn(id, command, config.cwd, env, [message.text, artifactPrompt].filter(Boolean).join("\n\n"), signal, onActivity);
116
125
  };
117
126
  return Object.assign(reply, { close: pool.close });
@@ -12,6 +12,7 @@ const TOOLS: Record<string, true> = {
12
12
  bash: true,
13
13
  edit: true,
14
14
  write: true,
15
+ computer: true,
15
16
  };
16
17
 
17
18
  function object(value: unknown, label: string): Record<string, unknown> {
@@ -75,7 +76,7 @@ export function parseMessengerConfig(value: unknown, baseDir: string): Messenger
75
76
  platform === "slack"
76
77
  ? ["tokenEnv", "appTokenEnv", "allowedUserIds", "allowedChannelIds"]
77
78
  : platform === "discord"
78
- ? ["tokenEnv", "allowedUserIds", "allowedChannelIds", "artifactDir"]
79
+ ? ["tokenEnv", "allowedUserIds", "allowedChannelIds", "artifactDir", "model", "usageFallbackModel"]
79
80
  : ["tokenEnv", "allowedUserIds", "allowedChannelIds"],
80
81
  `${platform} config`,
81
82
  );
@@ -89,6 +90,12 @@ export function parseMessengerConfig(value: unknown, baseDir: string): Messenger
89
90
  config.allowedChannelIds = ids(item.allowedChannelIds, `${platform}.allowedChannelIds`);
90
91
  if (platform === "discord" && item.artifactDir !== undefined)
91
92
  config.artifactDir = path.resolve(baseDir, string(item.artifactDir, "discord.artifactDir"));
93
+ if (platform === "discord" && item.model !== undefined)
94
+ config.model = string(item.model, "discord.model");
95
+ if (platform === "discord" && item.usageFallbackModel !== undefined) {
96
+ if (!config.model) throw new Error("discord.usageFallbackModel requires discord.model");
97
+ config.usageFallbackModel = string(item.usageFallbackModel, "discord.usageFallbackModel");
98
+ }
92
99
  platforms[platform] = config;
93
100
  }
94
101
  const toolNames = input.toolNames === undefined ? [] : input.toolNames;
@@ -97,7 +104,7 @@ export function parseMessengerConfig(value: unknown, baseDir: string): Messenger
97
104
  toolNames.some(name => typeof name !== "string" || !Object.hasOwn(TOOLS, name)) ||
98
105
  new Set(toolNames).size !== toolNames.length
99
106
  ) {
100
- throw new Error("toolNames must be a unique list drawn from read, grep, glob, bash, edit, write");
107
+ throw new Error("toolNames must be a unique list drawn from read, grep, glob, find, search, bash, edit, write, computer");
101
108
  }
102
109
  return {
103
110
  cwd: path.resolve(baseDir, input.cwd === undefined ? "." : string(input.cwd, "cwd")),
@@ -0,0 +1,8 @@
1
+ import { isUsageLimitError } from "../util/retry";
2
+
3
+ /** Only retry a worker's first failed step, before any tools could have executed. */
4
+ export function messengerUsageFallback(fallback: string | undefined, current: string, result: { done: boolean; steps: number; stopClass?: unknown; doneReason?: string }): string | undefined {
5
+ if (!fallback || fallback === current || result.done || result.stopClass || result.steps !== 1) return undefined;
6
+ const reason = result.doneReason ?? "";
7
+ return isUsageLimitError(reason) || /usage\/quota limit reached/i.test(reason) ? fallback : undefined;
8
+ }
@@ -45,6 +45,9 @@ export interface MessengerPlatformConfig {
45
45
  allowedChannelIds?: string[];
46
46
  /** Dedicated operator-approved directory for Discord output files. */
47
47
  artifactDir?: string;
48
+ /** Optional platform-specific model and persistent-usage-limit fallback. */
49
+ model?: string;
50
+ usageFallbackModel?: string;
48
51
  }
49
52
 
50
53
  export interface MessengerConfig {