@inceptionstack/roundhouse 0.3.14 → 0.3.16
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,62 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Credential checks
|
|
2
|
+
* Credential checks — validates token presence and format.
|
|
3
|
+
* API connectivity is tested by the telegram checks (checks/telegram.ts).
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
|
-
import
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
7
|
+
import { parseEnvFile } from "../../env-file";
|
|
8
|
+
import type { DoctorCheck, DoctorContext } from "../types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the Telegram bot token from process env or the .env file.
|
|
12
|
+
* Shared by credential and telegram checks.
|
|
13
|
+
*/
|
|
14
|
+
export async function resolveToken(ctx: DoctorContext): Promise<string | null> {
|
|
15
|
+
let token = ctx.env.TELEGRAM_BOT_TOKEN;
|
|
16
|
+
if (!token) {
|
|
17
|
+
try {
|
|
18
|
+
const entries = parseEnvFile(await readFile(ctx.envFilePath, "utf8"));
|
|
19
|
+
const raw = entries.get("TELEGRAM_BOT_TOKEN");
|
|
20
|
+
if (raw) token = raw.replace(/^["']|["']$/g, "");
|
|
21
|
+
} catch {}
|
|
22
|
+
}
|
|
23
|
+
return token || null;
|
|
24
|
+
}
|
|
6
25
|
|
|
7
26
|
export const credentialChecks: DoctorCheck[] = [
|
|
8
27
|
{
|
|
9
28
|
id: "telegram-token", category: "credentials", name: "Telegram bot token",
|
|
10
29
|
async run(ctx) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
const { readFile } = await import("node:fs/promises");
|
|
16
|
-
const envContent = await readFile(ctx.envFilePath, "utf8");
|
|
17
|
-
const match = envContent.match(/^TELEGRAM_BOT_TOKEN=(.+)$/m);
|
|
18
|
-
if (match) token = match[1].trim().replace(/^["']|["']$/g, "");
|
|
19
|
-
} catch {}
|
|
20
|
-
}
|
|
30
|
+
const base = { id: "telegram-token", category: "credentials" as const, name: "Telegram bot token" };
|
|
31
|
+
const token = await resolveToken(ctx);
|
|
32
|
+
|
|
21
33
|
if (!token) {
|
|
22
34
|
return {
|
|
23
|
-
|
|
24
|
-
status: "fail", summary: "TELEGRAM_BOT_TOKEN not set",
|
|
35
|
+
...base, status: "fail", summary: "TELEGRAM_BOT_TOKEN not set",
|
|
25
36
|
details: ["Set TELEGRAM_BOT_TOKEN in your environment or ~/.roundhouse/.env"],
|
|
26
37
|
};
|
|
27
38
|
}
|
|
28
39
|
if (!/^\d+:[A-Za-z0-9_-]+$/.test(token)) {
|
|
29
40
|
return {
|
|
30
|
-
|
|
31
|
-
status: "fail", summary: "invalid format",
|
|
41
|
+
...base, status: "fail", summary: "invalid format",
|
|
32
42
|
details: ["Token should match pattern: digits:alphanumeric"],
|
|
33
43
|
};
|
|
34
44
|
}
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
const res = await fetch(`https://api.telegram.org/bot${token}/getMe`, {
|
|
38
|
-
signal: AbortSignal.timeout(10000),
|
|
39
|
-
});
|
|
40
|
-
if (res.ok) {
|
|
41
|
-
const data = await res.json() as any;
|
|
42
|
-
const username = data.result?.username ?? "unknown";
|
|
43
|
-
return {
|
|
44
|
-
id: "telegram-token", category: "credentials", name: "Telegram bot token",
|
|
45
|
-
status: "pass", summary: `@${username}`,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
return {
|
|
49
|
-
id: "telegram-token", category: "credentials", name: "Telegram bot token",
|
|
50
|
-
status: "fail", summary: `API returned ${res.status}`,
|
|
51
|
-
details: ["Token may be invalid or revoked"],
|
|
52
|
-
};
|
|
53
|
-
} catch (err) {
|
|
54
|
-
return {
|
|
55
|
-
id: "telegram-token", category: "credentials", name: "Telegram bot token",
|
|
56
|
-
status: "warn", summary: "cannot reach Telegram API",
|
|
57
|
-
details: [(err as Error).message],
|
|
58
|
-
};
|
|
59
|
-
}
|
|
45
|
+
return { ...base, status: "pass", summary: "present, valid format" };
|
|
60
46
|
},
|
|
61
47
|
},
|
|
62
48
|
];
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram connectivity checks — tests API access, config, and webhook status.
|
|
3
|
+
* Token resolution is shared with credentials.ts via resolveToken().
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
7
|
+
import type { DoctorCheck, DoctorContext } from "../types";
|
|
8
|
+
import { resolveToken } from "./credentials";
|
|
9
|
+
|
|
10
|
+
/** Load and parse the gateway config, returning null on any error. */
|
|
11
|
+
async function loadGatewayConfig(ctx: DoctorContext): Promise<any | null> {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(await readFile(ctx.configPath, "utf8"));
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const telegramChecks: DoctorCheck[] = [
|
|
20
|
+
{
|
|
21
|
+
id: "telegram-configured", category: "network", name: "Telegram adapter configured",
|
|
22
|
+
async run(ctx) {
|
|
23
|
+
const base = { id: "telegram-configured", category: "network" as const, name: "Telegram adapter configured" };
|
|
24
|
+
const cfg = await loadGatewayConfig(ctx);
|
|
25
|
+
if (!cfg) {
|
|
26
|
+
return { ...base, status: "info", summary: "skipped (no config file)" };
|
|
27
|
+
}
|
|
28
|
+
if (cfg.chat?.adapters?.telegram) {
|
|
29
|
+
const mode = cfg.chat.adapters.telegram.mode ?? "polling";
|
|
30
|
+
return { ...base, status: "pass", summary: `mode: ${mode}` };
|
|
31
|
+
}
|
|
32
|
+
return { ...base, status: "info", summary: "not configured (no telegram adapter in config)" };
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
id: "telegram-api", category: "network", name: "Telegram API reachable",
|
|
38
|
+
async run(ctx) {
|
|
39
|
+
const base = { id: "telegram-api", category: "network" as const, name: "Telegram API reachable" };
|
|
40
|
+
const token = await resolveToken(ctx);
|
|
41
|
+
if (!token) {
|
|
42
|
+
return { ...base, status: "info", summary: "skipped (no token)" };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch(`https://api.telegram.org/bot${token}/getMe`, {
|
|
47
|
+
signal: AbortSignal.timeout(10000),
|
|
48
|
+
});
|
|
49
|
+
if (res.ok) {
|
|
50
|
+
const data = await res.json() as any;
|
|
51
|
+
return { ...base, status: "pass", summary: `@${data.result?.username ?? "unknown"}` };
|
|
52
|
+
}
|
|
53
|
+
if (res.status === 401) {
|
|
54
|
+
return { ...base, status: "fail", summary: "401 Unauthorized — token is invalid or revoked" };
|
|
55
|
+
}
|
|
56
|
+
return { ...base, status: "fail", summary: `API returned ${res.status}` };
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return {
|
|
59
|
+
...base, status: "fail", summary: "cannot reach api.telegram.org",
|
|
60
|
+
details: [(err as Error).message],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
id: "telegram-webhook", category: "network", name: "Telegram webhook status",
|
|
68
|
+
async run(ctx) {
|
|
69
|
+
const base = { id: "telegram-webhook", category: "network" as const, name: "Telegram webhook status" };
|
|
70
|
+
const token = await resolveToken(ctx);
|
|
71
|
+
if (!token) {
|
|
72
|
+
return { ...base, status: "info", summary: "skipped (no token)" };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const cfg = await loadGatewayConfig(ctx);
|
|
76
|
+
const configuredMode = cfg?.chat?.adapters?.telegram?.mode ?? "polling";
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const res = await fetch(`https://api.telegram.org/bot${token}/getWebhookInfo`, {
|
|
80
|
+
signal: AbortSignal.timeout(10000),
|
|
81
|
+
});
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
return { ...base, status: "warn", summary: `getWebhookInfo returned ${res.status}` };
|
|
84
|
+
}
|
|
85
|
+
const data = await res.json() as any;
|
|
86
|
+
const webhookUrl = data.result?.url;
|
|
87
|
+
const pendingUpdates = data.result?.pending_update_count ?? 0;
|
|
88
|
+
|
|
89
|
+
if (configuredMode === "polling") {
|
|
90
|
+
if (webhookUrl) {
|
|
91
|
+
return {
|
|
92
|
+
...base, status: "warn", summary: "webhook set but mode is polling",
|
|
93
|
+
details: [
|
|
94
|
+
`Webhook URL: ${webhookUrl}`,
|
|
95
|
+
"Polling won't receive updates while a webhook is active.",
|
|
96
|
+
"The gateway will clear this on startup, but if it fails to start, messages are lost.",
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return { ...base, status: "pass", summary: `no webhook (polling mode), ${pendingUpdates} pending updates` };
|
|
101
|
+
} else {
|
|
102
|
+
if (!webhookUrl) {
|
|
103
|
+
return { ...base, status: "warn", summary: "webhook mode configured but no webhook set" };
|
|
104
|
+
}
|
|
105
|
+
return { ...base, status: "pass", summary: `webhook: ${webhookUrl}, ${pendingUpdates} pending` };
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
return {
|
|
109
|
+
...base, status: "warn", summary: "cannot check webhook status",
|
|
110
|
+
details: [(err as Error).message],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
];
|
package/src/cli/doctor/runner.ts
CHANGED
|
@@ -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/doctor.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { formatResult, formatSummary, formatCategoryHeader } from "./doctor/outp
|
|
|
7
7
|
import { runDoctor, createDoctorContext } from "./doctor/runner";
|
|
8
8
|
|
|
9
9
|
const CATEGORY_ORDER: DoctorCategory[] = [
|
|
10
|
-
"system", "config", "credentials", "agent", "stt", "disk", "systemd",
|
|
10
|
+
"system", "config", "credentials", "network", "agent", "stt", "disk", "systemd",
|
|
11
11
|
];
|
|
12
12
|
|
|
13
13
|
export async function cmdDoctor(args: string[]): Promise<void> {
|
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:
|
|
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 "--
|
|
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 (--
|
|
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
|
-
--
|
|
966
|
+
--with-psst Use psst vault for secrets (default: .env file)
|
|
967
967
|
|
|
968
968
|
Behavior:
|
|
969
969
|
--non-interactive No pairing, no prompts
|