@oh-my-pi/pi-mom 1.337.0

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/src/log.ts ADDED
@@ -0,0 +1,271 @@
1
+ import chalk from "chalk";
2
+
3
+ export interface LogContext {
4
+ channelId: string;
5
+ userName?: string;
6
+ channelName?: string; // For display like #dev-team vs C16HET4EQ
7
+ }
8
+
9
+ function timestamp(): string {
10
+ const now = new Date();
11
+ const hh = String(now.getHours()).padStart(2, "0");
12
+ const mm = String(now.getMinutes()).padStart(2, "0");
13
+ const ss = String(now.getSeconds()).padStart(2, "0");
14
+ return `[${hh}:${mm}:${ss}]`;
15
+ }
16
+
17
+ function formatContext(ctx: LogContext): string {
18
+ // DMs: [DM:username]
19
+ // Channels: [#channel-name:username] or [C16HET4EQ:username] if no name
20
+ if (ctx.channelId.startsWith("D")) {
21
+ return `[DM:${ctx.userName || ctx.channelId}]`;
22
+ }
23
+ const channel = ctx.channelName || ctx.channelId;
24
+ const user = ctx.userName || "unknown";
25
+ return `[${channel.startsWith("#") ? channel : `#${channel}`}:${user}]`;
26
+ }
27
+
28
+ function truncate(text: string, maxLen: number): string {
29
+ if (text.length <= maxLen) return text;
30
+ return `${text.substring(0, maxLen)}\n(truncated at ${maxLen} chars)`;
31
+ }
32
+
33
+ function formatToolArgs(args: Record<string, unknown>): string {
34
+ const lines: string[] = [];
35
+
36
+ for (const [key, value] of Object.entries(args)) {
37
+ // Skip the label - it's already shown in the tool name
38
+ if (key === "label") continue;
39
+
40
+ // For read tool, format path with offset/limit
41
+ if (key === "path" && typeof value === "string") {
42
+ const offset = args.offset as number | undefined;
43
+ const limit = args.limit as number | undefined;
44
+ if (offset !== undefined && limit !== undefined) {
45
+ lines.push(`${value}:${offset}-${offset + limit}`);
46
+ } else {
47
+ lines.push(value);
48
+ }
49
+ continue;
50
+ }
51
+
52
+ // Skip offset/limit since we already handled them
53
+ if (key === "offset" || key === "limit") continue;
54
+
55
+ // For other values, format them
56
+ if (typeof value === "string") {
57
+ // Multi-line strings get indented
58
+ if (value.includes("\n")) {
59
+ lines.push(value);
60
+ } else {
61
+ lines.push(value);
62
+ }
63
+ } else {
64
+ lines.push(JSON.stringify(value));
65
+ }
66
+ }
67
+
68
+ return lines.join("\n");
69
+ }
70
+
71
+ // User messages
72
+ export function logUserMessage(ctx: LogContext, text: string): void {
73
+ console.log(chalk.green(`${timestamp()} ${formatContext(ctx)} ${text}`));
74
+ }
75
+
76
+ // Tool execution
77
+ export function logToolStart(ctx: LogContext, toolName: string, label: string, args: Record<string, unknown>): void {
78
+ const formattedArgs = formatToolArgs(args);
79
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ↳ ${toolName}: ${label}`));
80
+ if (formattedArgs) {
81
+ // Indent the args
82
+ const indented = formattedArgs
83
+ .split("\n")
84
+ .map((line) => ` ${line}`)
85
+ .join("\n");
86
+ console.log(chalk.dim(indented));
87
+ }
88
+ }
89
+
90
+ export function logToolSuccess(ctx: LogContext, toolName: string, durationMs: number, result: string): void {
91
+ const duration = (durationMs / 1000).toFixed(1);
92
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✓ ${toolName} (${duration}s)`));
93
+
94
+ const truncated = truncate(result, 1000);
95
+ if (truncated) {
96
+ const indented = truncated
97
+ .split("\n")
98
+ .map((line) => ` ${line}`)
99
+ .join("\n");
100
+ console.log(chalk.dim(indented));
101
+ }
102
+ }
103
+
104
+ export function logToolError(ctx: LogContext, toolName: string, durationMs: number, error: string): void {
105
+ const duration = (durationMs / 1000).toFixed(1);
106
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✗ ${toolName} (${duration}s)`));
107
+
108
+ const truncated = truncate(error, 1000);
109
+ const indented = truncated
110
+ .split("\n")
111
+ .map((line) => ` ${line}`)
112
+ .join("\n");
113
+ console.log(chalk.dim(indented));
114
+ }
115
+
116
+ // Response streaming
117
+ export function logResponseStart(ctx: LogContext): void {
118
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} → Streaming response...`));
119
+ }
120
+
121
+ export function logThinking(ctx: LogContext, thinking: string): void {
122
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💭 Thinking`));
123
+ const truncated = truncate(thinking, 1000);
124
+ const indented = truncated
125
+ .split("\n")
126
+ .map((line) => ` ${line}`)
127
+ .join("\n");
128
+ console.log(chalk.dim(indented));
129
+ }
130
+
131
+ export function logResponse(ctx: LogContext, text: string): void {
132
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💬 Response`));
133
+ const truncated = truncate(text, 1000);
134
+ const indented = truncated
135
+ .split("\n")
136
+ .map((line) => ` ${line}`)
137
+ .join("\n");
138
+ console.log(chalk.dim(indented));
139
+ }
140
+
141
+ // Attachments
142
+ export function logDownloadStart(ctx: LogContext, filename: string, localPath: string): void {
143
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ↓ Downloading attachment`));
144
+ console.log(chalk.dim(` ${filename} → ${localPath}`));
145
+ }
146
+
147
+ export function logDownloadSuccess(ctx: LogContext, sizeKB: number): void {
148
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✓ Downloaded (${sizeKB.toLocaleString()} KB)`));
149
+ }
150
+
151
+ export function logDownloadError(ctx: LogContext, filename: string, error: string): void {
152
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✗ Download failed`));
153
+ console.log(chalk.dim(` ${filename}: ${error}`));
154
+ }
155
+
156
+ // Control
157
+ export function logStopRequest(ctx: LogContext): void {
158
+ console.log(chalk.green(`${timestamp()} ${formatContext(ctx)} stop`));
159
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ⊗ Stop requested - aborting`));
160
+ }
161
+
162
+ // System
163
+ export function logInfo(message: string): void {
164
+ console.log(chalk.blue(`${timestamp()} [system] ${message}`));
165
+ }
166
+
167
+ export function logWarning(message: string, details?: string): void {
168
+ console.log(chalk.yellow(`${timestamp()} [system] ⚠ ${message}`));
169
+ if (details) {
170
+ const indented = details
171
+ .split("\n")
172
+ .map((line) => ` ${line}`)
173
+ .join("\n");
174
+ console.log(chalk.dim(indented));
175
+ }
176
+ }
177
+
178
+ export function logAgentError(ctx: LogContext | "system", error: string): void {
179
+ const context = ctx === "system" ? "[system]" : formatContext(ctx);
180
+ console.log(chalk.yellow(`${timestamp()} ${context} ✗ Agent error`));
181
+ const indented = error
182
+ .split("\n")
183
+ .map((line) => ` ${line}`)
184
+ .join("\n");
185
+ console.log(chalk.dim(indented));
186
+ }
187
+
188
+ // Usage summary
189
+ export function logUsageSummary(
190
+ ctx: LogContext,
191
+ usage: {
192
+ input: number;
193
+ output: number;
194
+ cacheRead: number;
195
+ cacheWrite: number;
196
+ cost: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number };
197
+ },
198
+ contextTokens?: number,
199
+ contextWindow?: number,
200
+ ): string {
201
+ const formatTokens = (count: number): string => {
202
+ if (count < 1000) return count.toString();
203
+ if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
204
+ if (count < 1000000) return `${Math.round(count / 1000)}k`;
205
+ return `${(count / 1000000).toFixed(1)}M`;
206
+ };
207
+
208
+ const lines: string[] = [];
209
+ lines.push("*Usage Summary*");
210
+ lines.push(`Tokens: ${usage.input.toLocaleString()} in, ${usage.output.toLocaleString()} out`);
211
+ if (usage.cacheRead > 0 || usage.cacheWrite > 0) {
212
+ lines.push(`Cache: ${usage.cacheRead.toLocaleString()} read, ${usage.cacheWrite.toLocaleString()} write`);
213
+ }
214
+ if (contextTokens && contextWindow) {
215
+ const contextPercent = ((contextTokens / contextWindow) * 100).toFixed(1);
216
+ lines.push(`Context: ${formatTokens(contextTokens)} / ${formatTokens(contextWindow)} (${contextPercent}%)`);
217
+ }
218
+ lines.push(
219
+ `Cost: $${usage.cost.input.toFixed(4)} in, $${usage.cost.output.toFixed(4)} out` +
220
+ (usage.cacheRead > 0 || usage.cacheWrite > 0
221
+ ? `, $${usage.cost.cacheRead.toFixed(4)} cache read, $${usage.cost.cacheWrite.toFixed(4)} cache write`
222
+ : ""),
223
+ );
224
+ lines.push(`*Total: $${usage.cost.total.toFixed(4)}*`);
225
+
226
+ const summary = lines.join("\n");
227
+
228
+ // Log to console
229
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💰 Usage`));
230
+ console.log(
231
+ chalk.dim(
232
+ ` ${usage.input.toLocaleString()} in + ${usage.output.toLocaleString()} out` +
233
+ (usage.cacheRead > 0 || usage.cacheWrite > 0
234
+ ? ` (${usage.cacheRead.toLocaleString()} cache read, ${usage.cacheWrite.toLocaleString()} cache write)`
235
+ : "") +
236
+ ` = $${usage.cost.total.toFixed(4)}`,
237
+ ),
238
+ );
239
+
240
+ return summary;
241
+ }
242
+
243
+ // Startup (no context needed)
244
+ export function logStartup(workingDir: string, sandbox: string): void {
245
+ console.log("Starting mom bot...");
246
+ console.log(` Working directory: ${workingDir}`);
247
+ console.log(` Sandbox: ${sandbox}`);
248
+ }
249
+
250
+ export function logConnected(): void {
251
+ console.log("⚡️ Mom bot connected and listening!");
252
+ console.log("");
253
+ }
254
+
255
+ export function logDisconnected(): void {
256
+ console.log("Mom bot disconnected.");
257
+ }
258
+
259
+ // Backfill
260
+ export function logBackfillStart(channelCount: number): void {
261
+ console.log(chalk.blue(`${timestamp()} [system] Backfilling ${channelCount} channels...`));
262
+ }
263
+
264
+ export function logBackfillChannel(channelName: string, messageCount: number): void {
265
+ console.log(chalk.blue(`${timestamp()} [system] #${channelName}: ${messageCount} messages`));
266
+ }
267
+
268
+ export function logBackfillComplete(totalMessages: number, durationMs: number): void {
269
+ const duration = (durationMs / 1000).toFixed(1);
270
+ console.log(chalk.blue(`${timestamp()} [system] Backfill complete: ${totalMessages} messages in ${duration}s`));
271
+ }
package/src/main.ts ADDED
@@ -0,0 +1,332 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { join, resolve } from "path";
4
+ import { type AgentRunner, getOrCreateRunner } from "./agent.js";
5
+ import { syncLogToContext } from "./context.js";
6
+ import { downloadChannel } from "./download.js";
7
+ import { createEventsWatcher } from "./events.js";
8
+ import * as log from "./log.js";
9
+ import { parseSandboxArg, type SandboxConfig, validateSandbox } from "./sandbox.js";
10
+ import { type MomHandler, type SlackBot, SlackBot as SlackBotClass, type SlackEvent } from "./slack.js";
11
+ import { ChannelStore } from "./store.js";
12
+
13
+ // ============================================================================
14
+ // Config
15
+ // ============================================================================
16
+
17
+ const MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;
18
+ const MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;
19
+
20
+ interface ParsedArgs {
21
+ workingDir?: string;
22
+ sandbox: SandboxConfig;
23
+ downloadChannel?: string;
24
+ }
25
+
26
+ function parseArgs(): ParsedArgs {
27
+ const args = process.argv.slice(2);
28
+ let sandbox: SandboxConfig = { type: "host" };
29
+ let workingDir: string | undefined;
30
+ let downloadChannelId: string | undefined;
31
+
32
+ for (let i = 0; i < args.length; i++) {
33
+ const arg = args[i];
34
+ if (arg.startsWith("--sandbox=")) {
35
+ sandbox = parseSandboxArg(arg.slice("--sandbox=".length));
36
+ } else if (arg === "--sandbox") {
37
+ sandbox = parseSandboxArg(args[++i] || "");
38
+ } else if (arg.startsWith("--download=")) {
39
+ downloadChannelId = arg.slice("--download=".length);
40
+ } else if (arg === "--download") {
41
+ downloadChannelId = args[++i];
42
+ } else if (!arg.startsWith("-")) {
43
+ workingDir = arg;
44
+ }
45
+ }
46
+
47
+ return {
48
+ workingDir: workingDir ? resolve(workingDir) : undefined,
49
+ sandbox,
50
+ downloadChannel: downloadChannelId,
51
+ };
52
+ }
53
+
54
+ const parsedArgs = parseArgs();
55
+
56
+ // Handle --download mode
57
+ if (parsedArgs.downloadChannel) {
58
+ if (!MOM_SLACK_BOT_TOKEN) {
59
+ console.error("Missing env: MOM_SLACK_BOT_TOKEN");
60
+ process.exit(1);
61
+ }
62
+ await downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);
63
+ process.exit(0);
64
+ }
65
+
66
+ // Normal bot mode - require working dir
67
+ if (!parsedArgs.workingDir) {
68
+ console.error("Usage: mom [--sandbox=host|docker:<name>] <working-directory>");
69
+ console.error(" mom --download <channel-id>");
70
+ process.exit(1);
71
+ }
72
+
73
+ const { workingDir, sandbox } = { workingDir: parsedArgs.workingDir, sandbox: parsedArgs.sandbox };
74
+
75
+ if (!MOM_SLACK_APP_TOKEN || !MOM_SLACK_BOT_TOKEN) {
76
+ console.error("Missing env: MOM_SLACK_APP_TOKEN, MOM_SLACK_BOT_TOKEN");
77
+ process.exit(1);
78
+ }
79
+
80
+ await validateSandbox(sandbox);
81
+
82
+ // ============================================================================
83
+ // State (per channel)
84
+ // ============================================================================
85
+
86
+ interface ChannelState {
87
+ running: boolean;
88
+ runner: AgentRunner;
89
+ store: ChannelStore;
90
+ stopRequested: boolean;
91
+ stopMessageTs?: string;
92
+ }
93
+
94
+ const channelStates = new Map<string, ChannelState>();
95
+
96
+ function getState(channelId: string): ChannelState {
97
+ let state = channelStates.get(channelId);
98
+ if (!state) {
99
+ const channelDir = join(workingDir, channelId);
100
+ state = {
101
+ running: false,
102
+ runner: getOrCreateRunner(sandbox, channelId, channelDir),
103
+ store: new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! }),
104
+ stopRequested: false,
105
+ };
106
+ channelStates.set(channelId, state);
107
+ }
108
+ return state;
109
+ }
110
+
111
+ // ============================================================================
112
+ // Create SlackContext adapter
113
+ // ============================================================================
114
+
115
+ function createSlackContext(event: SlackEvent, slack: SlackBot, state: ChannelState, isEvent?: boolean) {
116
+ let messageTs: string | null = null;
117
+ const threadMessageTs: string[] = [];
118
+ let accumulatedText = "";
119
+ let isWorking = true;
120
+ const workingIndicator = " ...";
121
+ let updatePromise = Promise.resolve();
122
+
123
+ const user = slack.getUser(event.user);
124
+
125
+ // Extract event filename for status message
126
+ const eventFilename = isEvent ? event.text.match(/^\[EVENT:([^:]+):/)?.[1] : undefined;
127
+
128
+ return {
129
+ message: {
130
+ text: event.text,
131
+ rawText: event.text,
132
+ user: event.user,
133
+ userName: user?.userName,
134
+ channel: event.channel,
135
+ ts: event.ts,
136
+ attachments: (event.attachments || []).map((a) => ({ local: a.local })),
137
+ },
138
+ channelName: slack.getChannel(event.channel)?.name,
139
+ store: state.store,
140
+ channels: slack.getAllChannels().map((c) => ({ id: c.id, name: c.name })),
141
+ users: slack.getAllUsers().map((u) => ({ id: u.id, userName: u.userName, displayName: u.displayName })),
142
+
143
+ respond: async (text: string, shouldLog = true) => {
144
+ updatePromise = updatePromise.then(async () => {
145
+ accumulatedText = accumulatedText ? `${accumulatedText}\n${text}` : text;
146
+ const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;
147
+
148
+ if (messageTs) {
149
+ await slack.updateMessage(event.channel, messageTs, displayText);
150
+ } else {
151
+ messageTs = await slack.postMessage(event.channel, displayText);
152
+ }
153
+
154
+ if (shouldLog && messageTs) {
155
+ slack.logBotResponse(event.channel, text, messageTs);
156
+ }
157
+ });
158
+ await updatePromise;
159
+ },
160
+
161
+ replaceMessage: async (text: string) => {
162
+ updatePromise = updatePromise.then(async () => {
163
+ accumulatedText = text;
164
+ const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;
165
+ if (messageTs) {
166
+ await slack.updateMessage(event.channel, messageTs, displayText);
167
+ } else {
168
+ messageTs = await slack.postMessage(event.channel, displayText);
169
+ }
170
+ });
171
+ await updatePromise;
172
+ },
173
+
174
+ respondInThread: async (text: string) => {
175
+ updatePromise = updatePromise.then(async () => {
176
+ if (messageTs) {
177
+ const ts = await slack.postInThread(event.channel, messageTs, text);
178
+ threadMessageTs.push(ts);
179
+ }
180
+ });
181
+ await updatePromise;
182
+ },
183
+
184
+ setTyping: async (isTyping: boolean) => {
185
+ if (isTyping && !messageTs) {
186
+ updatePromise = updatePromise.then(async () => {
187
+ if (!messageTs) {
188
+ accumulatedText = eventFilename ? `_Starting event: ${eventFilename}_` : "_Thinking_";
189
+ messageTs = await slack.postMessage(event.channel, accumulatedText + workingIndicator);
190
+ }
191
+ });
192
+ await updatePromise;
193
+ }
194
+ },
195
+
196
+ uploadFile: async (filePath: string, title?: string) => {
197
+ await slack.uploadFile(event.channel, filePath, title);
198
+ },
199
+
200
+ setWorking: async (working: boolean) => {
201
+ updatePromise = updatePromise.then(async () => {
202
+ isWorking = working;
203
+ if (messageTs) {
204
+ const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;
205
+ await slack.updateMessage(event.channel, messageTs, displayText);
206
+ }
207
+ });
208
+ await updatePromise;
209
+ },
210
+
211
+ deleteMessage: async () => {
212
+ updatePromise = updatePromise.then(async () => {
213
+ // Delete thread messages first (in reverse order)
214
+ for (let i = threadMessageTs.length - 1; i >= 0; i--) {
215
+ try {
216
+ await slack.deleteMessage(event.channel, threadMessageTs[i]);
217
+ } catch {
218
+ // Ignore errors deleting thread messages
219
+ }
220
+ }
221
+ threadMessageTs.length = 0;
222
+ // Then delete main message
223
+ if (messageTs) {
224
+ await slack.deleteMessage(event.channel, messageTs);
225
+ messageTs = null;
226
+ }
227
+ });
228
+ await updatePromise;
229
+ },
230
+ };
231
+ }
232
+
233
+ // ============================================================================
234
+ // Handler
235
+ // ============================================================================
236
+
237
+ const handler: MomHandler = {
238
+ isRunning(channelId: string): boolean {
239
+ const state = channelStates.get(channelId);
240
+ return state?.running ?? false;
241
+ },
242
+
243
+ async handleStop(channelId: string, slack: SlackBot): Promise<void> {
244
+ const state = channelStates.get(channelId);
245
+ if (state?.running) {
246
+ state.stopRequested = true;
247
+ state.runner.abort();
248
+ const ts = await slack.postMessage(channelId, "_Stopping..._");
249
+ state.stopMessageTs = ts; // Save for updating later
250
+ } else {
251
+ await slack.postMessage(channelId, "_Nothing running_");
252
+ }
253
+ },
254
+
255
+ async handleEvent(event: SlackEvent, slack: SlackBot, isEvent?: boolean): Promise<void> {
256
+ const state = getState(event.channel);
257
+ const channelDir = join(workingDir, event.channel);
258
+
259
+ // Start run
260
+ state.running = true;
261
+ state.stopRequested = false;
262
+
263
+ log.logInfo(`[${event.channel}] Starting run: ${event.text.substring(0, 50)}`);
264
+
265
+ try {
266
+ // SYNC context from log.jsonl BEFORE processing
267
+ // This adds any messages that were logged while mom wasn't running
268
+ // Exclude messages >= current ts (will be handled by agent)
269
+ const syncedCount = syncLogToContext(channelDir, event.ts);
270
+ if (syncedCount > 0) {
271
+ log.logInfo(`[${event.channel}] Synced ${syncedCount} messages from log to context`);
272
+ }
273
+
274
+ // Create context adapter
275
+ const ctx = createSlackContext(event, slack, state, isEvent);
276
+
277
+ // Run the agent
278
+ await ctx.setTyping(true);
279
+ await ctx.setWorking(true);
280
+ const result = await state.runner.run(ctx as any, state.store);
281
+ await ctx.setWorking(false);
282
+
283
+ if (result.stopReason === "aborted" && state.stopRequested) {
284
+ if (state.stopMessageTs) {
285
+ await slack.updateMessage(event.channel, state.stopMessageTs, "_Stopped_");
286
+ state.stopMessageTs = undefined;
287
+ } else {
288
+ await slack.postMessage(event.channel, "_Stopped_");
289
+ }
290
+ }
291
+ } catch (err) {
292
+ log.logWarning(`[${event.channel}] Run error`, err instanceof Error ? err.message : String(err));
293
+ } finally {
294
+ state.running = false;
295
+ }
296
+ },
297
+ };
298
+
299
+ // ============================================================================
300
+ // Start
301
+ // ============================================================================
302
+
303
+ log.logStartup(workingDir, sandbox.type === "host" ? "host" : `docker:${sandbox.container}`);
304
+
305
+ // Shared store for attachment downloads (also used per-channel in getState)
306
+ const sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! });
307
+
308
+ const bot = new SlackBotClass(handler, {
309
+ appToken: MOM_SLACK_APP_TOKEN,
310
+ botToken: MOM_SLACK_BOT_TOKEN,
311
+ workingDir,
312
+ store: sharedStore,
313
+ });
314
+
315
+ // Start events watcher
316
+ const eventsWatcher = createEventsWatcher(workingDir, bot);
317
+ eventsWatcher.start();
318
+
319
+ // Handle shutdown
320
+ process.on("SIGINT", () => {
321
+ log.logInfo("Shutting down...");
322
+ eventsWatcher.stop();
323
+ process.exit(0);
324
+ });
325
+
326
+ process.on("SIGTERM", () => {
327
+ log.logInfo("Shutting down...");
328
+ eventsWatcher.stop();
329
+ process.exit(0);
330
+ });
331
+
332
+ bot.start();