thepopebot 1.2.76-beta.20 → 1.2.76-beta.22

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/README.md CHANGED
@@ -104,7 +104,7 @@ The wizard walks you through everything:
104
104
  **That's it.** Visit your APP_URL when the wizard finishes.
105
105
 
106
106
  - **Web Chat**: Visit your APP_URL to chat with your agent, create jobs, upload files
107
- - **Telegram** (optional): Run `npm run setup-telegram` to connect a Telegram bot
107
+ - **Telegram** (optional): Connect a Telegram bot from `/admin/event-handler/telegram`
108
108
  - **Webhook**: Send a POST to `/api/create-agent-job` with your API key to create jobs programmatically
109
109
  - **Cron**: Edit `agent-job/CRONS.json` to schedule recurring jobs
110
110
 
@@ -143,9 +143,9 @@ See [Coding Agents](docs/CODING_AGENTS.md) for details on all five agent backend
143
143
  > ```bash
144
144
  > # Update .env and GitHub variable in one command:
145
145
  > npx thepopebot set-var APP_URL https://your-new-url.ngrok.io
146
- > # If Telegram is configured, re-register the webhook:
147
- > npm run setup-telegram
148
146
  > ```
147
+ >
148
+ > If Telegram is configured, click **Re-register webhook** at `/admin/event-handler/telegram` after the URL change.
149
149
 
150
150
  ---
151
151
 
package/bin/CLAUDE.md CHANGED
@@ -8,7 +8,6 @@ Entry point: `cli.js` (invoked via `npx thepopebot <command>`).
8
8
  |---------|---------|
9
9
  | `init [--no-managed] [--no-install]` | Scaffold project from templates, sync managed files, create `.env`, install deps |
10
10
  | `setup` | Run interactive setup wizard (see `setup/CLAUDE.md`) |
11
- | `setup-telegram` | Reconfigure Telegram webhook |
12
11
  | `setup-ssl` | Configure SSL with Let's Encrypt wildcard cert |
13
12
  | `upgrade [@beta\|version]` | Upgrade package, run init, rebuild, commit, push, restart Docker |
14
13
  | `reset [file]` | Restore a template file to defaults |
package/bin/cli.js CHANGED
@@ -53,7 +53,6 @@ Commands:
53
53
  upgrade|update [@beta|version] Upgrade thepopebot (install, init, build, commit, push)
54
54
  setup Run interactive setup wizard
55
55
  setup-ssl Configure SSL with Let's Encrypt wildcard cert
56
- setup-telegram Reconfigure Telegram webhook
57
56
  reset-auth Regenerate AUTH_SECRET (invalidates all sessions)
58
57
  reset [file] Restore a template file (or list available templates)
59
58
  reset-all Nuclear reset — restore entire project to fresh init state
@@ -256,7 +255,6 @@ async function init(options = {}) {
256
255
  type: 'module',
257
256
  scripts: {
258
257
  setup: 'thepopebot setup',
259
- 'setup-telegram': 'thepopebot setup-telegram',
260
258
  'reset-auth': 'thepopebot reset-auth',
261
259
  },
262
260
  dependencies: {
@@ -783,15 +781,6 @@ function setupSsl() {
783
781
  }
784
782
  }
785
783
 
786
- function setupTelegram() {
787
- const setupScript = path.join(__dirname, '..', 'setup', 'setup-telegram.mjs');
788
- try {
789
- execFileSync(process.execPath, [setupScript], { stdio: 'inherit', cwd: process.cwd() });
790
- } catch {
791
- process.exit(1);
792
- }
793
- }
794
-
795
784
  async function resetAuth() {
796
785
  const { randomBytes } = await import('crypto');
797
786
  const { updateEnvVariable } = await import(path.join(__dirname, '..', 'setup', 'lib', 'auth.mjs'));
@@ -1066,9 +1055,6 @@ switch (command) {
1066
1055
  case 'setup-ssl':
1067
1056
  setupSsl();
1068
1057
  break;
1069
- case 'setup-telegram':
1070
- setupTelegram();
1071
- break;
1072
1058
  case 'reset-auth':
1073
1059
  await resetAuth();
1074
1060
  break;
@@ -1373,6 +1373,7 @@ export async function getGitHubConfig() {
1373
1373
  }
1374
1374
 
1375
1375
  // Build variables list: known names + any extras, with current value
1376
+ const variablesError = !Array.isArray(remoteVariables) ? (remoteVariables?.error || 'Failed to load variables') : null;
1376
1377
  const remoteVarMap = new Map(
1377
1378
  Array.isArray(remoteVariables) ? remoteVariables.map((v) => [v.name, v.value]) : []
1378
1379
  );
@@ -1383,7 +1384,7 @@ export async function getGitHubConfig() {
1383
1384
  }
1384
1385
  }
1385
1386
 
1386
- return { secrets, variables };
1387
+ return { secrets, variables, variablesError };
1387
1388
  }
1388
1389
 
1389
1390
  /**
@@ -68,12 +68,12 @@ function DefaultAgentSection({ settings, onReload }) {
68
68
  };
69
69
  return /* @__PURE__ */ jsxs("div", { children: [
70
70
  /* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
71
- /* @__PURE__ */ jsx("h2", { className: "text-base font-medium", children: "Default Coding Agent" }),
71
+ /* @__PURE__ */ jsx("h2", { className: "text-base font-medium", children: "Coding Agent" }),
72
72
  /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Select which coding agent runs headless tasks and code workspaces." })
73
73
  ] }),
74
74
  /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card p-4 space-y-3", children: [
75
75
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
76
- /* @__PURE__ */ jsx("label", { className: "text-sm font-medium shrink-0", children: "Agent" }),
76
+ /* @__PURE__ */ jsx("label", { className: "text-sm font-medium shrink-0", children: "Default Coding Agent" }),
77
77
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
78
78
  saving && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Saving..." }),
79
79
  saved && /* @__PURE__ */ jsxs("span", { className: "text-xs text-green-500 inline-flex items-center gap-1", children: [
@@ -91,12 +91,12 @@ function DefaultAgentSection({ settings, onReload }) {
91
91
  return (
92
92
  <div>
93
93
  <div className="mb-4">
94
- <h2 className="text-base font-medium">Default Coding Agent</h2>
94
+ <h2 className="text-base font-medium">Coding Agent</h2>
95
95
  <p className="text-sm text-muted-foreground">Select which coding agent runs headless tasks and code workspaces.</p>
96
96
  </div>
97
97
  <div className="rounded-lg border bg-card p-4 space-y-3">
98
98
  <div className="flex items-center justify-between">
99
- <label className="text-sm font-medium shrink-0">Agent</label>
99
+ <label className="text-sm font-medium shrink-0">Default Coding Agent</label>
100
100
  <div className="flex items-center gap-3">
101
101
  {saving && <span className="text-xs text-muted-foreground">Saving...</span>}
102
102
  {saved && <span className="text-xs text-green-500 inline-flex items-center gap-1"><CheckIcon size={12} /> Saved</span>}
@@ -354,6 +354,11 @@ function GitHubVariablesPage() {
354
354
  onCancel: () => setShowAdd(false)
355
355
  }
356
356
  ),
357
+ data.variablesError && /* @__PURE__ */ jsxs("div", { className: "mb-4 rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive", children: [
358
+ "Couldn't read GitHub variables: ",
359
+ data.variablesError,
360
+ ". Statuses below may be inaccurate. Check that the GitHub PAT has Variables: Read scope."
361
+ ] }),
357
362
  /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-4", children: /* @__PURE__ */ jsx("div", { className: "divide-y divide-border", children: data.variables.map((v) => /* @__PURE__ */ jsx(VariableRow, { name: v.name, isSet: v.isSet, currentValue: v.value, onUpdate: handleUpdate, onDelete: handleDelete }, v.name)) }) })
358
363
  ] });
359
364
  }
@@ -389,6 +389,11 @@ export function GitHubVariablesPage() {
389
389
  onAdd={handleUpdate}
390
390
  onCancel={() => setShowAdd(false)}
391
391
  />
392
+ {data.variablesError && (
393
+ <div className="mb-4 rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive">
394
+ Couldn't read GitHub variables: {data.variablesError}. Statuses below may be inaccurate. Check that the GitHub PAT has Variables: Read scope.
395
+ </div>
396
+ )}
392
397
  <div className="rounded-lg border bg-card p-4">
393
398
  <div className="divide-y divide-border">
394
399
  {data.variables.map((v) => (
@@ -4,12 +4,12 @@ import { useState, useEffect } from "react";
4
4
  import { PageLayout } from "./page-layout.js";
5
5
  import { UserIcon, ClockIcon, ZapIcon, MessageIcon, GitBranchIcon, SettingsIcon } from "./icons.js";
6
6
  const TABS = [
7
+ { id: "general", label: "General", href: "/admin/general", icon: SettingsIcon },
7
8
  { id: "event-handler", label: "Event Handler", href: "/admin/event-handler", icon: MessageIcon },
8
- { id: "github", label: "GitHub", href: "/admin/github", icon: GitBranchIcon },
9
- { id: "users", label: "Users", href: "/admin/users", icon: UserIcon },
10
9
  { id: "crons", label: "Crons", href: "/admin/crons", icon: ClockIcon },
11
10
  { id: "triggers", label: "Triggers", href: "/admin/triggers", icon: ZapIcon },
12
- { id: "general", label: "General", href: "/admin/general", icon: SettingsIcon }
11
+ { id: "users", label: "Users", href: "/admin/users", icon: UserIcon },
12
+ { id: "github", label: "GitHub", href: "/admin/github", icon: GitBranchIcon }
13
13
  ];
14
14
  function SettingsLayout({ session, children }) {
15
15
  const [activePath, setActivePath] = useState("");
@@ -5,12 +5,12 @@ import { PageLayout } from './page-layout.js';
5
5
  import { UserIcon, ClockIcon, ZapIcon, MessageIcon, GitBranchIcon, SettingsIcon } from './icons.js';
6
6
 
7
7
  const TABS = [
8
+ { id: 'general', label: 'General', href: '/admin/general', icon: SettingsIcon },
8
9
  { id: 'event-handler', label: 'Event Handler', href: '/admin/event-handler', icon: MessageIcon },
9
- { id: 'github', label: 'GitHub', href: '/admin/github', icon: GitBranchIcon },
10
- { id: 'users', label: 'Users', href: '/admin/users', icon: UserIcon },
11
10
  { id: 'crons', label: 'Crons', href: '/admin/crons', icon: ClockIcon },
12
11
  { id: 'triggers', label: 'Triggers', href: '/admin/triggers', icon: ZapIcon },
13
- { id: 'general', label: 'General', href: '/admin/general', icon: SettingsIcon },
12
+ { id: 'users', label: 'Users', href: '/admin/users', icon: UserIcon },
13
+ { id: 'github', label: 'GitHub', href: '/admin/github', icon: GitBranchIcon },
14
14
  ];
15
15
 
16
16
  export function SettingsLayout({ session, children }) {
@@ -37,7 +37,6 @@ const EVENT_HANDLER_TABS = [
37
37
  ];
38
38
  const GITHUB_TABS = [
39
39
  { id: "tokens", label: "Tokens", href: "/admin/github/tokens" },
40
- { id: "secrets", label: "Secrets", href: "/admin/github/secrets" },
41
40
  { id: "variables", label: "Variables", href: "/admin/github/variables" }
42
41
  ];
43
42
  function ApiKeysLayout({ children }) {
@@ -60,7 +60,6 @@ const EVENT_HANDLER_TABS = [
60
60
 
61
61
  const GITHUB_TABS = [
62
62
  { id: 'tokens', label: 'Tokens', href: '/admin/github/tokens' },
63
- { id: 'secrets', label: 'Secrets', href: '/admin/github/secrets' },
64
63
  { id: 'variables', label: 'Variables', href: '/admin/github/variables' },
65
64
  ];
66
65
 
@@ -780,32 +780,50 @@ function TerminalView({ codeWorkspaceId, wsPath, isActive = true, showToolbar =
780
780
  ] }) })
781
781
  ] });
782
782
  }
783
- const STORAGE_KEY = "thepopebot-workspace-command:code";
783
+ const STORAGE_KEY = "thepopebot-workspace-command";
784
+ const FALLBACK_BY_MODE = { agent: "push", code: "create-pr" };
784
785
  function ToolbarCommandButton({ codeWorkspaceId, diffStats, onDiffStatsRefresh, onShowDiff }) {
786
+ const [chatMode, setChatMode] = useState("code");
787
+ useEffect(() => {
788
+ if (!codeWorkspaceId) return;
789
+ let cancelled = false;
790
+ fetch(`/code/${codeWorkspaceId}/chat-data`).then((r) => r.json()).then((data) => {
791
+ if (cancelled) return;
792
+ if (data?.chatMode) setChatMode(data.chatMode);
793
+ }).catch(() => {
794
+ });
795
+ return () => {
796
+ cancelled = true;
797
+ };
798
+ }, [codeWorkspaceId]);
799
+ const storageKey = `${STORAGE_KEY}:${chatMode}`;
785
800
  const [selectedCommand, setSelectedCommandState] = useState(() => {
786
801
  try {
787
- return localStorage.getItem(STORAGE_KEY) || "create-pr";
802
+ return localStorage.getItem(storageKey) || FALLBACK_BY_MODE[chatMode] || "create-pr";
788
803
  } catch {
789
- return "create-pr";
804
+ return FALLBACK_BY_MODE[chatMode] || "create-pr";
790
805
  }
791
806
  });
792
807
  const setSelectedCommand = (cmd) => {
793
808
  setSelectedCommandState(cmd);
794
809
  try {
795
- localStorage.setItem(STORAGE_KEY, cmd);
810
+ localStorage.setItem(storageKey, cmd);
796
811
  } catch {
797
812
  }
798
813
  };
799
814
  useEffect(() => {
800
815
  let stored = null;
801
816
  try {
802
- stored = localStorage.getItem(STORAGE_KEY);
817
+ stored = localStorage.getItem(storageKey);
803
818
  } catch {
804
819
  }
805
- if (stored) return;
820
+ if (stored) {
821
+ setSelectedCommandState(stored);
822
+ return;
823
+ }
806
824
  let cancelled = false;
807
825
  import("../chat/actions.js").then(({ getModeGitActionDefault }) => {
808
- getModeGitActionDefault("code").then((val) => {
826
+ getModeGitActionDefault(chatMode).then((val) => {
809
827
  if (cancelled || !val) return;
810
828
  setSelectedCommandState(val);
811
829
  }).catch(() => {
@@ -815,7 +833,7 @@ function ToolbarCommandButton({ codeWorkspaceId, diffStats, onDiffStatsRefresh,
815
833
  return () => {
816
834
  cancelled = true;
817
835
  };
818
- }, []);
836
+ }, [chatMode, storageKey]);
819
837
  const [commandRunning, setCommandRunning] = useState(false);
820
838
  const [dialogOpen, setDialogOpen] = useState(false);
821
839
  const [commandOutput, setCommandOutput] = useState("");
@@ -842,31 +842,54 @@ export default function TerminalView({ codeWorkspaceId, wsPath, isActive = true,
842
842
  );
843
843
  }
844
844
 
845
- const STORAGE_KEY = 'thepopebot-workspace-command:code';
845
+ const STORAGE_KEY = 'thepopebot-workspace-command';
846
+ const FALLBACK_BY_MODE = { agent: 'push', code: 'create-pr' };
846
847
 
847
848
  function ToolbarCommandButton({ codeWorkspaceId, diffStats, onDiffStatsRefresh, onShowDiff }) {
849
+ // Resolve chatMode from the workspace's chat (matches code-mode-toggle's per-mode behavior).
850
+ // Defaults to 'code' until the fetch resolves, since code workspaces are most often code-mode.
851
+ const [chatMode, setChatMode] = useState('code');
852
+
853
+ useEffect(() => {
854
+ if (!codeWorkspaceId) return;
855
+ let cancelled = false;
856
+ fetch(`/code/${codeWorkspaceId}/chat-data`)
857
+ .then((r) => r.json())
858
+ .then((data) => {
859
+ if (cancelled) return;
860
+ if (data?.chatMode) setChatMode(data.chatMode);
861
+ })
862
+ .catch(() => {});
863
+ return () => { cancelled = true; };
864
+ }, [codeWorkspaceId]);
865
+
866
+ const storageKey = `${STORAGE_KEY}:${chatMode}`;
848
867
  const [selectedCommand, setSelectedCommandState] = useState(() => {
849
- try { return localStorage.getItem(STORAGE_KEY) || 'create-pr'; } catch { return 'create-pr'; }
868
+ try { return localStorage.getItem(storageKey) || FALLBACK_BY_MODE[chatMode] || 'create-pr'; }
869
+ catch { return FALLBACK_BY_MODE[chatMode] || 'create-pr'; }
850
870
  });
851
871
  const setSelectedCommand = (cmd) => {
852
872
  setSelectedCommandState(cmd);
853
- try { localStorage.setItem(STORAGE_KEY, cmd); } catch {}
873
+ try { localStorage.setItem(storageKey, cmd); } catch {}
854
874
  };
855
875
 
856
- // Seed from admin default if user hasn't picked anything for code mode yet.
876
+ // When chatMode resolves (or changes), reload preference from its storage key, then seed from admin default if unset.
857
877
  useEffect(() => {
858
878
  let stored = null;
859
- try { stored = localStorage.getItem(STORAGE_KEY); } catch {}
860
- if (stored) return;
879
+ try { stored = localStorage.getItem(storageKey); } catch {}
880
+ if (stored) {
881
+ setSelectedCommandState(stored);
882
+ return;
883
+ }
861
884
  let cancelled = false;
862
885
  import('../chat/actions.js').then(({ getModeGitActionDefault }) => {
863
- getModeGitActionDefault('code').then((val) => {
886
+ getModeGitActionDefault(chatMode).then((val) => {
864
887
  if (cancelled || !val) return;
865
888
  setSelectedCommandState(val);
866
889
  }).catch(() => {});
867
890
  }).catch(() => {});
868
891
  return () => { cancelled = true; };
869
- }, []);
892
+ }, [chatMode, storageKey]);
870
893
  const [commandRunning, setCommandRunning] = useState(false);
871
894
  const [dialogOpen, setDialogOpen] = useState(false);
872
895
  const [commandOutput, setCommandOutput] = useState('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thepopebot",
3
- "version": "1.2.76-beta.20",
3
+ "version": "1.2.76-beta.22",
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": {
@@ -15,22 +15,22 @@
15
15
  */
16
16
  export const CONFIG_TARGETS = {
17
17
  // Secrets → DB encrypted (never .env)
18
- GH_TOKEN: { env: true, dbSecret: true, secret: 'AGENT_GH_TOKEN' },
19
- ANTHROPIC_API_KEY: { dbSecret: true, secret: 'AGENT_ANTHROPIC_API_KEY' },
20
- OPENAI_API_KEY: { dbSecret: true, secret: 'AGENT_OPENAI_API_KEY' },
21
- GOOGLE_API_KEY: { dbSecret: true, secret: 'AGENT_GOOGLE_API_KEY' },
22
- CUSTOM_API_KEY: { dbSecret: true, secret: 'AGENT_CUSTOM_API_KEY' },
23
- MOONSHOT_API_KEY: { dbSecret: true, secret: 'AGENT_MOONSHOT_API_KEY' },
24
- CLAUDE_CODE_OAUTH_TOKEN: { dbSecret: true, secret: 'AGENT_CLAUDE_CODE_OAUTH_TOKEN' },
18
+ GH_TOKEN: { env: true, dbSecret: true },
19
+ ANTHROPIC_API_KEY: { dbSecret: true },
20
+ OPENAI_API_KEY: { dbSecret: true },
21
+ GOOGLE_API_KEY: { dbSecret: true },
22
+ CUSTOM_API_KEY: { dbSecret: true },
23
+ MOONSHOT_API_KEY: { dbSecret: true },
24
+ CLAUDE_CODE_OAUTH_TOKEN: { dbSecret: true },
25
25
  GH_WEBHOOK_SECRET: { dbSecret: true, secret: true },
26
26
  TELEGRAM_BOT_TOKEN: { dbSecret: true },
27
27
  TELEGRAM_WEBHOOK_SECRET: { dbSecret: true },
28
28
 
29
29
  // Plain config → DB (not .env)
30
- LLM_PROVIDER: { db: true, variable: true },
31
- LLM_MODEL: { db: true, variable: true },
32
- CUSTOM_OPENAI_BASE_URL: { db: true, variable: true },
33
- AGENT_BACKEND: { db: true, variable: true },
30
+ LLM_PROVIDER: { db: true },
31
+ LLM_MODEL: { db: true },
32
+ CUSTOM_OPENAI_BASE_URL: { db: true },
33
+ AGENT_BACKEND: { db: true },
34
34
 
35
35
  // Infrastructure → .env only (needed before DB is available)
36
36
  GH_OWNER: { env: true },
@@ -38,8 +38,7 @@ export const CONFIG_TARGETS = {
38
38
  APP_URL: { env: true, variable: true },
39
39
  APP_HOSTNAME: { env: true },
40
40
 
41
- // GitHub-only
42
- BRAVE_API_KEY: { secret: 'AGENT_LLM_BRAVE_API_KEY' },
41
+ // GitHub variables consumed by scaffolded workflows
43
42
  AUTO_MERGE: { variable: true, default: 'true', firstRunOnly: true },
44
43
  ALLOWED_PATHS: { variable: true, default: '/logs,/agents', firstRunOnly: true },
45
44
  RUNS_ON: { variable: true },
@@ -1,229 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import chalk from 'chalk';
4
- import * as clack from '@clack/prompts';
5
-
6
- import { checkPrerequisites } from './lib/prerequisites.mjs';
7
- import { setVariables, setSecrets } from './lib/github.mjs';
8
- import { setTelegramWebhook, validateBotToken, getBotFatherURL } from './lib/telegram.mjs';
9
- import { confirm, keepOrReconfigure, generateTelegramWebhookSecret, promptForOptionalKey, maskSecret, openOrShowURL } from './lib/prompts.mjs';
10
- import { updateEnvVariable } from './lib/auth.mjs';
11
- import { loadEnvFile } from './lib/env.mjs';
12
-
13
- const logo = `
14
- _____ _ ____ ____ _
15
- |_ _| |__ ___| _ \\ ___ _ __ ___| __ ) ___ | |_
16
- | | | '_ \\ / _ \\ |_) / _ \\| '_ \\ / _ \\ _ \\ / _ \\| __|
17
- | | | | | | __/ __/ (_) | |_) | __/ |_) | (_) | |_
18
- |_| |_| |_|\\___|_| \\___/| .__/ \\___|____/ \\___/ \\__|
19
- |_|
20
- `;
21
-
22
- async function main() {
23
- console.log(chalk.cyan(logo));
24
- clack.intro('Telegram Setup');
25
- clack.log.info('Connect a Telegram bot to your agent. This wizard will walk you through creating a bot, registering a webhook, and linking your chat.');
26
-
27
- const TOTAL_STEPS = 5;
28
- let currentStep = 0;
29
-
30
- // Track values for summary
31
- let botUsername = null;
32
- let webhookUrl = null;
33
-
34
- // ─── Step 1: Prerequisites ──────────────────────────────────────────
35
- clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] Checking prerequisites`);
36
- clack.log.info('Verifying git remote and loading existing configuration.');
37
-
38
- const prereqs = await checkPrerequisites();
39
-
40
- if (!prereqs.git.remoteInfo) {
41
- clack.log.error('Could not detect GitHub repository from git remote.');
42
- clack.cancel('Run setup first to initialize your repository.');
43
- process.exit(1);
44
- }
45
-
46
- const { owner, repo } = prereqs.git.remoteInfo;
47
- clack.log.success(`Repository: ${owner}/${repo}`);
48
-
49
- const env = loadEnvFile();
50
-
51
- if (env) {
52
- clack.log.info('Existing .env detected — previously configured values can be skipped.');
53
- }
54
-
55
- // ─── Step 2: App URL ────────────────────────────────────────────────
56
- clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] App URL`);
57
- clack.log.info('Your bot needs a public HTTPS URL so Telegram can deliver messages to it via webhook.');
58
- clack.log.warn('Make sure your server is running and publicly accessible.');
59
-
60
- let appUrl = null;
61
-
62
- if (await keepOrReconfigure('APP_URL', env?.APP_URL || null)) {
63
- appUrl = env.APP_URL;
64
- }
65
-
66
- if (!appUrl) {
67
- clack.log.info(
68
- 'Enter the public URL where your agent is running.\n' +
69
- ' Examples:\n' +
70
- ' ngrok: https://abc123.ngrok.io\n' +
71
- ' VPS: https://mybot.example.com\n' +
72
- ' PaaS: https://mybot.vercel.app'
73
- );
74
-
75
- while (!appUrl) {
76
- const url = await clack.text({
77
- message: 'Enter your APP_URL (https://...):',
78
- validate: (input) => {
79
- if (!input) return 'URL is required';
80
- if (!input.startsWith('https://')) return 'URL must start with https://';
81
- },
82
- });
83
- if (clack.isCancel(url)) {
84
- clack.cancel('Setup cancelled.');
85
- process.exit(0);
86
- }
87
- appUrl = url.replace(/\/$/, '');
88
- }
89
- }
90
-
91
- // Update APP_URL and APP_HOSTNAME in .env
92
- const appHostname = new URL(appUrl).hostname;
93
- updateEnvVariable('APP_URL', appUrl);
94
- updateEnvVariable('APP_HOSTNAME', appHostname);
95
- clack.log.success('APP_URL saved to .env');
96
-
97
- // Set APP_URL variable on GitHub
98
- const urlSpinner = clack.spinner();
99
- urlSpinner.start('Updating APP_URL variable on GitHub...');
100
- const urlResult = await setVariables(owner, repo, { APP_URL: appUrl });
101
- if (urlResult.APP_URL.success) {
102
- urlSpinner.stop('APP_URL variable updated on GitHub');
103
- } else {
104
- urlSpinner.stop(`Failed: ${urlResult.APP_URL.error}`);
105
- }
106
-
107
- // ─── Step 3: Bot Token ──────────────────────────────────────────────
108
- clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] Telegram Bot Token`);
109
- clack.log.info('Your agent needs a Telegram bot token from @BotFather to send and receive messages.');
110
-
111
- let token = null;
112
-
113
- if (await keepOrReconfigure('Telegram Bot', env?.TELEGRAM_BOT_TOKEN ? maskSecret(env.TELEGRAM_BOT_TOKEN) : null)) {
114
- // Validate existing token
115
- token = env.TELEGRAM_BOT_TOKEN;
116
- const validateSpinner = clack.spinner();
117
- validateSpinner.start('Validating existing bot token...');
118
- const validation = await validateBotToken(token);
119
- if (validation.valid) {
120
- botUsername = validation.botInfo.username;
121
- validateSpinner.stop(`Bot: @${botUsername}`);
122
- } else {
123
- validateSpinner.stop(`Invalid token in .env: ${validation.error}`);
124
- clack.log.warn('Existing token is invalid — you\'ll need to enter a new one.');
125
- token = null;
126
- }
127
- }
128
-
129
- if (!token) {
130
- clack.log.info(
131
- 'Create a Telegram bot via @BotFather:\n' +
132
- ' 1. Open @BotFather in Telegram\n' +
133
- ' 2. Send /newbot and follow the prompts\n' +
134
- ' 3. Copy the bot token'
135
- );
136
-
137
- await openOrShowURL(getBotFatherURL(), 'Telegram BotFather');
138
-
139
- let tokenValid = false;
140
- while (!tokenValid) {
141
- const inputToken = await clack.password({
142
- message: 'Telegram bot token:',
143
- validate: (input) => {
144
- if (!input) return 'Token is required';
145
- if (!/^\d+:[A-Za-z0-9_-]+$/.test(input)) {
146
- return 'Invalid format. Should be like 123456789:ABC-DEF...';
147
- }
148
- },
149
- });
150
- if (clack.isCancel(inputToken)) {
151
- clack.cancel('Setup cancelled.');
152
- process.exit(0);
153
- }
154
-
155
- const validateSpinner = clack.spinner();
156
- validateSpinner.start('Validating bot token...');
157
- const validation = await validateBotToken(inputToken);
158
-
159
- if (!validation.valid) {
160
- validateSpinner.stop(`Invalid token: ${validation.error}`);
161
- continue;
162
- }
163
-
164
- token = inputToken;
165
- botUsername = validation.botInfo.username;
166
- validateSpinner.stop(`Bot: @${botUsername}`);
167
- tokenValid = true;
168
- }
169
- }
170
-
171
- // Bug fix #146: save token to .env
172
- updateEnvVariable('TELEGRAM_BOT_TOKEN', token);
173
-
174
- // ─── Step 4: Webhook ────────────────────────────────────────────────
175
- clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] Register Webhook`);
176
- clack.log.info('Registering a webhook tells Telegram where to send messages for your bot.');
177
-
178
- // Handle webhook secret
179
- let webhookSecret = env?.TELEGRAM_WEBHOOK_SECRET;
180
- if (webhookSecret) {
181
- clack.log.success('Using existing webhook secret');
182
- } else {
183
- webhookSecret = await generateTelegramWebhookSecret();
184
- updateEnvVariable('TELEGRAM_WEBHOOK_SECRET', webhookSecret);
185
- clack.log.success('Generated webhook secret');
186
- }
187
-
188
- // Register Telegram webhook
189
- webhookUrl = `${appUrl}/api/telegram/webhook`;
190
- const tgSpinner = clack.spinner();
191
- tgSpinner.start('Registering Telegram webhook...');
192
- const tgResult = await setTelegramWebhook(token, webhookUrl, webhookSecret);
193
- if (tgResult.ok) {
194
- tgSpinner.stop('Telegram webhook registered');
195
- } else {
196
- tgSpinner.stop(`Failed: ${tgResult.description}`);
197
- }
198
-
199
- // ─── Step 5: Optional Keys ──────────────────────────────────────────
200
- clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] Optional Keys`);
201
- clack.log.info('An OpenAI API key enables voice message transcription via Whisper.');
202
-
203
- if (await keepOrReconfigure('OpenAI key', env?.OPENAI_API_KEY ? maskSecret(env.OPENAI_API_KEY) : null)) {
204
- // Keep existing
205
- } else {
206
- const openaiKey = await promptForOptionalKey('openai', 'voice messages');
207
- if (openaiKey) {
208
- updateEnvVariable('OPENAI_API_KEY', openaiKey);
209
- const s = clack.spinner();
210
- s.start('Setting OpenAI secret on GitHub...');
211
- await setSecrets(owner, repo, { AGENT_OPENAI_API_KEY: openaiKey });
212
- s.stop('OpenAI secret set');
213
- clack.log.success(`OpenAI key added for voice (${maskSecret(openaiKey)})`);
214
- }
215
- }
216
-
217
- // ─── Summary ────────────────────────────────────────────────────────
218
- let summary = '';
219
- summary += `Bot: @${botUsername || '(unknown)'}\n`;
220
- summary += `Webhook: ${webhookUrl}`;
221
- clack.note(summary, 'Telegram Configuration');
222
-
223
- clack.outro('Telegram setup complete! Link your personal chat at /profile/telegram.');
224
- }
225
-
226
- main().catch((error) => {
227
- clack.log.error(`Failed: ${error.message}`);
228
- process.exit(1);
229
- });