@inceptionstack/roundhouse 0.3.14 → 0.3.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inceptionstack/roundhouse",
3
- "version": "0.3.14",
3
+ "version": "0.3.15",
4
4
  "type": "module",
5
5
  "description": "Multi-platform chat gateway that routes messages through a configured AI agent",
6
6
  "license": "MIT",
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Telegram connectivity checks
3
+ */
4
+
5
+ import { readFile } from "node:fs/promises";
6
+ import type { DoctorCheck } from "../types";
7
+ import { parseEnvFile } from "../../env-file";
8
+
9
+ /** Resolve the bot token from env or .env file */
10
+ async function resolveToken(ctx: { env: NodeJS.ProcessEnv; envFilePath: string }): Promise<string | null> {
11
+ let token = ctx.env.TELEGRAM_BOT_TOKEN;
12
+ if (!token) {
13
+ try {
14
+ const entries = parseEnvFile(await readFile(ctx.envFilePath, "utf8"));
15
+ const raw = entries.get("TELEGRAM_BOT_TOKEN");
16
+ if (raw) token = raw.replace(/^["']|["']$/g, "");
17
+ } catch {}
18
+ }
19
+ return token || null;
20
+ }
21
+
22
+ export const telegramChecks: DoctorCheck[] = [
23
+ {
24
+ id: "telegram-configured", category: "network", name: "Telegram adapter configured",
25
+ async run(ctx) {
26
+ const base = { id: "telegram-configured", category: "network" as const, name: "Telegram adapter configured" };
27
+ try {
28
+ const raw = await readFile(ctx.configPath, "utf8");
29
+ const cfg = JSON.parse(raw);
30
+ if (cfg.chat?.adapters?.telegram) {
31
+ const mode = cfg.chat.adapters.telegram.mode ?? "polling";
32
+ return { ...base, status: "pass", summary: `mode: ${mode}` };
33
+ }
34
+ return { ...base, status: "info", summary: "not configured (no telegram adapter in config)" };
35
+ } catch {
36
+ return { ...base, status: "info", summary: "skipped (no config file)" };
37
+ }
38
+ },
39
+ },
40
+
41
+ {
42
+ id: "telegram-api", category: "network", name: "Telegram API reachable",
43
+ async run(ctx) {
44
+ const base = { id: "telegram-api", category: "network" as const, name: "Telegram API reachable" };
45
+ const token = await resolveToken(ctx);
46
+ if (!token) {
47
+ return { ...base, status: "info", summary: "skipped (no token)" };
48
+ }
49
+
50
+ try {
51
+ const res = await fetch(`https://api.telegram.org/bot${token}/getMe`, {
52
+ signal: AbortSignal.timeout(10000),
53
+ });
54
+ if (res.ok) {
55
+ const data = await res.json() as any;
56
+ return { ...base, status: "pass", summary: `@${data.result?.username ?? "unknown"}` };
57
+ }
58
+ if (res.status === 401) {
59
+ return { ...base, status: "fail", summary: "401 Unauthorized — token is invalid or revoked" };
60
+ }
61
+ return { ...base, status: "fail", summary: `API returned ${res.status}` };
62
+ } catch (err) {
63
+ return {
64
+ ...base, status: "fail", summary: "cannot reach api.telegram.org",
65
+ details: [(err as Error).message],
66
+ };
67
+ }
68
+ },
69
+ },
70
+
71
+ {
72
+ id: "telegram-webhook", category: "network", name: "Telegram webhook status",
73
+ async run(ctx) {
74
+ const base = { id: "telegram-webhook", category: "network" as const, name: "Telegram webhook status" };
75
+ const token = await resolveToken(ctx);
76
+ if (!token) {
77
+ return { ...base, status: "info", summary: "skipped (no token)" };
78
+ }
79
+
80
+ // Check what mode is configured
81
+ let configuredMode = "polling";
82
+ try {
83
+ const raw = await readFile(ctx.configPath, "utf8");
84
+ const cfg = JSON.parse(raw);
85
+ configuredMode = cfg.chat?.adapters?.telegram?.mode ?? "polling";
86
+ } catch {}
87
+
88
+ try {
89
+ const res = await fetch(`https://api.telegram.org/bot${token}/getWebhookInfo`, {
90
+ signal: AbortSignal.timeout(10000),
91
+ });
92
+ if (!res.ok) {
93
+ return { ...base, status: "warn", summary: `getWebhookInfo returned ${res.status}` };
94
+ }
95
+ const data = await res.json() as any;
96
+ const webhookUrl = data.result?.url;
97
+ const pendingUpdates = data.result?.pending_update_count ?? 0;
98
+
99
+ if (configuredMode === "polling") {
100
+ if (webhookUrl) {
101
+ return {
102
+ ...base, status: "warn", summary: `webhook set but mode is polling`,
103
+ details: [
104
+ `Webhook URL: ${webhookUrl}`,
105
+ "Polling won't receive updates while a webhook is active.",
106
+ "The gateway will clear this on startup, but if it fails to start, messages are lost.",
107
+ ],
108
+ };
109
+ }
110
+ return { ...base, status: "pass", summary: `no webhook (polling mode), ${pendingUpdates} pending updates` };
111
+ } else {
112
+ // webhook mode
113
+ if (!webhookUrl) {
114
+ return { ...base, status: "warn", summary: "webhook mode configured but no webhook set" };
115
+ }
116
+ return { ...base, status: "pass", summary: `webhook: ${webhookUrl}, ${pendingUpdates} pending` };
117
+ }
118
+ } catch (err) {
119
+ return {
120
+ ...base, status: "warn", summary: "cannot check webhook status",
121
+ details: [(err as Error).message],
122
+ };
123
+ }
124
+ },
125
+ },
126
+ ];
@@ -13,6 +13,7 @@ import { agentChecks } from "./checks/agent";
13
13
  import { systemdChecks } from "./checks/systemd";
14
14
  import { diskChecks } from "./checks/disk";
15
15
  import { sttChecks } from "./checks/stt";
16
+ import { telegramChecks } from "./checks/telegram";
16
17
 
17
18
  /** Create a DoctorContext with sensible defaults */
18
19
  export async function createDoctorContext(overrides: Partial<DoctorContext> = {}): Promise<DoctorContext> {
@@ -34,6 +35,7 @@ const ALL_CHECKS: DoctorCheck[] = [
34
35
  ...configChecks,
35
36
  ...credentialChecks,
36
37
  ...agentChecks,
38
+ ...telegramChecks,
37
39
  ...sttChecks,
38
40
  ...diskChecks,
39
41
  ...systemdChecks,
package/src/cli/setup.ts CHANGED
@@ -136,7 +136,7 @@ export function parseSetupArgs(argv: string[]): SetupOptions {
136
136
  notifyChatIds: [],
137
137
  systemd: platform() === "linux",
138
138
  voice: true,
139
- psst: true,
139
+ psst: false,
140
140
  nonInteractive: false,
141
141
  force: false,
142
142
  dryRun: false,
@@ -159,7 +159,7 @@ export function parseSetupArgs(argv: string[]): SetupOptions {
159
159
  case "--notify-chat": opts.notifyChatIds.push(parseInt(next(), 10)); break;
160
160
  case "--no-systemd": opts.systemd = false; break;
161
161
  case "--no-voice": opts.voice = false; break;
162
- case "--no-psst": opts.psst = false; break;
162
+ case "--with-psst": opts.psst = true; break;
163
163
  case "--non-interactive": opts.nonInteractive = true; break;
164
164
  case "--force": opts.force = true; break;
165
165
  case "--dry-run": opts.dryRun = true; break;
@@ -446,7 +446,7 @@ async function stepInstallPackages(opts: SetupOptions): Promise<void> {
446
446
  async function stepStoreSecrets(opts: SetupOptions, botInfo: BotInfo): Promise<void> {
447
447
  if (!opts.psst) {
448
448
  step("⑥", "Storing secrets...");
449
- ok("Skipped (--no-psst)");
449
+ ok("Skipped (default — use --with-psst to enable)");
450
450
  return;
451
451
  }
452
452
 
@@ -963,7 +963,7 @@ Channel:
963
963
  Service:
964
964
  --no-systemd Skip systemd install
965
965
  --no-voice Disable voice/STT
966
- --no-psst Skip psst, use plaintext env file
966
+ --with-psst Use psst vault for secrets (default: .env file)
967
967
 
968
968
  Behavior:
969
969
  --non-interactive No pairing, no prompts