thepopebot 1.2.78 → 1.2.82

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/api/index.js CHANGED
@@ -16,15 +16,8 @@ import { setAgentJobSecret } from '../lib/db/config.js';
16
16
  // ── Per-key lock for OAuth token refresh ────────────────────────────
17
17
  const _refreshLocks = new Map();
18
18
 
19
- // Bot token — resolved from DB/env
20
- let telegramBotToken = null;
21
-
22
-
23
19
  function getTelegramBotToken() {
24
- if (!telegramBotToken) {
25
- telegramBotToken = getConfig('TELEGRAM_BOT_TOKEN') || null;
26
- }
27
- return telegramBotToken;
20
+ return getConfig('TELEGRAM_BOT_TOKEN') || null;
28
21
  }
29
22
 
30
23
 
@@ -287,9 +287,7 @@ export async function createOAuthToken(tokenType, name, token) {
287
287
  const user = await requireAdmin();
288
288
  try {
289
289
  const { createOAuthToken: dbCreate } = await import('../db/oauth-tokens.js');
290
- const { invalidateConfigCache } = await import('../config.js');
291
290
  const result = dbCreate(tokenType, name || 'OAuth Token', token, user.id);
292
- invalidateConfigCache();
293
291
  return result;
294
292
  } catch (err) {
295
293
  console.error('Failed to create OAuth token:', err);
@@ -322,9 +320,7 @@ export async function deleteOAuthToken(id) {
322
320
  await requireAdmin();
323
321
  try {
324
322
  const { deleteOAuthTokenById } = await import('../db/oauth-tokens.js');
325
- const { invalidateConfigCache } = await import('../config.js');
326
323
  deleteOAuthTokenById(id);
327
- invalidateConfigCache();
328
324
  return { success: true };
329
325
  } catch (err) {
330
326
  console.error('Failed to delete OAuth token:', err);
@@ -793,7 +789,6 @@ export async function updateCodingAgentConfig(agent, config) {
793
789
  await requireAdmin();
794
790
  try {
795
791
  const { setConfigValue } = await import('../db/config.js');
796
- const { invalidateConfigCache } = await import('../config.js');
797
792
 
798
793
  if (agent === 'claude-code') {
799
794
  if (config.enabled !== undefined) setConfigValue('CODING_AGENT_CLAUDE_CODE_ENABLED', String(config.enabled));
@@ -823,7 +818,6 @@ export async function updateCodingAgentConfig(agent, config) {
823
818
  return { error: 'Invalid agent' };
824
819
  }
825
820
 
826
- invalidateConfigCache();
827
821
  if (agent === 'claude-code' && config.backend !== undefined) {
828
822
  await syncLitellmConfig();
829
823
  }
@@ -842,9 +836,7 @@ export async function setCodingAgentDefault(agent) {
842
836
  await requireAdmin();
843
837
  try {
844
838
  const { setConfigValue } = await import('../db/config.js');
845
- const { invalidateConfigCache } = await import('../config.js');
846
839
  setConfigValue('CODING_AGENT', agent);
847
- invalidateConfigCache();
848
840
  return { success: true };
849
841
  } catch (err) {
850
842
  console.error('Failed to set default coding agent:', err);
@@ -893,9 +885,7 @@ export async function setModeDefault(mode, field, value) {
893
885
  const stored = field === 'autoRun' ? (value ? 'true' : 'false') : value;
894
886
 
895
887
  const { setConfigValue } = await import('../db/config.js');
896
- const { invalidateConfigCache } = await import('../config.js');
897
888
  setConfigValue(key, stored);
898
- invalidateConfigCache();
899
889
  return { success: true };
900
890
  } catch (err) {
901
891
  console.error('Failed to set mode default:', err);
@@ -937,9 +927,7 @@ export async function updateGeneralSetting(key, value) {
937
927
  }
938
928
  try {
939
929
  const { setConfigValue } = await import('../db/config.js');
940
- const { invalidateConfigCache } = await import('../config.js');
941
930
  setConfigValue(key, value);
942
- invalidateConfigCache();
943
931
  return { success: true };
944
932
  } catch (err) {
945
933
  console.error('Failed to update general setting:', err);
@@ -985,7 +973,6 @@ export async function updateApiKeySetting(key, value) {
985
973
  }
986
974
  try {
987
975
  const { setConfigSecret, deleteConfigSecret } = await import('../db/config.js');
988
- const { invalidateConfigCache } = await import('../config.js');
989
976
 
990
977
  if (value) {
991
978
  setConfigSecret(key, value, user.id);
@@ -993,7 +980,6 @@ export async function updateApiKeySetting(key, value) {
993
980
  deleteConfigSecret(key);
994
981
  }
995
982
 
996
- invalidateConfigCache();
997
983
  return { success: true };
998
984
  } catch (err) {
999
985
  console.error('Failed to update API key setting:', err);
@@ -1013,9 +999,7 @@ export async function regenerateWebhookSecret(key) {
1013
999
  const { randomBytes } = await import('crypto');
1014
1000
  const secret = randomBytes(32).toString('hex');
1015
1001
  const { setConfigSecret } = await import('../db/config.js');
1016
- const { invalidateConfigCache } = await import('../config.js');
1017
1002
  setConfigSecret(key, secret, user.id);
1018
- invalidateConfigCache();
1019
1003
  return { success: true };
1020
1004
  } catch (err) {
1021
1005
  console.error('Failed to regenerate webhook secret:', err);
@@ -1034,12 +1018,19 @@ export async function regenerateWebhookSecret(key) {
1034
1018
  export async function getTelegramStatus() {
1035
1019
  await requireAuth();
1036
1020
  try {
1037
- const { getConfigSecret } = await import('../db/config.js');
1021
+ const { getConfigSecret, getConfigValue } = await import('../db/config.js');
1022
+ const { getConfig } = await import('../config.js');
1038
1023
  const { validateBotToken, getTelegramWebhookInfo } = await import('../tools/telegram.js');
1039
1024
 
1040
1025
  const botToken = getConfigSecret('TELEGRAM_BOT_TOKEN');
1041
1026
  const webhookSecret = getConfigSecret('TELEGRAM_WEBHOOK_SECRET');
1042
1027
 
1028
+ const appUrl = getConfig('APP_URL');
1029
+ const defaultWebhookUrl = appUrl
1030
+ ? `${appUrl.replace(/\/$/, '')}/api/telegram/webhook`
1031
+ : '';
1032
+ const webhookUrlOverride = getConfigValue('TELEGRAM_WEBHOOK_URL') || null;
1033
+
1043
1034
  let botInfo = null;
1044
1035
  let webhookInfo = null;
1045
1036
  if (botToken) {
@@ -1065,6 +1056,8 @@ export async function getTelegramStatus() {
1065
1056
  botTokenSet: !!botToken,
1066
1057
  webhookSecretSet: !!webhookSecret,
1067
1058
  webhookInfo,
1059
+ defaultWebhookUrl,
1060
+ webhookUrlOverride,
1068
1061
  };
1069
1062
  } catch (err) {
1070
1063
  console.error('Failed to get Telegram status:', err);
@@ -1094,31 +1087,64 @@ export async function validateTelegramToken(token) {
1094
1087
  /**
1095
1088
  * Register the Telegram webhook with the currently saved bot token.
1096
1089
  * Generates a fresh webhook secret, saves it, and calls Telegram's setWebhook.
1097
- * APP_URL must be configured.
1098
- */
1099
- export async function registerTelegramWebhook() {
1090
+ *
1091
+ * URL resolution:
1092
+ * - explicit `webhookUrl` arg (persisted as override if it differs from default)
1093
+ * - stored `TELEGRAM_WEBHOOK_URL` override
1094
+ * - `${APP_URL}/api/telegram/webhook` default
1095
+ *
1096
+ * Pass an empty string for `webhookUrl` to clear the override and fall back
1097
+ * to the APP_URL-derived default.
1098
+ */
1099
+ export async function registerTelegramWebhook(webhookUrl) {
1100
1100
  const user = await requireAdmin();
1101
1101
  try {
1102
- const { getConfigSecret, setConfigSecret } = await import('../db/config.js');
1103
- const { invalidateConfigCache, getConfig } = await import('../config.js');
1102
+ const { getConfigSecret, setConfigSecret, getConfigValue, setConfigValue, deleteConfigValue } =
1103
+ await import('../db/config.js');
1104
+ const { getConfig } = await import('../config.js');
1104
1105
  const { setTelegramWebhook, generateWebhookSecret } = await import('../tools/telegram.js');
1105
1106
 
1106
1107
  const botToken = getConfigSecret('TELEGRAM_BOT_TOKEN');
1107
1108
  if (!botToken) return { error: 'Bot token must be set first' };
1108
1109
 
1109
1110
  const appUrl = getConfig('APP_URL');
1110
- if (!appUrl) return { error: 'APP_URL must be configured first' };
1111
+ const defaultUrl = appUrl ? `${appUrl.replace(/\/$/, '')}/api/telegram/webhook` : '';
1112
+
1113
+ // Resolve the URL to register.
1114
+ let urlToRegister;
1115
+ if (typeof webhookUrl === 'string') {
1116
+ const trimmed = webhookUrl.trim();
1117
+ if (!trimmed || trimmed === defaultUrl) {
1118
+ deleteConfigValue('TELEGRAM_WEBHOOK_URL');
1119
+ urlToRegister = trimmed || defaultUrl;
1120
+ } else {
1121
+ setConfigValue('TELEGRAM_WEBHOOK_URL', trimmed, user.id);
1122
+ urlToRegister = trimmed;
1123
+ }
1124
+ } else {
1125
+ urlToRegister = getConfigValue('TELEGRAM_WEBHOOK_URL') || defaultUrl;
1126
+ }
1127
+
1128
+ if (!urlToRegister) {
1129
+ return { error: 'Webhook URL is required (set APP_URL or supply a URL)' };
1130
+ }
1131
+ try {
1132
+ const parsed = new URL(urlToRegister);
1133
+ if (parsed.protocol !== 'https:') {
1134
+ return { error: 'Webhook URL must use https://' };
1135
+ }
1136
+ } catch {
1137
+ return { error: 'Webhook URL is not a valid URL' };
1138
+ }
1111
1139
 
1112
- const webhookUrl = `${appUrl.replace(/\/$/, '')}/api/telegram/webhook`;
1113
1140
  const secret = generateWebhookSecret();
1114
1141
  setConfigSecret('TELEGRAM_WEBHOOK_SECRET', secret, user.id);
1115
- invalidateConfigCache();
1116
1142
 
1117
- const result = await setTelegramWebhook(botToken, webhookUrl, secret);
1143
+ const result = await setTelegramWebhook(botToken, urlToRegister, secret);
1118
1144
  if (!result.ok) {
1119
1145
  return { error: result.description || 'Failed to register webhook' };
1120
1146
  }
1121
- return { success: true, webhookUrl };
1147
+ return { success: true, webhookUrl: urlToRegister };
1122
1148
  } catch (err) {
1123
1149
  console.error('Failed to register Telegram webhook:', err);
1124
1150
  return { error: err.message };
@@ -1180,13 +1206,11 @@ export async function updateProviderCredential(credentialKey, value) {
1180
1206
  return { error: 'Invalid credential key' };
1181
1207
  }
1182
1208
  const { setConfigSecret, deleteConfigSecret } = await import('../db/config.js');
1183
- const { invalidateConfigCache } = await import('../config.js');
1184
1209
  if (value) {
1185
1210
  setConfigSecret(credentialKey, value, user.id);
1186
1211
  } else {
1187
1212
  deleteConfigSecret(credentialKey);
1188
1213
  }
1189
- invalidateConfigCache();
1190
1214
  await syncLitellmConfig();
1191
1215
  return { success: true };
1192
1216
  } catch (err) {
@@ -1257,11 +1281,9 @@ export async function addCustomProvider(config) {
1257
1281
  const user = await requireAdmin();
1258
1282
  try {
1259
1283
  const { setCustomProvider } = await import('../db/config.js');
1260
- const { invalidateConfigCache } = await import('../config.js');
1261
1284
  // Generate slug from name
1262
1285
  const slug = config.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
1263
1286
  setCustomProvider(slug, config, user.id);
1264
- invalidateConfigCache();
1265
1287
  await syncLitellmConfig();
1266
1288
  return { success: true, key: slug };
1267
1289
  } catch (err) {
@@ -1277,9 +1299,7 @@ export async function updateCustomProvider(key, config) {
1277
1299
  const user = await requireAdmin();
1278
1300
  try {
1279
1301
  const { setCustomProvider } = await import('../db/config.js');
1280
- const { invalidateConfigCache } = await import('../config.js');
1281
1302
  setCustomProvider(key, config, user.id);
1282
- invalidateConfigCache();
1283
1303
  await syncLitellmConfig();
1284
1304
  return { success: true };
1285
1305
  } catch (err) {
@@ -1295,9 +1315,7 @@ export async function removeCustomProvider(key) {
1295
1315
  const user = await requireAdmin();
1296
1316
  try {
1297
1317
  const { deleteCustomProvider } = await import('../db/config.js');
1298
- const { invalidateConfigCache } = await import('../config.js');
1299
1318
  deleteCustomProvider(key);
1300
- invalidateConfigCache();
1301
1319
  await syncLitellmConfig();
1302
1320
  return { success: true };
1303
1321
  } catch (err) {
@@ -1313,11 +1331,9 @@ export async function setActiveLlm(provider, model, maxTokens) {
1313
1331
  const user = await requireAdmin();
1314
1332
  try {
1315
1333
  const { setConfigValue } = await import('../db/config.js');
1316
- const { invalidateConfigCache } = await import('../config.js');
1317
1334
  setConfigValue('LLM_PROVIDER', provider, user.id);
1318
1335
  setConfigValue('LLM_MODEL', model, user.id);
1319
1336
  if (maxTokens) setConfigValue('LLM_MAX_TOKENS', maxTokens, user.id);
1320
- invalidateConfigCache();
1321
1337
  return { success: true };
1322
1338
  } catch (err) {
1323
1339
  console.error('Failed to set active LLM:', err);
@@ -273,6 +273,8 @@ function ApiKeysTelegramPage() {
273
273
  const [tokenError, setTokenError] = useState(null);
274
274
  const [webhookSaving, setWebhookSaving] = useState(false);
275
275
  const [webhookError, setWebhookError] = useState(null);
276
+ const [webhookEditing, setWebhookEditing] = useState(false);
277
+ const [webhookUrlInput, setWebhookUrlInput] = useState("");
276
278
  const loadStatus = async () => {
277
279
  try {
278
280
  const result = await getTelegramStatus();
@@ -284,6 +286,12 @@ function ApiKeysTelegramPage() {
284
286
  useEffect(() => {
285
287
  loadStatus();
286
288
  }, []);
289
+ useEffect(() => {
290
+ if (!status || webhookEditing) return;
291
+ setWebhookUrlInput(
292
+ status.webhookUrlOverride || status.webhookInfo?.url || status.defaultWebhookUrl || ""
293
+ );
294
+ }, [status, webhookEditing]);
287
295
  if (loading) {
288
296
  return /* @__PURE__ */ jsx("div", { className: "h-48 animate-pulse rounded-md bg-border/50" });
289
297
  }
@@ -318,11 +326,33 @@ function ApiKeysTelegramPage() {
318
326
  const handleRegisterWebhook = async () => {
319
327
  setWebhookSaving(true);
320
328
  setWebhookError(null);
321
- const result = await registerTelegramWebhook();
322
- if (result?.error) setWebhookError(result.error);
329
+ const url = webhookEditing ? webhookUrlInput.trim() : void 0;
330
+ const result = await registerTelegramWebhook(url);
331
+ if (result?.error) {
332
+ setWebhookError(result.error);
333
+ setWebhookSaving(false);
334
+ return;
335
+ }
336
+ setWebhookEditing(false);
337
+ setWebhookUrlInput("");
323
338
  await loadStatus();
324
339
  setWebhookSaving(false);
325
340
  };
341
+ const startWebhookEdit = () => {
342
+ setWebhookError(null);
343
+ setWebhookUrlInput(
344
+ status.webhookUrlOverride || status.webhookInfo?.url || status.defaultWebhookUrl || ""
345
+ );
346
+ setWebhookEditing(true);
347
+ };
348
+ const cancelWebhookEdit = () => {
349
+ setWebhookEditing(false);
350
+ setWebhookUrlInput("");
351
+ setWebhookError(null);
352
+ };
353
+ const resetWebhookToDefault = () => {
354
+ setWebhookUrlInput(status.defaultWebhookUrl || "");
355
+ };
326
356
  return /* @__PURE__ */ jsxs("div", { children: [
327
357
  /* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
328
358
  /* @__PURE__ */ jsx("h2", { className: "text-base font-medium", children: "Telegram" }),
@@ -417,33 +447,75 @@ function ApiKeysTelegramPage() {
417
447
  /* @__PURE__ */ jsx("div", { className: `rounded-lg border bg-card p-4 ${!step1Done ? "opacity-50 pointer-events-none" : ""}`, children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
418
448
  /* @__PURE__ */ jsx(StepIndicator, { n: 2, state: step2Done ? "done" : step1Done ? "active" : "pending" }),
419
449
  /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
420
- /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between gap-2", children: /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
421
- /* @__PURE__ */ jsx("h3", { className: "text-sm font-medium", children: "Webhook" }),
422
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mt-0.5", children: "Register your public URL with Telegram so it can deliver messages to your bot." }),
423
- step2Done && /* @__PURE__ */ jsxs("div", { className: "mt-2 text-xs text-muted-foreground truncate", children: [
424
- /* @__PURE__ */ jsx("span", { className: "font-mono", children: status.webhookInfo.url }),
425
- status.webhookInfo.pendingUpdates > 0 && /* @__PURE__ */ jsxs("span", { className: "ml-2 text-yellow-500", children: [
426
- "(",
427
- status.webhookInfo.pendingUpdates,
428
- " pending)"
429
- ] }),
430
- status.webhookInfo.lastErrorMessage && /* @__PURE__ */ jsxs("div", { className: "mt-1 text-destructive", children: [
431
- "Last error: ",
432
- status.webhookInfo.lastErrorMessage
450
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
451
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
452
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-medium", children: "Webhook" }),
453
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mt-0.5", children: "Register a public URL with Telegram so it can deliver messages to your bot." }),
454
+ step2Done && !webhookEditing && /* @__PURE__ */ jsxs("div", { className: "mt-2 text-xs text-muted-foreground truncate", children: [
455
+ /* @__PURE__ */ jsx("span", { className: "font-mono", children: status.webhookInfo.url }),
456
+ status.webhookInfo.pendingUpdates > 0 && /* @__PURE__ */ jsxs("span", { className: "ml-2 text-yellow-500", children: [
457
+ "(",
458
+ status.webhookInfo.pendingUpdates,
459
+ " pending)"
460
+ ] }),
461
+ status.webhookInfo.lastErrorMessage && /* @__PURE__ */ jsxs("div", { className: "mt-1 text-destructive", children: [
462
+ "Last error: ",
463
+ status.webhookInfo.lastErrorMessage
464
+ ] })
433
465
  ] })
434
- ] })
435
- ] }) }),
436
- /* @__PURE__ */ jsxs("div", { className: "mt-3", children: [
437
- webhookError && /* @__PURE__ */ jsx("div", { className: "text-xs text-destructive mb-2", children: webhookError }),
438
- /* @__PURE__ */ jsx(
466
+ ] }),
467
+ step2Done && !webhookEditing && /* @__PURE__ */ jsx(
439
468
  "button",
440
469
  {
441
- onClick: handleRegisterWebhook,
442
- disabled: !step1Done || webhookSaving,
443
- className: "rounded-md bg-foreground text-background px-2.5 py-1.5 text-xs font-medium hover:bg-foreground/90 disabled:opacity-50 transition-colors",
444
- children: webhookSaving ? "Registering..." : step2Done ? "Re-register Webhook" : "Register Webhook"
470
+ onClick: startWebhookEdit,
471
+ className: "shrink-0 text-xs text-muted-foreground hover:text-foreground underline transition-colors",
472
+ children: "Change"
445
473
  }
446
474
  )
475
+ ] }),
476
+ (!step2Done || webhookEditing) && /* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-col gap-2", children: [
477
+ /* @__PURE__ */ jsx(
478
+ "input",
479
+ {
480
+ type: "text",
481
+ value: webhookUrlInput,
482
+ onChange: (e) => {
483
+ if (!webhookEditing) setWebhookEditing(true);
484
+ setWebhookUrlInput(e.target.value);
485
+ },
486
+ placeholder: "https://example.com/api/telegram/webhook",
487
+ spellCheck: false,
488
+ className: "rounded-md border border-border bg-background px-3 py-1.5 text-sm font-mono focus:outline-none focus:ring-1 focus:ring-foreground"
489
+ }
490
+ ),
491
+ webhookError && /* @__PURE__ */ jsx("div", { className: "text-xs text-destructive", children: webhookError }),
492
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
493
+ /* @__PURE__ */ jsx(
494
+ "button",
495
+ {
496
+ onClick: handleRegisterWebhook,
497
+ disabled: !step1Done || webhookSaving || !webhookUrlInput.trim(),
498
+ className: "rounded-md bg-foreground text-background px-2.5 py-1.5 text-xs font-medium hover:bg-foreground/90 disabled:opacity-50 transition-colors",
499
+ children: webhookSaving ? "Registering..." : step2Done ? "Re-register Webhook" : "Register Webhook"
500
+ }
501
+ ),
502
+ webhookEditing && step2Done && /* @__PURE__ */ jsx(
503
+ "button",
504
+ {
505
+ onClick: cancelWebhookEdit,
506
+ className: "rounded-md border border-border px-2.5 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors",
507
+ children: "Cancel"
508
+ }
509
+ ),
510
+ status.defaultWebhookUrl && webhookUrlInput.trim() !== status.defaultWebhookUrl && /* @__PURE__ */ jsx(
511
+ "button",
512
+ {
513
+ onClick: resetWebhookToDefault,
514
+ className: "ml-auto text-xs text-muted-foreground hover:text-foreground underline transition-colors",
515
+ children: "Reset to default"
516
+ }
517
+ )
518
+ ] })
447
519
  ] })
448
520
  ] })
449
521
  ] }) })
@@ -329,6 +329,8 @@ export function ApiKeysTelegramPage() {
329
329
  // Step 2 — webhook
330
330
  const [webhookSaving, setWebhookSaving] = useState(false);
331
331
  const [webhookError, setWebhookError] = useState(null);
332
+ const [webhookEditing, setWebhookEditing] = useState(false);
333
+ const [webhookUrlInput, setWebhookUrlInput] = useState('');
332
334
 
333
335
  const loadStatus = async () => {
334
336
  try {
@@ -343,6 +345,16 @@ export function ApiKeysTelegramPage() {
343
345
  loadStatus();
344
346
  }, []);
345
347
 
348
+ // Keep the webhook URL input in sync with the saved/effective URL when not
349
+ // actively editing — covers the "unregistered" case where the field needs
350
+ // to be prefilled with the default so the user can just hit Register.
351
+ useEffect(() => {
352
+ if (!status || webhookEditing) return;
353
+ setWebhookUrlInput(
354
+ status.webhookUrlOverride || status.webhookInfo?.url || status.defaultWebhookUrl || ''
355
+ );
356
+ }, [status, webhookEditing]);
357
+
346
358
  if (loading) {
347
359
  return <div className="h-48 animate-pulse rounded-md bg-border/50" />;
348
360
  }
@@ -383,12 +395,37 @@ export function ApiKeysTelegramPage() {
383
395
  const handleRegisterWebhook = async () => {
384
396
  setWebhookSaving(true);
385
397
  setWebhookError(null);
386
- const result = await registerTelegramWebhook();
387
- if (result?.error) setWebhookError(result.error);
398
+ const url = webhookEditing ? webhookUrlInput.trim() : undefined;
399
+ const result = await registerTelegramWebhook(url);
400
+ if (result?.error) {
401
+ setWebhookError(result.error);
402
+ setWebhookSaving(false);
403
+ return;
404
+ }
405
+ setWebhookEditing(false);
406
+ setWebhookUrlInput('');
388
407
  await loadStatus();
389
408
  setWebhookSaving(false);
390
409
  };
391
410
 
411
+ const startWebhookEdit = () => {
412
+ setWebhookError(null);
413
+ setWebhookUrlInput(
414
+ status.webhookUrlOverride || status.webhookInfo?.url || status.defaultWebhookUrl || ''
415
+ );
416
+ setWebhookEditing(true);
417
+ };
418
+
419
+ const cancelWebhookEdit = () => {
420
+ setWebhookEditing(false);
421
+ setWebhookUrlInput('');
422
+ setWebhookError(null);
423
+ };
424
+
425
+ const resetWebhookToDefault = () => {
426
+ setWebhookUrlInput(status.defaultWebhookUrl || '');
427
+ };
428
+
392
429
  return (
393
430
  <div>
394
431
  <div className="mb-4">
@@ -488,9 +525,9 @@ export function ApiKeysTelegramPage() {
488
525
  <div className="min-w-0">
489
526
  <h3 className="text-sm font-medium">Webhook</h3>
490
527
  <p className="text-xs text-muted-foreground mt-0.5">
491
- Register your public URL with Telegram so it can deliver messages to your bot.
528
+ Register a public URL with Telegram so it can deliver messages to your bot.
492
529
  </p>
493
- {step2Done && (
530
+ {step2Done && !webhookEditing && (
494
531
  <div className="mt-2 text-xs text-muted-foreground truncate">
495
532
  <span className="font-mono">{status.webhookInfo.url}</span>
496
533
  {status.webhookInfo.pendingUpdates > 0 && (
@@ -506,24 +543,62 @@ export function ApiKeysTelegramPage() {
506
543
  </div>
507
544
  )}
508
545
  </div>
509
- </div>
510
-
511
- <div className="mt-3">
512
- {webhookError && (
513
- <div className="text-xs text-destructive mb-2">{webhookError}</div>
546
+ {step2Done && !webhookEditing && (
547
+ <button
548
+ onClick={startWebhookEdit}
549
+ className="shrink-0 text-xs text-muted-foreground hover:text-foreground underline transition-colors"
550
+ >
551
+ Change
552
+ </button>
514
553
  )}
515
- <button
516
- onClick={handleRegisterWebhook}
517
- disabled={!step1Done || webhookSaving}
518
- className="rounded-md bg-foreground text-background px-2.5 py-1.5 text-xs font-medium hover:bg-foreground/90 disabled:opacity-50 transition-colors"
519
- >
520
- {webhookSaving
521
- ? 'Registering...'
522
- : step2Done
523
- ? 'Re-register Webhook'
524
- : 'Register Webhook'}
525
- </button>
526
554
  </div>
555
+
556
+ {(!step2Done || webhookEditing) && (
557
+ <div className="mt-3 flex flex-col gap-2">
558
+ <input
559
+ type="text"
560
+ value={webhookUrlInput}
561
+ onChange={(e) => {
562
+ if (!webhookEditing) setWebhookEditing(true);
563
+ setWebhookUrlInput(e.target.value);
564
+ }}
565
+ placeholder="https://example.com/api/telegram/webhook"
566
+ spellCheck={false}
567
+ className="rounded-md border border-border bg-background px-3 py-1.5 text-sm font-mono focus:outline-none focus:ring-1 focus:ring-foreground"
568
+ />
569
+ {webhookError && <div className="text-xs text-destructive">{webhookError}</div>}
570
+ <div className="flex items-center gap-2">
571
+ <button
572
+ onClick={handleRegisterWebhook}
573
+ disabled={!step1Done || webhookSaving || !webhookUrlInput.trim()}
574
+ className="rounded-md bg-foreground text-background px-2.5 py-1.5 text-xs font-medium hover:bg-foreground/90 disabled:opacity-50 transition-colors"
575
+ >
576
+ {webhookSaving
577
+ ? 'Registering...'
578
+ : step2Done
579
+ ? 'Re-register Webhook'
580
+ : 'Register Webhook'}
581
+ </button>
582
+ {webhookEditing && step2Done && (
583
+ <button
584
+ onClick={cancelWebhookEdit}
585
+ className="rounded-md border border-border px-2.5 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors"
586
+ >
587
+ Cancel
588
+ </button>
589
+ )}
590
+ {status.defaultWebhookUrl &&
591
+ webhookUrlInput.trim() !== status.defaultWebhookUrl && (
592
+ <button
593
+ onClick={resetWebhookToDefault}
594
+ className="ml-auto text-xs text-muted-foreground hover:text-foreground underline transition-colors"
595
+ >
596
+ Reset to default
597
+ </button>
598
+ )}
599
+ </div>
600
+ </div>
601
+ )}
527
602
  </div>
528
603
  </div>
529
604
  </div>
package/lib/config.js CHANGED
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * Config resolver. Reads from DB, falls back to defaults.
3
- * In-memory cache, invalidated on config writes.
4
3
  *
5
4
  * Usage:
6
5
  * import { getConfig } from '../config.js';
@@ -63,6 +62,7 @@ const CONFIG_KEYS = new Set([
63
62
  'CODE_MODE_GIT_ACTION',
64
63
  'AGENT_MODE_AUTO_RUN',
65
64
  'CODE_MODE_AUTO_RUN',
65
+ 'TELEGRAM_WEBHOOK_URL',
66
66
  ]);
67
67
 
68
68
  // Default values
@@ -87,23 +87,12 @@ const DEFAULTS = {
87
87
  CODE_MODE_AUTO_RUN: 'false',
88
88
  };
89
89
 
90
- // In-memory cache on globalThis to survive Next.js webpack chunk duplication.
91
- // Server actions and route handlers may be bundled into separate chunks, each
92
- // with their own copy of module-level variables. globalThis is shared across all chunks.
93
- const _cache = (globalThis.__popebotConfigCache ??= new Map());
94
-
95
90
  /**
96
91
  * Get a config value. Resolution: DB → default.
97
92
  * @param {string} key
98
93
  * @returns {string|undefined}
99
94
  */
100
95
  export function getConfig(key) {
101
- // Check cache first
102
- const cached = _cache.get(key);
103
- if (cached) {
104
- return cached.value;
105
- }
106
-
107
96
  let value;
108
97
 
109
98
  // OAuth tokens: multi-token support with LRU rotation
@@ -154,14 +143,5 @@ export function getConfig(key) {
154
143
  value = getDefaultModel(provider);
155
144
  }
156
145
 
157
- // Cache and return
158
- _cache.set(key, { value });
159
146
  return value;
160
147
  }
161
-
162
- /**
163
- * Invalidate the config cache. Call after any config write.
164
- */
165
- export function invalidateConfigCache() {
166
- _cache.clear();
167
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thepopebot",
3
- "version": "1.2.78",
3
+ "version": "1.2.82",
4
4
  "type": "module",
5
5
  "description": "Create autonomous AI agents with a two-layer architecture: Next.js Event Handler + Docker Agent.",
6
6
  "bin": {