omnikey-cli 1.5.2 → 1.5.4

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.
@@ -0,0 +1,384 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.AgentAbortError = void 0;
7
+ exports.listRecentSessions = listRecentSessions;
8
+ exports.listTaskTemplates = listTaskTemplates;
9
+ exports.setDefaultTaskTemplate = setDefaultTaskTemplate;
10
+ exports.listProjectGroups = listProjectGroups;
11
+ exports.runAgentTurn = runAgentTurn;
12
+ exports.extractFinalAnswerFromHistory = extractFinalAnswerFromHistory;
13
+ const axios_1 = __importDefault(require("axios"));
14
+ const ws_1 = __importDefault(require("ws"));
15
+ const child_process_1 = require("child_process");
16
+ const fs_1 = require("fs");
17
+ const path_1 = __importDefault(require("path"));
18
+ const crypto_1 = require("crypto");
19
+ const config_1 = require("./config");
20
+ const omnikeyAuth_1 = require("./omnikeyAuth");
21
+ async function listRecentSessions(logger, limit = 5) {
22
+ const token = await (0, omnikeyAuth_1.fetchJwtToken)(logger);
23
+ const url = `${(0, config_1.omnikeyBaseUrl)()}/api/agent/sessions`;
24
+ const resp = await axios_1.default.get(url, {
25
+ timeout: 10000,
26
+ headers: { Authorization: `Bearer ${token}` },
27
+ });
28
+ return (resp.data ?? []).slice(0, limit);
29
+ }
30
+ async function listTaskTemplates(logger) {
31
+ const token = await (0, omnikeyAuth_1.fetchJwtToken)(logger);
32
+ const url = `${(0, config_1.omnikeyBaseUrl)()}/api/instructions/templates`;
33
+ const resp = await axios_1.default.get(url, {
34
+ timeout: 10000,
35
+ headers: { Authorization: `Bearer ${token}` },
36
+ });
37
+ return resp.data?.templates ?? [];
38
+ }
39
+ /**
40
+ * Mark a task template as the active default on the backend. Subsequent
41
+ * agent runs automatically pick it up via stored_instructions — no need
42
+ * to prepend it to the user prompt.
43
+ */
44
+ async function setDefaultTaskTemplate(logger, templateId) {
45
+ const token = await (0, omnikeyAuth_1.fetchJwtToken)(logger);
46
+ const url = `${(0, config_1.omnikeyBaseUrl)()}/api/instructions/templates/${encodeURIComponent(templateId)}/set-default`;
47
+ await axios_1.default.post(url, undefined, {
48
+ timeout: 10000,
49
+ headers: { Authorization: `Bearer ${token}` },
50
+ });
51
+ logger.info("Set default task template", { templateId });
52
+ }
53
+ async function listProjectGroups(logger) {
54
+ const token = await (0, omnikeyAuth_1.fetchJwtToken)(logger);
55
+ const url = `${(0, config_1.omnikeyBaseUrl)()}/api/agent/groups`;
56
+ const resp = await axios_1.default.get(url, {
57
+ timeout: 10000,
58
+ headers: { Authorization: `Bearer ${token}` },
59
+ });
60
+ return resp.data?.groups ?? [];
61
+ }
62
+ class AgentAbortError extends Error {
63
+ constructor(message = "Agent run aborted") {
64
+ super(message);
65
+ this.name = "AgentAbortError";
66
+ }
67
+ }
68
+ exports.AgentAbortError = AgentAbortError;
69
+ function extractTagged(content, tag) {
70
+ const re = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "i");
71
+ const m = content.match(re);
72
+ return m?.[1]?.trim() || null;
73
+ }
74
+ function stripTagged(content, tag) {
75
+ return content.replace(new RegExp(`<${tag}[^>]*>[\\s\\S]*?<\\/${tag}>`, "gi"), "");
76
+ }
77
+ function cleanReasoning(content) {
78
+ return content
79
+ .replace(/<\/?shell_function_calls>/gi, "")
80
+ .replace(/<final_answer>([\s\S]*?)<\/final_answer>/gi, "$1")
81
+ .trim();
82
+ }
83
+ const SHELL_TIMEOUT_MS = 5 * 60 * 1000;
84
+ const SHELL_OUTPUT_MAX = 64 * 1024;
85
+ // Mirrors WINDOWS_SHELL_CANDIDATES in src/agent/mcpRuntime.ts
86
+ const WINDOWS_SHELL_CANDIDATES = [
87
+ "C:\\Program Files\\PowerShell\\7\\pwsh.exe",
88
+ "C:\\Program Files\\PowerShell\\6\\pwsh.exe",
89
+ "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
90
+ "C:\\Windows\\System32\\cmd.exe",
91
+ "C:\\Windows\\cmd.exe",
92
+ ];
93
+ // Resolve the Windows shell: COMSPEC → SystemRoot\System32\cmd.exe → candidate list.
94
+ // Mirrors resolveLoginShell() in src/agent/mcpRuntime.ts, with SystemRoot used to
95
+ // locate cmd.exe from the Win32 system root rather than a hardcoded drive letter.
96
+ function resolveWindowsShell() {
97
+ const comspec = process.env.COMSPEC ?? "";
98
+ if (comspec && (0, fs_1.existsSync)(comspec))
99
+ return comspec;
100
+ const systemRoot = process.env.SystemRoot ?? "C:\\Windows";
101
+ const cmdFromRoot = path_1.default.join(systemRoot, "System32", "cmd.exe");
102
+ if ((0, fs_1.existsSync)(cmdFromRoot))
103
+ return cmdFromRoot;
104
+ for (const candidate of WINDOWS_SHELL_CANDIDATES) {
105
+ if ((0, fs_1.existsSync)(candidate))
106
+ return candidate;
107
+ }
108
+ return "cmd.exe";
109
+ }
110
+ // Build shell args for the resolved shell — mirrors wrapWithLoginShell() in
111
+ // src/agent/mcpRuntime.ts. PowerShell/pwsh use -NoProfile -Command; cmd uses /c.
112
+ function buildWindowsShellArgs(shell, script) {
113
+ const name = path_1.default.basename(shell).toLowerCase();
114
+ if (name === "pwsh.exe" || name === "powershell.exe") {
115
+ return ["-NoProfile", "-Command", script];
116
+ }
117
+ return ["/c", script];
118
+ }
119
+ const PLATFORM = process.platform === "win32"
120
+ ? "windows"
121
+ : process.platform === "darwin"
122
+ ? "macos"
123
+ : "linux";
124
+ /**
125
+ * Execute a shell script locally and capture combined stdout+stderr.
126
+ * On macOS/Linux: invoke the login shell with `-l -c <script>` (mirrors the
127
+ * macOS AgentRunner.runShellCommandWithStatus path).
128
+ * On Windows: resolve the shell via COMSPEC / SystemRoot / candidate list and
129
+ * pass the script with the appropriate flags (-NoProfile -Command or /c),
130
+ * mirroring how the Windows app executes scripts in the terminal.
131
+ * Output is capped so a runaway script can't blow up the WebSocket payload.
132
+ */
133
+ function runShellScript(script, logger) {
134
+ return new Promise((resolve) => {
135
+ let shell;
136
+ let shellArgs;
137
+ if (process.platform !== "darwin" && process.platform === "win32") {
138
+ shell = resolveWindowsShell();
139
+ shellArgs = buildWindowsShellArgs(shell, script);
140
+ }
141
+ else {
142
+ shell = process.env.SHELL || "/bin/zsh";
143
+ shellArgs = ["-l", "-c", script];
144
+ }
145
+ logger.info("Executing shell script from agent", {
146
+ shell,
147
+ platform: PLATFORM,
148
+ length: script.length,
149
+ });
150
+ const child = (0, child_process_1.spawn)(shell, shellArgs, {
151
+ cwd: process.env.HOME ?? process.env.USERPROFILE ?? process.cwd(),
152
+ env: process.env,
153
+ });
154
+ let buf = "";
155
+ let truncated = false;
156
+ const append = (chunk) => {
157
+ if (truncated)
158
+ return;
159
+ const room = SHELL_OUTPUT_MAX - buf.length;
160
+ if (room <= 0) {
161
+ truncated = true;
162
+ return;
163
+ }
164
+ const text = chunk.toString("utf8");
165
+ if (text.length <= room) {
166
+ buf += text;
167
+ }
168
+ else {
169
+ buf += text.slice(0, room);
170
+ truncated = true;
171
+ }
172
+ };
173
+ child.stdout.on("data", append);
174
+ child.stderr.on("data", append);
175
+ const timeout = setTimeout(() => {
176
+ logger.warn("Shell script timed out; sending SIGTERM", {
177
+ timeoutMs: SHELL_TIMEOUT_MS,
178
+ });
179
+ try {
180
+ child.kill("SIGTERM");
181
+ }
182
+ catch {
183
+ /* noop */
184
+ }
185
+ }, SHELL_TIMEOUT_MS);
186
+ child.on("error", (err) => {
187
+ clearTimeout(timeout);
188
+ resolve({
189
+ output: `${buf}\n[shell spawn error: ${err.message}]`,
190
+ status: -1,
191
+ });
192
+ });
193
+ child.on("close", (code, signal) => {
194
+ clearTimeout(timeout);
195
+ const status = typeof code === "number" ? code : signal ? 1 : 0;
196
+ const finalOutput = truncated
197
+ ? `${buf}\n... [truncated to ${SHELL_OUTPUT_MAX} bytes]`
198
+ : buf;
199
+ logger.info("Shell script finished", {
200
+ status,
201
+ signal,
202
+ outputLength: finalOutput.length,
203
+ });
204
+ resolve({ output: finalOutput, status });
205
+ });
206
+ });
207
+ }
208
+ /**
209
+ * Drive a single agent turn over the /ws/omni-agent WebSocket. Streams
210
+ * intermediate blocks via `onBlock` and resolves with the final answer.
211
+ *
212
+ * When the agent emits a `<shell_script>` block we execute it locally with
213
+ * the user's login shell (mirroring the macOS app's AgentRunner) and send
214
+ * the combined stdout+stderr back as the next user turn so the agent can
215
+ * continue reasoning over the result.
216
+ */
217
+ async function runAgentTurn(logger, opts) {
218
+ const token = await (0, omnikeyAuth_1.fetchJwtToken)(logger);
219
+ const sessionId = opts.sessionId || (0, crypto_1.randomUUID)();
220
+ const url = (0, config_1.omnikeyWsUrl)("/ws/omni-agent");
221
+ return new Promise((resolve, reject) => {
222
+ if (opts.signal?.aborted) {
223
+ reject(new AgentAbortError());
224
+ return;
225
+ }
226
+ const ws = new ws_1.default(url, {
227
+ headers: { Authorization: `Bearer ${token}` },
228
+ });
229
+ let settled = false;
230
+ const finish = (err, result) => {
231
+ if (settled)
232
+ return;
233
+ settled = true;
234
+ if (opts.signal && onAbort) {
235
+ opts.signal.removeEventListener("abort", onAbort);
236
+ }
237
+ try {
238
+ ws.close();
239
+ }
240
+ catch {
241
+ /* noop */
242
+ }
243
+ if (err)
244
+ reject(err);
245
+ else if (result)
246
+ resolve(result);
247
+ };
248
+ const onAbort = opts.signal
249
+ ? () => {
250
+ logger.info("Agent run aborted by caller", { sessionId });
251
+ finish(new AgentAbortError());
252
+ }
253
+ : null;
254
+ if (opts.signal && onAbort) {
255
+ opts.signal.addEventListener("abort", onAbort, { once: true });
256
+ }
257
+ const send = (msg) => {
258
+ ws.send(JSON.stringify(msg), (err) => {
259
+ if (err)
260
+ finish(err);
261
+ });
262
+ };
263
+ ws.on("open", () => {
264
+ logger.info("Agent WebSocket open", { sessionId });
265
+ send({
266
+ session_id: sessionId,
267
+ sender: "client",
268
+ content: opts.prompt,
269
+ is_terminal_output: false,
270
+ is_error: false,
271
+ platform: PLATFORM,
272
+ group_name: opts.groupName,
273
+ });
274
+ });
275
+ ws.on("message", async (data) => {
276
+ let msg;
277
+ try {
278
+ msg = JSON.parse(data.toString());
279
+ }
280
+ catch (e) {
281
+ logger.warn("Failed to parse agent ws message", {
282
+ error: e.message,
283
+ });
284
+ return;
285
+ }
286
+ const content = msg.content || "";
287
+ if (msg.is_error) {
288
+ finish(new Error(content || "Agent reported an error"));
289
+ return;
290
+ }
291
+ if (msg.is_web_call) {
292
+ await opts.onBlock({ kind: "webCall", text: content });
293
+ return;
294
+ }
295
+ if (msg.is_image_rendering) {
296
+ await opts.onBlock({ kind: "imageRendering", text: content });
297
+ return;
298
+ }
299
+ if (msg.is_mcp_call) {
300
+ await opts.onBlock({ kind: "mcpCall", text: content });
301
+ return;
302
+ }
303
+ const finalAnswer = extractTagged(content, "final_answer");
304
+ if (finalAnswer) {
305
+ await opts.onBlock({ kind: "finalAnswer", text: finalAnswer });
306
+ finish(null, { sessionId, finalAnswer });
307
+ return;
308
+ }
309
+ const shellScript = extractTagged(content, "shell_script");
310
+ if (shellScript) {
311
+ const reasoning = cleanReasoning(stripTagged(content, "shell_script"));
312
+ if (reasoning)
313
+ await opts.onBlock({ kind: "reasoning", text: reasoning });
314
+ await opts.onBlock({ kind: "shellCommand", text: shellScript });
315
+ try {
316
+ const { output, status } = await runShellScript(shellScript, logger);
317
+ const statusLabel = status === 0 ? "success" : `error (exit code: ${status})`;
318
+ await opts.onBlock({
319
+ kind: "terminalOutput",
320
+ text: `[terminal ${statusLabel}]\n${output}`,
321
+ });
322
+ send({
323
+ session_id: sessionId,
324
+ sender: "client",
325
+ content: output,
326
+ is_terminal_output: true,
327
+ is_error: status !== 0,
328
+ platform: PLATFORM,
329
+ });
330
+ }
331
+ catch (err) {
332
+ const message = err.message;
333
+ logger.error("Shell execution failed", { error: message });
334
+ await opts.onBlock({
335
+ kind: "terminalOutput",
336
+ text: `[terminal error]\n${message}`,
337
+ });
338
+ send({
339
+ session_id: sessionId,
340
+ sender: "client",
341
+ content: `Failed to execute shell script: ${message}`,
342
+ is_terminal_output: true,
343
+ is_error: true,
344
+ platform: PLATFORM,
345
+ });
346
+ }
347
+ return;
348
+ }
349
+ const reasoning = cleanReasoning(content);
350
+ if (reasoning) {
351
+ await opts.onBlock({ kind: "reasoning", text: reasoning });
352
+ }
353
+ });
354
+ ws.on("error", (err) => {
355
+ logger.error("Agent WebSocket error", { error: err.message });
356
+ finish(err);
357
+ });
358
+ ws.on("close", () => {
359
+ if (!settled)
360
+ finish(new Error("Agent WebSocket closed before final answer"));
361
+ });
362
+ });
363
+ }
364
+ /**
365
+ * Find the most recent `<final_answer>` text in a stored session's history JSON.
366
+ */
367
+ function extractFinalAnswerFromHistory(historyJson) {
368
+ try {
369
+ const history = JSON.parse(historyJson);
370
+ for (let i = history.length - 1; i >= 0; i--) {
371
+ const entry = history[i];
372
+ if (entry.role !== "assistant")
373
+ continue;
374
+ const content = typeof entry.content === "string" ? entry.content : "";
375
+ const fa = extractTagged(content, "final_answer");
376
+ if (fa)
377
+ return fa;
378
+ }
379
+ }
380
+ catch {
381
+ /* ignore */
382
+ }
383
+ return null;
384
+ }
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadOmnikeyConfig = loadOmnikeyConfig;
7
+ exports.omnikeyBaseUrl = omnikeyBaseUrl;
8
+ exports.omnikeyWsUrl = omnikeyWsUrl;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const os_1 = __importDefault(require("os"));
12
+ const DEFAULT_HOST = "127.0.0.1";
13
+ const DEFAULT_PORT = 7071;
14
+ const DEFAULT_SQLITE = path_1.default.join(os_1.default.homedir(), ".omnikey", "omnikey-selfhosted.sqlite");
15
+ const CONFIG_PATH = path_1.default.join(os_1.default.homedir(), ".omnikey", "config.json");
16
+ let cached = null;
17
+ function resolveSqlitePath(raw) {
18
+ if (typeof raw === "string" && raw.trim()) {
19
+ return path_1.default.isAbsolute(raw)
20
+ ? raw
21
+ : path_1.default.join(os_1.default.homedir(), ".omnikey", raw);
22
+ }
23
+ return DEFAULT_SQLITE;
24
+ }
25
+ function resolvePort(raw) {
26
+ if (typeof raw === "number" && Number.isFinite(raw))
27
+ return raw;
28
+ if (typeof raw === "string" && raw.trim()) {
29
+ const n = Number(raw);
30
+ if (Number.isFinite(n))
31
+ return n;
32
+ }
33
+ return DEFAULT_PORT;
34
+ }
35
+ function loadOmnikeyConfig() {
36
+ if (cached)
37
+ return cached;
38
+ let parsed = {};
39
+ try {
40
+ if (fs_1.default.existsSync(CONFIG_PATH)) {
41
+ const raw = fs_1.default.readFileSync(CONFIG_PATH, "utf-8");
42
+ parsed = JSON.parse(raw);
43
+ }
44
+ }
45
+ catch {
46
+ // Fall through with defaults; downstream code will surface a clearer error
47
+ // when it actually tries to open the SQLite file or reach the API.
48
+ parsed = {};
49
+ }
50
+ cached = {
51
+ sqlitePath: resolveSqlitePath(parsed.SQLITE_PATH),
52
+ omnikeyPort: resolvePort(parsed.OMNIKEY_PORT),
53
+ omnikeyHost: typeof parsed.OMNIKEY_HOST === "string" && parsed.OMNIKEY_HOST.trim()
54
+ ? parsed.OMNIKEY_HOST
55
+ : DEFAULT_HOST,
56
+ };
57
+ return cached;
58
+ }
59
+ function omnikeyBaseUrl() {
60
+ const { omnikeyHost, omnikeyPort } = loadOmnikeyConfig();
61
+ return `http://${omnikeyHost}:${omnikeyPort}`;
62
+ }
63
+ function omnikeyWsUrl(path) {
64
+ const { omnikeyHost, omnikeyPort } = loadOmnikeyConfig();
65
+ const suffix = path.startsWith("/") ? path : `/${path}`;
66
+ return `ws://${omnikeyHost}:${omnikeyPort}${suffix}`;
67
+ }
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.initDb = initDb;
7
+ exports.closeDb = closeDb;
8
+ exports.getRecentSessions = getRecentSessions;
9
+ exports.getSessionById = getSessionById;
10
+ exports.getMostRecentSession = getMostRecentSession;
11
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
12
+ const config_1 = require("./config");
13
+ let dbInstance = null;
14
+ function initDb(logger) {
15
+ if (dbInstance)
16
+ return dbInstance;
17
+ const { sqlitePath } = (0, config_1.loadOmnikeyConfig)();
18
+ dbInstance = new better_sqlite3_1.default(sqlitePath, {
19
+ readonly: true,
20
+ fileMustExist: true,
21
+ });
22
+ // WAL is set by the writer; we only read. Set busy timeout so concurrent
23
+ // writes from omnikey-ai never throw SQLITE_BUSY at us.
24
+ dbInstance.pragma("busy_timeout = 5000");
25
+ logger.info("Opened SQLite database (read-only)", { path: sqlitePath });
26
+ return dbInstance;
27
+ }
28
+ function closeDb(logger) {
29
+ if (!dbInstance)
30
+ return;
31
+ try {
32
+ dbInstance.close();
33
+ logger?.info("Closed SQLite database");
34
+ }
35
+ catch (err) {
36
+ logger?.warn("Error closing SQLite database", {
37
+ error: err.message,
38
+ });
39
+ }
40
+ finally {
41
+ dbInstance = null;
42
+ }
43
+ }
44
+ function getRecentSessions(limit) {
45
+ const db = dbInstance;
46
+ if (!db)
47
+ throw new Error("Database not initialised. Call initDb() first.");
48
+ const rows = db
49
+ .prepare(`SELECT id, title, turns, last_active_at AS lastActiveAt, group_name AS groupName
50
+ FROM agent_sessions
51
+ ORDER BY last_active_at DESC
52
+ LIMIT ?`)
53
+ .all(limit);
54
+ return rows;
55
+ }
56
+ function getSessionById(sessionId) {
57
+ const db = dbInstance;
58
+ if (!db)
59
+ throw new Error("Database not initialised. Call initDb() first.");
60
+ const row = db
61
+ .prepare(`SELECT id, title, turns, last_active_at AS lastActiveAt, group_name AS groupName, history_json AS historyJson
62
+ FROM agent_sessions
63
+ WHERE id = ?`)
64
+ .get(sessionId);
65
+ return row ?? null;
66
+ }
67
+ function getMostRecentSession() {
68
+ const db = dbInstance;
69
+ if (!db)
70
+ throw new Error("Database not initialised. Call initDb() first.");
71
+ const row = db
72
+ .prepare(`SELECT id, title, turns, last_active_at AS lastActiveAt, group_name AS groupName, history_json AS historyJson
73
+ FROM agent_sessions
74
+ ORDER BY last_active_at DESC
75
+ LIMIT 1`)
76
+ .get();
77
+ return row ?? null;
78
+ }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.logger = void 0;
7
+ const dotenv_1 = __importDefault(require("dotenv"));
8
+ dotenv_1.default.config();
9
+ const express_1 = __importDefault(require("express"));
10
+ const crypto_1 = require("crypto");
11
+ const winston_1 = __importDefault(require("winston"));
12
+ const zod_1 = require("zod");
13
+ const notifyTelegram_1 = require("./notifyTelegram");
14
+ const db_1 = require("./db");
15
+ exports.logger = winston_1.default.createLogger({
16
+ level: process.env.LOG_LEVEL || "info",
17
+ defaultMeta: { conId: (0, crypto_1.randomUUID)() },
18
+ format: winston_1.default.format.combine(winston_1.default.format.colorize(), winston_1.default.format.timestamp(), winston_1.default.format.printf(({ timestamp, level, message, ...meta }) => {
19
+ const metaString = Object.keys(meta).length ? JSON.stringify(meta) : "";
20
+ const date = new Date(timestamp).toLocaleString();
21
+ return `[${date}] ${level}: ${message} ${metaString}`;
22
+ })),
23
+ transports: [new winston_1.default.transports.Console()],
24
+ });
25
+ const app = (0, express_1.default)();
26
+ const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 6666;
27
+ try {
28
+ (0, db_1.initDb)(exports.logger);
29
+ }
30
+ catch (e) {
31
+ exports.logger.error("Failed to open omnikey SQLite database:", e);
32
+ }
33
+ const botToken = process.env.TELEGRAM_BOT_TOKEN ?? "";
34
+ if (botToken) {
35
+ try {
36
+ const bot = (0, notifyTelegram_1.initTelegram)(botToken);
37
+ exports.logger.info("Telegram bot initialized", {
38
+ botTokenSet: !!botToken,
39
+ bot: !!bot,
40
+ });
41
+ (0, notifyTelegram_1.setupMessageListener)(exports.logger, bot);
42
+ }
43
+ catch (e) {
44
+ exports.logger.error("Failed to init telegram:", e);
45
+ }
46
+ }
47
+ app.use(express_1.default.json());
48
+ const sendBodySchema = zod_1.z.object({
49
+ message: zod_1.z.string().min(1, "message must not be empty"),
50
+ parseMode: zod_1.z.enum(["Markdown", "MarkdownV2", "HTML"]).optional(),
51
+ });
52
+ app.get("/", (req, res) => {
53
+ res.send("Telegram bot service (TypeScript)");
54
+ });
55
+ app.post("/telegram/send", async (req, res) => {
56
+ exports.logger.defaultMeta = { conId: "sending notification" };
57
+ const parsed = sendBodySchema.safeParse(req.body);
58
+ if (!parsed.success) {
59
+ exports.logger.warn("Invalid /telegram/send body", {
60
+ issues: parsed.error.issues,
61
+ });
62
+ return res.status(400).json({
63
+ message: "Invalid request body",
64
+ issues: parsed.error.issues,
65
+ });
66
+ }
67
+ const { message, parseMode } = parsed.data;
68
+ try {
69
+ await (0, notifyTelegram_1.notify)(exports.logger, message, { parseMode });
70
+ return res.json({
71
+ message: "Message sent",
72
+ parseMode: parseMode ?? "Markdown",
73
+ });
74
+ }
75
+ catch (e) {
76
+ exports.logger.error("Failed to send message:", e);
77
+ const description = e?.response?.body
78
+ ?.description ?? e.message;
79
+ return res.status(502).json({
80
+ message: "Failed to deliver message to Telegram",
81
+ error: description,
82
+ });
83
+ }
84
+ });
85
+ app.listen(port, () => {
86
+ exports.logger.info(`Server listening on http://localhost:${port}`);
87
+ });
88
+ process.on("SIGINT", () => {
89
+ exports.logger.info("Received SIGINT. Exiting...");
90
+ (0, db_1.closeDb)(exports.logger);
91
+ process.exit(0);
92
+ });
93
+ process.on("SIGTERM", () => {
94
+ exports.logger.info("Received SIGTERM. Exiting...");
95
+ (0, db_1.closeDb)(exports.logger);
96
+ process.exit(0);
97
+ });