thepopebot 1.2.76-beta.2 → 1.2.76-beta.21

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 (128) hide show
  1. package/README.md +3 -3
  2. package/api/CLAUDE.md +11 -4
  3. package/api/index.js +56 -18
  4. package/bin/CLAUDE.md +7 -4
  5. package/bin/cli.js +25 -45
  6. package/config/CLAUDE.md +23 -4
  7. package/drizzle/0021_coding_agent_workspace.sql +1 -0
  8. package/drizzle/0022_organic_apocalypse.sql +16 -0
  9. package/drizzle/0023_needy_ender_wiggin.sql +1 -0
  10. package/drizzle/meta/0021_snapshot.json +639 -0
  11. package/drizzle/meta/0022_snapshot.json +743 -0
  12. package/drizzle/meta/0023_snapshot.json +750 -0
  13. package/drizzle/meta/_journal.json +21 -0
  14. package/lib/CLAUDE.md +2 -2
  15. package/lib/actions.js +9 -1
  16. package/lib/ai/CLAUDE.md +72 -57
  17. package/lib/ai/helper-llm.js +108 -0
  18. package/lib/ai/index.js +308 -438
  19. package/lib/ai/line-mappers.js +42 -24
  20. package/lib/ai/scope.js +26 -0
  21. package/lib/ai/sdk-adapters/CLAUDE.md +114 -0
  22. package/lib/ai/sdk-adapters/claude-code.js +120 -8
  23. package/lib/ai/system-prompt.js +34 -0
  24. package/lib/ai/workspace-setup.js +19 -35
  25. package/lib/channels/CLAUDE.md +14 -4
  26. package/lib/channels/base.js +6 -2
  27. package/lib/channels/commands/index.js +42 -0
  28. package/lib/channels/commands/session.js +53 -0
  29. package/lib/channels/commands/verify.js +18 -0
  30. package/lib/channels/telegram.js +79 -28
  31. package/lib/chat/CLAUDE.md +4 -4
  32. package/lib/chat/actions.js +270 -49
  33. package/lib/chat/api.js +185 -31
  34. package/lib/chat/components/CLAUDE.md +6 -2
  35. package/lib/chat/components/chat-input.js +77 -47
  36. package/lib/chat/components/chat-input.jsx +77 -40
  37. package/lib/chat/components/chat-page.js +2 -0
  38. package/lib/chat/components/chat-page.jsx +3 -0
  39. package/lib/chat/components/chat.js +62 -14
  40. package/lib/chat/components/chat.jsx +68 -10
  41. package/lib/chat/components/code-mode-toggle.js +141 -22
  42. package/lib/chat/components/code-mode-toggle.jsx +129 -20
  43. package/lib/chat/components/containers-page.js +58 -40
  44. package/lib/chat/components/containers-page.jsx +64 -25
  45. package/lib/chat/components/crons-page.js +17 -3
  46. package/lib/chat/components/crons-page.jsx +34 -6
  47. package/lib/chat/components/index.js +2 -2
  48. package/lib/chat/components/message.js +18 -3
  49. package/lib/chat/components/message.jsx +18 -3
  50. package/lib/chat/components/profile-page.js +182 -4
  51. package/lib/chat/components/profile-page.jsx +196 -1
  52. package/lib/chat/components/scope-picker.js +21 -0
  53. package/lib/chat/components/scope-picker.jsx +27 -0
  54. package/lib/chat/components/settings-chat-page.js +11 -11
  55. package/lib/chat/components/settings-chat-page.jsx +14 -18
  56. package/lib/chat/components/settings-coding-agents-page.js +110 -16
  57. package/lib/chat/components/settings-coding-agents-page.jsx +87 -3
  58. package/lib/chat/components/settings-github-page.js +5 -0
  59. package/lib/chat/components/settings-github-page.jsx +5 -0
  60. package/lib/chat/components/settings-layout.js +3 -3
  61. package/lib/chat/components/settings-layout.jsx +3 -3
  62. package/lib/chat/components/settings-secrets-layout.js +1 -2
  63. package/lib/chat/components/settings-secrets-layout.jsx +1 -2
  64. package/lib/chat/components/settings-secrets-page.js +180 -75
  65. package/lib/chat/components/settings-secrets-page.jsx +212 -66
  66. package/lib/chat/components/triggers-page.js +17 -3
  67. package/lib/chat/components/triggers-page.jsx +34 -6
  68. package/lib/chat/components/ui/combobox.js +18 -2
  69. package/lib/chat/components/ui/combobox.jsx +17 -1
  70. package/lib/chat/components/ui/dropdown-menu.js +23 -2
  71. package/lib/chat/components/ui/dropdown-menu.jsx +27 -2
  72. package/lib/chat/telegram-profile.js +33 -0
  73. package/lib/cluster/CLAUDE.md +9 -3
  74. package/lib/code/CLAUDE.md +11 -3
  75. package/lib/code/actions.js +47 -8
  76. package/lib/code/terminal-view.js +31 -21
  77. package/lib/code/terminal-view.jsx +32 -23
  78. package/lib/config.js +15 -4
  79. package/lib/containers/CLAUDE.md +16 -6
  80. package/lib/db/CLAUDE.md +5 -2
  81. package/lib/db/chats.js +9 -17
  82. package/lib/db/code-workspaces.js +8 -3
  83. package/lib/db/config.js +0 -1
  84. package/lib/db/index.js +12 -0
  85. package/lib/db/schema.js +24 -1
  86. package/lib/db/user-channels.js +129 -0
  87. package/lib/llm-providers.js +8 -0
  88. package/lib/maintenance.js +31 -21
  89. package/lib/tools/CLAUDE.md +12 -3
  90. package/lib/tools/assemblyai.js +17 -0
  91. package/lib/tools/create-agent-job.js +12 -8
  92. package/lib/tools/docker.js +34 -10
  93. package/lib/tools/github.js +34 -0
  94. package/lib/tools/telegram.js +106 -0
  95. package/lib/utils/render-md.js +44 -18
  96. package/package.json +8 -8
  97. package/setup/CLAUDE.md +11 -5
  98. package/setup/lib/providers.mjs +2 -1
  99. package/setup/lib/targets.mjs +13 -16
  100. package/setup/lib/telegram.mjs +8 -69
  101. package/templates/.env.example +0 -7
  102. package/templates/.github/workflows/rebuild-event-handler.yml +1 -1
  103. package/templates/.gitignore.template +1 -3
  104. package/templates/CLAUDE.md +1 -1
  105. package/templates/CLAUDE.md.template +29 -7
  106. package/templates/agent-job/CLAUDE.md.template +5 -3
  107. package/templates/agent-job/CRONS.json +16 -0
  108. package/templates/agent-job/SYSTEM.md +16 -11
  109. package/templates/agents/CLAUDE.md.template +17 -17
  110. package/templates/coding-workspace/CLAUDE.md.template +7 -0
  111. package/templates/data/CLAUDE.md.template +1 -1
  112. package/templates/docker-compose.custom.yml +1 -0
  113. package/templates/docker-compose.yml +1 -0
  114. package/templates/event-handler/CLAUDE.md.template +79 -0
  115. package/templates/event-handler/TRIGGERS.json +18 -2
  116. package/templates/skills/CLAUDE.md.template +20 -22
  117. package/templates/skills/{library/agent-job-secrets → agent-job-secrets}/SKILL.md +2 -2
  118. package/lib/ai/agent.js +0 -65
  119. package/lib/ai/async-channel.js +0 -51
  120. package/lib/ai/model.js +0 -130
  121. package/lib/ai/tools.js +0 -164
  122. package/lib/tools/openai.js +0 -37
  123. package/setup/lib/telegram-verify.mjs +0 -63
  124. package/setup/setup-telegram.mjs +0 -260
  125. package/templates/agent-job/SOUL.md +0 -17
  126. /package/templates/{skills/active/.gitkeep → coding-workspace/SYSTEM.md} +0 -0
  127. /package/templates/skills/{library/agent-job-secrets → agent-job-secrets}/agent-job-secrets.js +0 -0
  128. /package/templates/skills/{library/playwright-cli → playwright-cli}/SKILL.md +0 -0
@@ -3,7 +3,17 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useState, useEffect } from "react";
4
4
  import { KeyIcon, CopyIcon, CheckIcon, TrashIcon, PlusIcon } from "./icons.js";
5
5
  import { SecretRow, EmptyState, formatDate, timeAgo } from "./settings-shared.js";
6
- import { createNewApiKey, getApiKeys, deleteApiKey, getApiKeySettings, updateApiKeySetting, regenerateWebhookSecret } from "../actions.js";
6
+ import {
7
+ createNewApiKey,
8
+ getApiKeys,
9
+ deleteApiKey,
10
+ getApiKeySettings,
11
+ updateApiKeySetting,
12
+ regenerateWebhookSecret,
13
+ getTelegramStatus,
14
+ validateTelegramToken,
15
+ registerTelegramWebhook
16
+ } from "../actions.js";
7
17
  function ApiKeysListPage() {
8
18
  const [keys, setKeys] = useState([]);
9
19
  const [loading, setLoading] = useState(true);
@@ -250,98 +260,193 @@ function ApiKeysVoicePage() {
250
260
  ) })
251
261
  ] });
252
262
  }
263
+ function StepIndicator({ n, state }) {
264
+ const cls = state === "done" ? "bg-green-500 text-white border-green-500" : state === "active" ? "border-foreground text-foreground" : "border-border text-muted-foreground";
265
+ return /* @__PURE__ */ jsx("div", { className: `flex h-6 w-6 shrink-0 items-center justify-center rounded-full border text-xs font-medium ${cls}`, children: state === "done" ? /* @__PURE__ */ jsx(CheckIcon, { className: "h-3 w-3" }) : n });
266
+ }
253
267
  function ApiKeysTelegramPage() {
254
- const [settings, setSettings] = useState(null);
268
+ const [status, setStatus] = useState(null);
255
269
  const [loading, setLoading] = useState(true);
256
- const [chatId, setChatId] = useState("");
257
- const [savingChatId, setSavingChatId] = useState(false);
258
- const [saving, setSaving] = useState(false);
259
- const loadSettings = async () => {
270
+ const [tokenInput, setTokenInput] = useState("");
271
+ const [tokenEditing, setTokenEditing] = useState(false);
272
+ const [tokenSaving, setTokenSaving] = useState(false);
273
+ const [tokenError, setTokenError] = useState(null);
274
+ const [webhookSaving, setWebhookSaving] = useState(false);
275
+ const [webhookError, setWebhookError] = useState(null);
276
+ const loadStatus = async () => {
260
277
  try {
261
- const result = await getApiKeySettings();
262
- setSettings(result);
263
- setChatId(result.telegramChatId || "");
264
- } catch {
278
+ const result = await getTelegramStatus();
279
+ setStatus(result);
265
280
  } finally {
266
281
  setLoading(false);
267
282
  }
268
283
  };
269
284
  useEffect(() => {
270
- loadSettings();
285
+ loadStatus();
271
286
  }, []);
272
- const getStatus = (key) => settings?.secrets?.find((s) => s.key === key)?.isSet || false;
273
- const handleSave = async (key, value) => {
274
- setSaving(true);
275
- await updateApiKeySetting(key, value);
276
- await loadSettings();
277
- setSaving(false);
287
+ if (loading) {
288
+ return /* @__PURE__ */ jsx("div", { className: "h-48 animate-pulse rounded-md bg-border/50" });
289
+ }
290
+ const step1Done = !!status.botInfo;
291
+ const step2Done = step1Done && status.webhookInfo?.url;
292
+ const handleSaveToken = async () => {
293
+ setTokenSaving(true);
294
+ setTokenError(null);
295
+ const validation = await validateTelegramToken(tokenInput.trim());
296
+ if (!validation.valid) {
297
+ setTokenError(validation.error || "Invalid token");
298
+ setTokenSaving(false);
299
+ return;
300
+ }
301
+ const saveResult = await updateApiKeySetting("TELEGRAM_BOT_TOKEN", tokenInput.trim());
302
+ if (saveResult?.error) {
303
+ setTokenError(saveResult.error);
304
+ setTokenSaving(false);
305
+ return;
306
+ }
307
+ setTokenInput("");
308
+ setTokenEditing(false);
309
+ await loadStatus();
310
+ setTokenSaving(false);
278
311
  };
279
- const handleRegenerate = async (key) => {
280
- setSaving(true);
281
- await regenerateWebhookSecret(key);
282
- await loadSettings();
283
- setSaving(false);
312
+ const handleClearToken = async () => {
313
+ setTokenSaving(true);
314
+ await updateApiKeySetting("TELEGRAM_BOT_TOKEN", "");
315
+ await loadStatus();
316
+ setTokenSaving(false);
284
317
  };
285
- const handleSaveChatId = async () => {
286
- setSavingChatId(true);
287
- await updateApiKeySetting("TELEGRAM_CHAT_ID", chatId);
288
- setSavingChatId(false);
318
+ const handleRegisterWebhook = async () => {
319
+ setWebhookSaving(true);
320
+ setWebhookError(null);
321
+ const result = await registerTelegramWebhook();
322
+ if (result?.error) setWebhookError(result.error);
323
+ await loadStatus();
324
+ setWebhookSaving(false);
289
325
  };
290
- if (loading) {
291
- return /* @__PURE__ */ jsx("div", { className: "h-24 animate-pulse rounded-md bg-border/50" });
292
- }
293
326
  return /* @__PURE__ */ jsxs("div", { children: [
294
327
  /* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
295
328
  /* @__PURE__ */ jsx("h2", { className: "text-base font-medium", children: "Telegram" }),
296
329
  /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Connect a Telegram bot to receive and send messages through your agent." })
297
330
  ] }),
298
- /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card p-4", children: [
299
- /* @__PURE__ */ jsxs("div", { className: "divide-y divide-border", children: [
300
- /* @__PURE__ */ jsx(
301
- SecretRow,
302
- {
303
- label: "Bot Token",
304
- isSet: getStatus("TELEGRAM_BOT_TOKEN"),
305
- saving,
306
- onSave: (val) => handleSave("TELEGRAM_BOT_TOKEN", val)
307
- }
308
- ),
309
- /* @__PURE__ */ jsx(
310
- SecretRow,
311
- {
312
- label: "Webhook Secret",
313
- isSet: getStatus("TELEGRAM_WEBHOOK_SECRET"),
314
- saving,
315
- onSave: (val) => handleSave("TELEGRAM_WEBHOOK_SECRET", val),
316
- onRegenerate: () => handleRegenerate("TELEGRAM_WEBHOOK_SECRET")
317
- }
318
- )
319
- ] }),
320
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 sm:flex-row sm:items-center pt-3 mt-3 border-t border-border", children: [
321
- /* @__PURE__ */ jsx("label", { className: "text-sm font-medium shrink-0", children: "Chat ID" }),
322
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-1", children: [
323
- /* @__PURE__ */ jsx(
324
- "input",
325
- {
326
- type: "text",
327
- value: chatId,
328
- onChange: (e) => setChatId(e.target.value),
329
- placeholder: "123456789",
330
- className: "flex-1 rounded-md border border-border bg-background px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-foreground",
331
- onKeyDown: (e) => e.key === "Enter" && handleSaveChatId()
332
- }
333
- ),
334
- /* @__PURE__ */ jsx(
335
- "button",
336
- {
337
- onClick: handleSaveChatId,
338
- disabled: savingChatId,
339
- className: "rounded-md px-2.5 py-1.5 text-xs font-medium border border-border text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50 transition-colors",
340
- children: savingChatId ? "Saving..." : "Save"
341
- }
342
- )
331
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
332
+ /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
333
+ /* @__PURE__ */ jsx(StepIndicator, { n: 1, state: step1Done ? "done" : "active" }),
334
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
335
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
336
+ /* @__PURE__ */ jsxs("div", { children: [
337
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-medium", children: "Bot Token" }),
338
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground mt-0.5", children: [
339
+ "Create a bot with",
340
+ " ",
341
+ /* @__PURE__ */ jsx(
342
+ "a",
343
+ {
344
+ href: "https://t.me/BotFather",
345
+ target: "_blank",
346
+ rel: "noopener noreferrer",
347
+ className: "underline hover:text-foreground",
348
+ children: "@BotFather"
349
+ }
350
+ ),
351
+ " ",
352
+ "and paste the token below."
353
+ ] })
354
+ ] }),
355
+ step1Done && !tokenEditing && /* @__PURE__ */ jsxs("div", { className: "shrink-0 text-right", children: [
356
+ /* @__PURE__ */ jsxs("div", { className: "text-sm font-medium", children: [
357
+ "@",
358
+ status.botInfo.username
359
+ ] }),
360
+ /* @__PURE__ */ jsx(
361
+ "button",
362
+ {
363
+ onClick: () => setTokenEditing(true),
364
+ className: "text-xs text-muted-foreground hover:text-foreground underline transition-colors",
365
+ children: "Change"
366
+ }
367
+ )
368
+ ] })
369
+ ] }),
370
+ (!step1Done || tokenEditing) && /* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-col gap-2", children: [
371
+ /* @__PURE__ */ jsx(
372
+ "input",
373
+ {
374
+ type: "password",
375
+ value: tokenInput,
376
+ onChange: (e) => setTokenInput(e.target.value),
377
+ placeholder: "123456789:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
378
+ className: "rounded-md border border-border bg-background px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-foreground",
379
+ onKeyDown: (e) => e.key === "Enter" && tokenInput.trim() && handleSaveToken()
380
+ }
381
+ ),
382
+ tokenError && /* @__PURE__ */ jsx("div", { className: "text-xs text-destructive", children: tokenError }),
383
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
384
+ /* @__PURE__ */ jsx(
385
+ "button",
386
+ {
387
+ onClick: handleSaveToken,
388
+ disabled: tokenSaving || !tokenInput.trim(),
389
+ 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",
390
+ children: tokenSaving ? "Validating..." : "Validate & Save"
391
+ }
392
+ ),
393
+ tokenEditing && /* @__PURE__ */ jsx(
394
+ "button",
395
+ {
396
+ onClick: () => {
397
+ setTokenEditing(false);
398
+ setTokenInput("");
399
+ setTokenError(null);
400
+ },
401
+ className: "rounded-md border border-border px-2.5 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors",
402
+ children: "Cancel"
403
+ }
404
+ ),
405
+ step1Done && tokenEditing && /* @__PURE__ */ jsx(
406
+ "button",
407
+ {
408
+ onClick: handleClearToken,
409
+ className: "ml-auto rounded-md border border-destructive text-destructive px-2.5 py-1.5 text-xs font-medium hover:bg-destructive/10 transition-colors",
410
+ children: "Remove"
411
+ }
412
+ )
413
+ ] })
414
+ ] })
343
415
  ] })
344
- ] })
416
+ ] }) }),
417
+ /* @__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
+ /* @__PURE__ */ jsx(StepIndicator, { n: 2, state: step2Done ? "done" : step1Done ? "active" : "pending" }),
419
+ /* @__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
433
+ ] })
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(
439
+ "button",
440
+ {
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"
445
+ }
446
+ )
447
+ ] })
448
+ ] })
449
+ ] }) })
345
450
  ] })
346
451
  ] });
347
452
  }
@@ -3,7 +3,17 @@
3
3
  import { useState, useEffect } from 'react';
4
4
  import { KeyIcon, CopyIcon, CheckIcon, TrashIcon, PlusIcon } from './icons.js';
5
5
  import { SecretRow, EmptyState, formatDate, timeAgo } from './settings-shared.js';
6
- import { createNewApiKey, getApiKeys, deleteApiKey, getApiKeySettings, updateApiKeySetting, regenerateWebhookSecret } from '../actions.js';
6
+ import {
7
+ createNewApiKey,
8
+ getApiKeys,
9
+ deleteApiKey,
10
+ getApiKeySettings,
11
+ updateApiKeySetting,
12
+ regenerateWebhookSecret,
13
+ getTelegramStatus,
14
+ validateTelegramToken,
15
+ registerTelegramWebhook,
16
+ } from '../actions.js';
7
17
 
8
18
  // ─────────────────────────────────────────────────────────────────────────────
9
19
  // Keys sub-tab — Multiple named API keys
@@ -288,100 +298,236 @@ export function ApiKeysVoicePage() {
288
298
  }
289
299
 
290
300
  // ─────────────────────────────────────────────────────────────────────────────
291
- // Telegram sub-tab — Bot Token + Webhook Secret + Chat ID
301
+ // Telegram sub-tab — Guided setup (bot token webhook chat verification)
292
302
  // ─────────────────────────────────────────────────────────────────────────────
293
303
 
304
+ function StepIndicator({ n, state }) {
305
+ // state: 'done' | 'active' | 'pending'
306
+ const cls =
307
+ state === 'done'
308
+ ? 'bg-green-500 text-white border-green-500'
309
+ : state === 'active'
310
+ ? 'border-foreground text-foreground'
311
+ : 'border-border text-muted-foreground';
312
+ return (
313
+ <div className={`flex h-6 w-6 shrink-0 items-center justify-center rounded-full border text-xs font-medium ${cls}`}>
314
+ {state === 'done' ? <CheckIcon className="h-3 w-3" /> : n}
315
+ </div>
316
+ );
317
+ }
318
+
294
319
  export function ApiKeysTelegramPage() {
295
- const [settings, setSettings] = useState(null);
320
+ const [status, setStatus] = useState(null);
296
321
  const [loading, setLoading] = useState(true);
297
- const [chatId, setChatId] = useState('');
298
- const [savingChatId, setSavingChatId] = useState(false);
299
- const [saving, setSaving] = useState(false);
300
322
 
301
- const loadSettings = async () => {
323
+ // Step 1 bot token
324
+ const [tokenInput, setTokenInput] = useState('');
325
+ const [tokenEditing, setTokenEditing] = useState(false);
326
+ const [tokenSaving, setTokenSaving] = useState(false);
327
+ const [tokenError, setTokenError] = useState(null);
328
+
329
+ // Step 2 — webhook
330
+ const [webhookSaving, setWebhookSaving] = useState(false);
331
+ const [webhookError, setWebhookError] = useState(null);
332
+
333
+ const loadStatus = async () => {
302
334
  try {
303
- const result = await getApiKeySettings();
304
- setSettings(result);
305
- setChatId(result.telegramChatId || '');
306
- } catch {
307
- // ignore
335
+ const result = await getTelegramStatus();
336
+ setStatus(result);
308
337
  } finally {
309
338
  setLoading(false);
310
339
  }
311
340
  };
312
341
 
313
342
  useEffect(() => {
314
- loadSettings();
343
+ loadStatus();
315
344
  }, []);
316
345
 
317
- const getStatus = (key) => settings?.secrets?.find((s) => s.key === key)?.isSet || false;
346
+ if (loading) {
347
+ return <div className="h-48 animate-pulse rounded-md bg-border/50" />;
348
+ }
318
349
 
319
- const handleSave = async (key, value) => {
320
- setSaving(true);
321
- await updateApiKeySetting(key, value);
322
- await loadSettings();
323
- setSaving(false);
350
+ const step1Done = !!status.botInfo;
351
+ const step2Done = step1Done && status.webhookInfo?.url;
352
+
353
+ // Step 1 handlers
354
+ const handleSaveToken = async () => {
355
+ setTokenSaving(true);
356
+ setTokenError(null);
357
+ const validation = await validateTelegramToken(tokenInput.trim());
358
+ if (!validation.valid) {
359
+ setTokenError(validation.error || 'Invalid token');
360
+ setTokenSaving(false);
361
+ return;
362
+ }
363
+ const saveResult = await updateApiKeySetting('TELEGRAM_BOT_TOKEN', tokenInput.trim());
364
+ if (saveResult?.error) {
365
+ setTokenError(saveResult.error);
366
+ setTokenSaving(false);
367
+ return;
368
+ }
369
+ setTokenInput('');
370
+ setTokenEditing(false);
371
+ await loadStatus();
372
+ setTokenSaving(false);
324
373
  };
325
374
 
326
- const handleRegenerate = async (key) => {
327
- setSaving(true);
328
- await regenerateWebhookSecret(key);
329
- await loadSettings();
330
- setSaving(false);
375
+ const handleClearToken = async () => {
376
+ setTokenSaving(true);
377
+ await updateApiKeySetting('TELEGRAM_BOT_TOKEN', '');
378
+ await loadStatus();
379
+ setTokenSaving(false);
331
380
  };
332
381
 
333
- const handleSaveChatId = async () => {
334
- setSavingChatId(true);
335
- await updateApiKeySetting('TELEGRAM_CHAT_ID', chatId);
336
- setSavingChatId(false);
382
+ // Step 2 handlers
383
+ const handleRegisterWebhook = async () => {
384
+ setWebhookSaving(true);
385
+ setWebhookError(null);
386
+ const result = await registerTelegramWebhook();
387
+ if (result?.error) setWebhookError(result.error);
388
+ await loadStatus();
389
+ setWebhookSaving(false);
337
390
  };
338
391
 
339
- if (loading) {
340
- return <div className="h-24 animate-pulse rounded-md bg-border/50" />;
341
- }
342
-
343
392
  return (
344
393
  <div>
345
394
  <div className="mb-4">
346
395
  <h2 className="text-base font-medium">Telegram</h2>
347
- <p className="text-sm text-muted-foreground">Connect a Telegram bot to receive and send messages through your agent.</p>
396
+ <p className="text-sm text-muted-foreground">
397
+ Connect a Telegram bot to receive and send messages through your agent.
398
+ </p>
348
399
  </div>
349
- <div className="rounded-lg border bg-card p-4">
350
- <div className="divide-y divide-border">
351
- <SecretRow
352
- label="Bot Token"
353
- isSet={getStatus('TELEGRAM_BOT_TOKEN')}
354
- saving={saving}
355
- onSave={(val) => handleSave('TELEGRAM_BOT_TOKEN', val)}
356
- />
357
- <SecretRow
358
- label="Webhook Secret"
359
- isSet={getStatus('TELEGRAM_WEBHOOK_SECRET')}
360
- saving={saving}
361
- onSave={(val) => handleSave('TELEGRAM_WEBHOOK_SECRET', val)}
362
- onRegenerate={() => handleRegenerate('TELEGRAM_WEBHOOK_SECRET')}
363
- />
400
+
401
+ <div className="space-y-3">
402
+ {/* ─── Step 1: Bot Token ─── */}
403
+ <div className="rounded-lg border bg-card p-4">
404
+ <div className="flex items-start gap-3">
405
+ <StepIndicator n={1} state={step1Done ? 'done' : 'active'} />
406
+ <div className="flex-1 min-w-0">
407
+ <div className="flex items-center justify-between gap-2">
408
+ <div>
409
+ <h3 className="text-sm font-medium">Bot Token</h3>
410
+ <p className="text-xs text-muted-foreground mt-0.5">
411
+ Create a bot with{' '}
412
+ <a
413
+ href="https://t.me/BotFather"
414
+ target="_blank"
415
+ rel="noopener noreferrer"
416
+ className="underline hover:text-foreground"
417
+ >
418
+ @BotFather
419
+ </a>{' '}
420
+ and paste the token below.
421
+ </p>
422
+ </div>
423
+ {step1Done && !tokenEditing && (
424
+ <div className="shrink-0 text-right">
425
+ <div className="text-sm font-medium">@{status.botInfo.username}</div>
426
+ <button
427
+ onClick={() => setTokenEditing(true)}
428
+ className="text-xs text-muted-foreground hover:text-foreground underline transition-colors"
429
+ >
430
+ Change
431
+ </button>
432
+ </div>
433
+ )}
434
+ </div>
435
+
436
+ {(!step1Done || tokenEditing) && (
437
+ <div className="mt-3 flex flex-col gap-2">
438
+ <input
439
+ type="password"
440
+ value={tokenInput}
441
+ onChange={(e) => setTokenInput(e.target.value)}
442
+ placeholder="123456789:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
443
+ className="rounded-md border border-border bg-background px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-foreground"
444
+ onKeyDown={(e) => e.key === 'Enter' && tokenInput.trim() && handleSaveToken()}
445
+ />
446
+ {tokenError && <div className="text-xs text-destructive">{tokenError}</div>}
447
+ <div className="flex items-center gap-2">
448
+ <button
449
+ onClick={handleSaveToken}
450
+ disabled={tokenSaving || !tokenInput.trim()}
451
+ 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"
452
+ >
453
+ {tokenSaving ? 'Validating...' : 'Validate & Save'}
454
+ </button>
455
+ {tokenEditing && (
456
+ <button
457
+ onClick={() => {
458
+ setTokenEditing(false);
459
+ setTokenInput('');
460
+ setTokenError(null);
461
+ }}
462
+ className="rounded-md border border-border px-2.5 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors"
463
+ >
464
+ Cancel
465
+ </button>
466
+ )}
467
+ {step1Done && tokenEditing && (
468
+ <button
469
+ onClick={handleClearToken}
470
+ className="ml-auto rounded-md border border-destructive text-destructive px-2.5 py-1.5 text-xs font-medium hover:bg-destructive/10 transition-colors"
471
+ >
472
+ Remove
473
+ </button>
474
+ )}
475
+ </div>
476
+ </div>
477
+ )}
478
+ </div>
479
+ </div>
364
480
  </div>
365
- <div className="flex flex-col gap-2 sm:flex-row sm:items-center pt-3 mt-3 border-t border-border">
366
- <label className="text-sm font-medium shrink-0">Chat ID</label>
367
- <div className="flex items-center gap-2 flex-1">
368
- <input
369
- type="text"
370
- value={chatId}
371
- onChange={(e) => setChatId(e.target.value)}
372
- placeholder="123456789"
373
- className="flex-1 rounded-md border border-border bg-background px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-foreground"
374
- onKeyDown={(e) => e.key === 'Enter' && handleSaveChatId()}
375
- />
376
- <button
377
- onClick={handleSaveChatId}
378
- disabled={savingChatId}
379
- className="rounded-md px-2.5 py-1.5 text-xs font-medium border border-border text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50 transition-colors"
380
- >
381
- {savingChatId ? 'Saving...' : 'Save'}
382
- </button>
481
+
482
+ {/* ─── Step 2: Webhook ─── */}
483
+ <div className={`rounded-lg border bg-card p-4 ${!step1Done ? 'opacity-50 pointer-events-none' : ''}`}>
484
+ <div className="flex items-start gap-3">
485
+ <StepIndicator n={2} state={step2Done ? 'done' : step1Done ? 'active' : 'pending'} />
486
+ <div className="flex-1 min-w-0">
487
+ <div className="flex items-center justify-between gap-2">
488
+ <div className="min-w-0">
489
+ <h3 className="text-sm font-medium">Webhook</h3>
490
+ <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.
492
+ </p>
493
+ {step2Done && (
494
+ <div className="mt-2 text-xs text-muted-foreground truncate">
495
+ <span className="font-mono">{status.webhookInfo.url}</span>
496
+ {status.webhookInfo.pendingUpdates > 0 && (
497
+ <span className="ml-2 text-yellow-500">
498
+ ({status.webhookInfo.pendingUpdates} pending)
499
+ </span>
500
+ )}
501
+ {status.webhookInfo.lastErrorMessage && (
502
+ <div className="mt-1 text-destructive">
503
+ Last error: {status.webhookInfo.lastErrorMessage}
504
+ </div>
505
+ )}
506
+ </div>
507
+ )}
508
+ </div>
509
+ </div>
510
+
511
+ <div className="mt-3">
512
+ {webhookError && (
513
+ <div className="text-xs text-destructive mb-2">{webhookError}</div>
514
+ )}
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
+ </div>
527
+ </div>
383
528
  </div>
384
529
  </div>
530
+
385
531
  </div>
386
532
  </div>
387
533
  );
@@ -40,9 +40,19 @@ function ActionCard({ action, index }) {
40
40
  ] }),
41
41
  type === "agent" && action.job && /* @__PURE__ */ jsxs("div", { children: [
42
42
  /* @__PURE__ */ jsx("pre", { className: "text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48", children: action.job }),
43
- (action.llm_provider || action.llm_model) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mt-2", children: [
44
- /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "LLM:" }),
45
- /* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-full bg-purple-500/10 text-purple-500 px-2 py-0.5 text-[10px] font-medium", children: [action.llm_provider, action.llm_model].filter(Boolean).join(" / ") })
43
+ (action.agent_backend || action.llm_model || action.scope) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mt-2 flex-wrap", children: [
44
+ action.agent_backend && /* @__PURE__ */ jsxs(Fragment, { children: [
45
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Agent:" }),
46
+ /* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-full bg-purple-500/10 text-purple-500 px-2 py-0.5 text-[10px] font-medium", children: action.agent_backend })
47
+ ] }),
48
+ action.llm_model && /* @__PURE__ */ jsxs(Fragment, { children: [
49
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Model:" }),
50
+ /* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-full bg-purple-500/10 text-purple-500 px-2 py-0.5 text-[10px] font-medium", children: action.llm_model })
51
+ ] }),
52
+ action.scope && /* @__PURE__ */ jsxs(Fragment, { children: [
53
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Scope:" }),
54
+ /* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-full bg-purple-500/10 text-purple-500 px-2 py-0.5 text-[10px] font-mono", children: action.scope })
55
+ ] })
46
56
  ] })
47
57
  ] }),
48
58
  type === "command" && action.command && /* @__PURE__ */ jsx("pre", { className: "text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48", children: action.command }),
@@ -54,6 +64,10 @@ function ActionCard({ action, index }) {
54
64
  action.vars && Object.keys(action.vars).length > 0 && /* @__PURE__ */ jsxs("div", { children: [
55
65
  /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground mb-1", children: "Variables" }),
56
66
  /* @__PURE__ */ jsx("pre", { className: "text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48", children: JSON.stringify(action.vars, null, 2) })
67
+ ] }),
68
+ action.headers && Object.keys(action.headers).length > 0 && /* @__PURE__ */ jsxs("div", { children: [
69
+ /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground mb-1", children: "Headers" }),
70
+ /* @__PURE__ */ jsx("pre", { className: "text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48", children: JSON.stringify(action.headers, null, 2) })
57
71
  ] })
58
72
  ] })
59
73
  ] });