@mariozechner/pi-mom 0.9.4 → 0.10.1

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/dist/log.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ export interface LogContext {
2
+ channelId: string;
3
+ userName?: string;
4
+ channelName?: string;
5
+ }
6
+ export declare function logUserMessage(ctx: LogContext, text: string): void;
7
+ export declare function logToolStart(ctx: LogContext, toolName: string, label: string, args: Record<string, unknown>): void;
8
+ export declare function logToolSuccess(ctx: LogContext, toolName: string, durationMs: number, result: string): void;
9
+ export declare function logToolError(ctx: LogContext, toolName: string, durationMs: number, error: string): void;
10
+ export declare function logResponseStart(ctx: LogContext): void;
11
+ export declare function logThinking(ctx: LogContext, thinking: string): void;
12
+ export declare function logResponse(ctx: LogContext, text: string): void;
13
+ export declare function logDownloadStart(ctx: LogContext, filename: string, localPath: string): void;
14
+ export declare function logDownloadSuccess(ctx: LogContext, sizeKB: number): void;
15
+ export declare function logDownloadError(ctx: LogContext, filename: string, error: string): void;
16
+ export declare function logStopRequest(ctx: LogContext): void;
17
+ export declare function logWarning(message: string, details?: string): void;
18
+ export declare function logAgentError(ctx: LogContext | "system", error: string): void;
19
+ export declare function logUsageSummary(ctx: LogContext, usage: {
20
+ input: number;
21
+ output: number;
22
+ cacheRead: number;
23
+ cacheWrite: number;
24
+ cost: {
25
+ input: number;
26
+ output: number;
27
+ cacheRead: number;
28
+ cacheWrite: number;
29
+ total: number;
30
+ };
31
+ }): string;
32
+ export declare function logStartup(workingDir: string, sandbox: string): void;
33
+ export declare function logConnected(): void;
34
+ export declare function logDisconnected(): void;
35
+ //# sourceMappingURL=log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAiED,wBAAgB,cAAc,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAElE;AAGD,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAWlH;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAY1G;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAUvG;AAGD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAEtD;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAQnE;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAQ/D;AAGD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAG3F;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAExE;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAGvF;AAGD,wBAAgB,cAAc,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAGpD;AAGD,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CASlE;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,GAAG,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAQ7E;AAGD,wBAAgB,eAAe,CAC9B,GAAG,EAAE,UAAU,EACf,KAAK,EAAE;IACN,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9F,GACC,MAAM,CA8BR;AAGD,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAIpE;AAED,wBAAgB,YAAY,IAAI,IAAI,CAGnC;AAED,wBAAgB,eAAe,IAAI,IAAI,CAEtC","sourcesContent":["import chalk from \"chalk\";\n\nexport interface LogContext {\n\tchannelId: string;\n\tuserName?: string;\n\tchannelName?: string; // For display like #dev-team vs C16HET4EQ\n}\n\nfunction timestamp(): string {\n\tconst now = new Date();\n\tconst hh = String(now.getHours()).padStart(2, \"0\");\n\tconst mm = String(now.getMinutes()).padStart(2, \"0\");\n\tconst ss = String(now.getSeconds()).padStart(2, \"0\");\n\treturn `[${hh}:${mm}:${ss}]`;\n}\n\nfunction formatContext(ctx: LogContext): string {\n\t// DMs: [DM:username]\n\t// Channels: [#channel-name:username] or [C16HET4EQ:username] if no name\n\tif (ctx.channelId.startsWith(\"D\")) {\n\t\treturn `[DM:${ctx.userName || ctx.channelId}]`;\n\t}\n\tconst channel = ctx.channelName || ctx.channelId;\n\tconst user = ctx.userName || \"unknown\";\n\treturn `[${channel.startsWith(\"#\") ? channel : `#${channel}`}:${user}]`;\n}\n\nfunction truncate(text: string, maxLen: number): string {\n\tif (text.length <= maxLen) return text;\n\treturn text.substring(0, maxLen) + `\\n(truncated at ${maxLen} chars)`;\n}\n\nfunction formatToolArgs(args: Record<string, unknown>): string {\n\tconst lines: string[] = [];\n\n\tfor (const [key, value] of Object.entries(args)) {\n\t\t// Skip the label - it's already shown in the tool name\n\t\tif (key === \"label\") continue;\n\n\t\t// For read tool, format path with offset/limit\n\t\tif (key === \"path\" && typeof value === \"string\") {\n\t\t\tconst offset = args.offset as number | undefined;\n\t\t\tconst limit = args.limit as number | undefined;\n\t\t\tif (offset !== undefined && limit !== undefined) {\n\t\t\t\tlines.push(`${value}:${offset}-${offset + limit}`);\n\t\t\t} else {\n\t\t\t\tlines.push(value);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Skip offset/limit since we already handled them\n\t\tif (key === \"offset\" || key === \"limit\") continue;\n\n\t\t// For other values, format them\n\t\tif (typeof value === \"string\") {\n\t\t\t// Multi-line strings get indented\n\t\t\tif (value.includes(\"\\n\")) {\n\t\t\t\tlines.push(value);\n\t\t\t} else {\n\t\t\t\tlines.push(value);\n\t\t\t}\n\t\t} else {\n\t\t\tlines.push(JSON.stringify(value));\n\t\t}\n\t}\n\n\treturn lines.join(\"\\n\");\n}\n\n// User messages\nexport function logUserMessage(ctx: LogContext, text: string): void {\n\tconsole.log(chalk.green(`${timestamp()} ${formatContext(ctx)} ${text}`));\n}\n\n// Tool execution\nexport function logToolStart(ctx: LogContext, toolName: string, label: string, args: Record<string, unknown>): void {\n\tconst formattedArgs = formatToolArgs(args);\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ↳ ${toolName}: ${label}`));\n\tif (formattedArgs) {\n\t\t// Indent the args\n\t\tconst indented = formattedArgs\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => ` ${line}`)\n\t\t\t.join(\"\\n\");\n\t\tconsole.log(chalk.dim(indented));\n\t}\n}\n\nexport function logToolSuccess(ctx: LogContext, toolName: string, durationMs: number, result: string): void {\n\tconst duration = (durationMs / 1000).toFixed(1);\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✓ ${toolName} (${duration}s)`));\n\n\tconst truncated = truncate(result, 1000);\n\tif (truncated) {\n\t\tconst indented = truncated\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => ` ${line}`)\n\t\t\t.join(\"\\n\");\n\t\tconsole.log(chalk.dim(indented));\n\t}\n}\n\nexport function logToolError(ctx: LogContext, toolName: string, durationMs: number, error: string): void {\n\tconst duration = (durationMs / 1000).toFixed(1);\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✗ ${toolName} (${duration}s)`));\n\n\tconst truncated = truncate(error, 1000);\n\tconst indented = truncated\n\t\t.split(\"\\n\")\n\t\t.map((line) => ` ${line}`)\n\t\t.join(\"\\n\");\n\tconsole.log(chalk.dim(indented));\n}\n\n// Response streaming\nexport function logResponseStart(ctx: LogContext): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} → Streaming response...`));\n}\n\nexport function logThinking(ctx: LogContext, thinking: string): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💭 Thinking`));\n\tconst truncated = truncate(thinking, 1000);\n\tconst indented = truncated\n\t\t.split(\"\\n\")\n\t\t.map((line) => ` ${line}`)\n\t\t.join(\"\\n\");\n\tconsole.log(chalk.dim(indented));\n}\n\nexport function logResponse(ctx: LogContext, text: string): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💬 Response`));\n\tconst truncated = truncate(text, 1000);\n\tconst indented = truncated\n\t\t.split(\"\\n\")\n\t\t.map((line) => ` ${line}`)\n\t\t.join(\"\\n\");\n\tconsole.log(chalk.dim(indented));\n}\n\n// Attachments\nexport function logDownloadStart(ctx: LogContext, filename: string, localPath: string): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ↓ Downloading attachment`));\n\tconsole.log(chalk.dim(` ${filename} → ${localPath}`));\n}\n\nexport function logDownloadSuccess(ctx: LogContext, sizeKB: number): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✓ Downloaded (${sizeKB.toLocaleString()} KB)`));\n}\n\nexport function logDownloadError(ctx: LogContext, filename: string, error: string): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✗ Download failed`));\n\tconsole.log(chalk.dim(` ${filename}: ${error}`));\n}\n\n// Control\nexport function logStopRequest(ctx: LogContext): void {\n\tconsole.log(chalk.green(`${timestamp()} ${formatContext(ctx)} stop`));\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ⊗ Stop requested - aborting`));\n}\n\n// System\nexport function logWarning(message: string, details?: string): void {\n\tconsole.log(chalk.yellow(`${timestamp()} [system] ⚠ ${message}`));\n\tif (details) {\n\t\tconst indented = details\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => ` ${line}`)\n\t\t\t.join(\"\\n\");\n\t\tconsole.log(chalk.dim(indented));\n\t}\n}\n\nexport function logAgentError(ctx: LogContext | \"system\", error: string): void {\n\tconst context = ctx === \"system\" ? \"[system]\" : formatContext(ctx);\n\tconsole.log(chalk.yellow(`${timestamp()} ${context} ✗ Agent error`));\n\tconst indented = error\n\t\t.split(\"\\n\")\n\t\t.map((line) => ` ${line}`)\n\t\t.join(\"\\n\");\n\tconsole.log(chalk.dim(indented));\n}\n\n// Usage summary\nexport function logUsageSummary(\n\tctx: LogContext,\n\tusage: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\tcost: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number };\n\t},\n): string {\n\tconst lines: string[] = [];\n\tlines.push(\"*Usage Summary*\");\n\tlines.push(`Tokens: ${usage.input.toLocaleString()} in, ${usage.output.toLocaleString()} out`);\n\tif (usage.cacheRead > 0 || usage.cacheWrite > 0) {\n\t\tlines.push(`Cache: ${usage.cacheRead.toLocaleString()} read, ${usage.cacheWrite.toLocaleString()} write`);\n\t}\n\tlines.push(\n\t\t`Cost: $${usage.cost.input.toFixed(4)} in, $${usage.cost.output.toFixed(4)} out` +\n\t\t\t(usage.cacheRead > 0 || usage.cacheWrite > 0\n\t\t\t\t? `, $${usage.cost.cacheRead.toFixed(4)} cache read, $${usage.cost.cacheWrite.toFixed(4)} cache write`\n\t\t\t\t: \"\"),\n\t);\n\tlines.push(`*Total: $${usage.cost.total.toFixed(4)}*`);\n\n\tconst summary = lines.join(\"\\n\");\n\n\t// Log to console\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💰 Usage`));\n\tconsole.log(\n\t\tchalk.dim(\n\t\t\t` ${usage.input.toLocaleString()} in + ${usage.output.toLocaleString()} out` +\n\t\t\t\t(usage.cacheRead > 0 || usage.cacheWrite > 0\n\t\t\t\t\t? ` (${usage.cacheRead.toLocaleString()} cache read, ${usage.cacheWrite.toLocaleString()} cache write)`\n\t\t\t\t\t: \"\") +\n\t\t\t\t` = $${usage.cost.total.toFixed(4)}`,\n\t\t),\n\t);\n\n\treturn summary;\n}\n\n// Startup (no context needed)\nexport function logStartup(workingDir: string, sandbox: string): void {\n\tconsole.log(\"Starting mom bot...\");\n\tconsole.log(` Working directory: ${workingDir}`);\n\tconsole.log(` Sandbox: ${sandbox}`);\n}\n\nexport function logConnected(): void {\n\tconsole.log(\"⚡️ Mom bot connected and listening!\");\n\tconsole.log(\"\");\n}\n\nexport function logDisconnected(): void {\n\tconsole.log(\"Mom bot disconnected.\");\n}\n"]}
package/dist/log.js ADDED
@@ -0,0 +1,195 @@
1
+ import chalk from "chalk";
2
+ function timestamp() {
3
+ const now = new Date();
4
+ const hh = String(now.getHours()).padStart(2, "0");
5
+ const mm = String(now.getMinutes()).padStart(2, "0");
6
+ const ss = String(now.getSeconds()).padStart(2, "0");
7
+ return `[${hh}:${mm}:${ss}]`;
8
+ }
9
+ function formatContext(ctx) {
10
+ // DMs: [DM:username]
11
+ // Channels: [#channel-name:username] or [C16HET4EQ:username] if no name
12
+ if (ctx.channelId.startsWith("D")) {
13
+ return `[DM:${ctx.userName || ctx.channelId}]`;
14
+ }
15
+ const channel = ctx.channelName || ctx.channelId;
16
+ const user = ctx.userName || "unknown";
17
+ return `[${channel.startsWith("#") ? channel : `#${channel}`}:${user}]`;
18
+ }
19
+ function truncate(text, maxLen) {
20
+ if (text.length <= maxLen)
21
+ return text;
22
+ return text.substring(0, maxLen) + `\n(truncated at ${maxLen} chars)`;
23
+ }
24
+ function formatToolArgs(args) {
25
+ const lines = [];
26
+ for (const [key, value] of Object.entries(args)) {
27
+ // Skip the label - it's already shown in the tool name
28
+ if (key === "label")
29
+ continue;
30
+ // For read tool, format path with offset/limit
31
+ if (key === "path" && typeof value === "string") {
32
+ const offset = args.offset;
33
+ const limit = args.limit;
34
+ if (offset !== undefined && limit !== undefined) {
35
+ lines.push(`${value}:${offset}-${offset + limit}`);
36
+ }
37
+ else {
38
+ lines.push(value);
39
+ }
40
+ continue;
41
+ }
42
+ // Skip offset/limit since we already handled them
43
+ if (key === "offset" || key === "limit")
44
+ continue;
45
+ // For other values, format them
46
+ if (typeof value === "string") {
47
+ // Multi-line strings get indented
48
+ if (value.includes("\n")) {
49
+ lines.push(value);
50
+ }
51
+ else {
52
+ lines.push(value);
53
+ }
54
+ }
55
+ else {
56
+ lines.push(JSON.stringify(value));
57
+ }
58
+ }
59
+ return lines.join("\n");
60
+ }
61
+ // User messages
62
+ export function logUserMessage(ctx, text) {
63
+ console.log(chalk.green(`${timestamp()} ${formatContext(ctx)} ${text}`));
64
+ }
65
+ // Tool execution
66
+ export function logToolStart(ctx, toolName, label, args) {
67
+ const formattedArgs = formatToolArgs(args);
68
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ↳ ${toolName}: ${label}`));
69
+ if (formattedArgs) {
70
+ // Indent the args
71
+ const indented = formattedArgs
72
+ .split("\n")
73
+ .map((line) => ` ${line}`)
74
+ .join("\n");
75
+ console.log(chalk.dim(indented));
76
+ }
77
+ }
78
+ export function logToolSuccess(ctx, toolName, durationMs, result) {
79
+ const duration = (durationMs / 1000).toFixed(1);
80
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✓ ${toolName} (${duration}s)`));
81
+ const truncated = truncate(result, 1000);
82
+ if (truncated) {
83
+ const indented = truncated
84
+ .split("\n")
85
+ .map((line) => ` ${line}`)
86
+ .join("\n");
87
+ console.log(chalk.dim(indented));
88
+ }
89
+ }
90
+ export function logToolError(ctx, toolName, durationMs, error) {
91
+ const duration = (durationMs / 1000).toFixed(1);
92
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✗ ${toolName} (${duration}s)`));
93
+ const truncated = truncate(error, 1000);
94
+ const indented = truncated
95
+ .split("\n")
96
+ .map((line) => ` ${line}`)
97
+ .join("\n");
98
+ console.log(chalk.dim(indented));
99
+ }
100
+ // Response streaming
101
+ export function logResponseStart(ctx) {
102
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} → Streaming response...`));
103
+ }
104
+ export function logThinking(ctx, thinking) {
105
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💭 Thinking`));
106
+ const truncated = truncate(thinking, 1000);
107
+ const indented = truncated
108
+ .split("\n")
109
+ .map((line) => ` ${line}`)
110
+ .join("\n");
111
+ console.log(chalk.dim(indented));
112
+ }
113
+ export function logResponse(ctx, text) {
114
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💬 Response`));
115
+ const truncated = truncate(text, 1000);
116
+ const indented = truncated
117
+ .split("\n")
118
+ .map((line) => ` ${line}`)
119
+ .join("\n");
120
+ console.log(chalk.dim(indented));
121
+ }
122
+ // Attachments
123
+ export function logDownloadStart(ctx, filename, localPath) {
124
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ↓ Downloading attachment`));
125
+ console.log(chalk.dim(` ${filename} → ${localPath}`));
126
+ }
127
+ export function logDownloadSuccess(ctx, sizeKB) {
128
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✓ Downloaded (${sizeKB.toLocaleString()} KB)`));
129
+ }
130
+ export function logDownloadError(ctx, filename, error) {
131
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✗ Download failed`));
132
+ console.log(chalk.dim(` ${filename}: ${error}`));
133
+ }
134
+ // Control
135
+ export function logStopRequest(ctx) {
136
+ console.log(chalk.green(`${timestamp()} ${formatContext(ctx)} stop`));
137
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ⊗ Stop requested - aborting`));
138
+ }
139
+ // System
140
+ export function logWarning(message, details) {
141
+ console.log(chalk.yellow(`${timestamp()} [system] ⚠ ${message}`));
142
+ if (details) {
143
+ const indented = details
144
+ .split("\n")
145
+ .map((line) => ` ${line}`)
146
+ .join("\n");
147
+ console.log(chalk.dim(indented));
148
+ }
149
+ }
150
+ export function logAgentError(ctx, error) {
151
+ const context = ctx === "system" ? "[system]" : formatContext(ctx);
152
+ console.log(chalk.yellow(`${timestamp()} ${context} ✗ Agent error`));
153
+ const indented = error
154
+ .split("\n")
155
+ .map((line) => ` ${line}`)
156
+ .join("\n");
157
+ console.log(chalk.dim(indented));
158
+ }
159
+ // Usage summary
160
+ export function logUsageSummary(ctx, usage) {
161
+ const lines = [];
162
+ lines.push("*Usage Summary*");
163
+ lines.push(`Tokens: ${usage.input.toLocaleString()} in, ${usage.output.toLocaleString()} out`);
164
+ if (usage.cacheRead > 0 || usage.cacheWrite > 0) {
165
+ lines.push(`Cache: ${usage.cacheRead.toLocaleString()} read, ${usage.cacheWrite.toLocaleString()} write`);
166
+ }
167
+ lines.push(`Cost: $${usage.cost.input.toFixed(4)} in, $${usage.cost.output.toFixed(4)} out` +
168
+ (usage.cacheRead > 0 || usage.cacheWrite > 0
169
+ ? `, $${usage.cost.cacheRead.toFixed(4)} cache read, $${usage.cost.cacheWrite.toFixed(4)} cache write`
170
+ : ""));
171
+ lines.push(`*Total: $${usage.cost.total.toFixed(4)}*`);
172
+ const summary = lines.join("\n");
173
+ // Log to console
174
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💰 Usage`));
175
+ console.log(chalk.dim(` ${usage.input.toLocaleString()} in + ${usage.output.toLocaleString()} out` +
176
+ (usage.cacheRead > 0 || usage.cacheWrite > 0
177
+ ? ` (${usage.cacheRead.toLocaleString()} cache read, ${usage.cacheWrite.toLocaleString()} cache write)`
178
+ : "") +
179
+ ` = $${usage.cost.total.toFixed(4)}`));
180
+ return summary;
181
+ }
182
+ // Startup (no context needed)
183
+ export function logStartup(workingDir, sandbox) {
184
+ console.log("Starting mom bot...");
185
+ console.log(` Working directory: ${workingDir}`);
186
+ console.log(` Sandbox: ${sandbox}`);
187
+ }
188
+ export function logConnected() {
189
+ console.log("⚡️ Mom bot connected and listening!");
190
+ console.log("");
191
+ }
192
+ export function logDisconnected() {
193
+ console.log("Mom bot disconnected.");
194
+ }
195
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"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","sourcesContent":["import chalk from \"chalk\";\n\nexport interface LogContext {\n\tchannelId: string;\n\tuserName?: string;\n\tchannelName?: string; // For display like #dev-team vs C16HET4EQ\n}\n\nfunction timestamp(): string {\n\tconst now = new Date();\n\tconst hh = String(now.getHours()).padStart(2, \"0\");\n\tconst mm = String(now.getMinutes()).padStart(2, \"0\");\n\tconst ss = String(now.getSeconds()).padStart(2, \"0\");\n\treturn `[${hh}:${mm}:${ss}]`;\n}\n\nfunction formatContext(ctx: LogContext): string {\n\t// DMs: [DM:username]\n\t// Channels: [#channel-name:username] or [C16HET4EQ:username] if no name\n\tif (ctx.channelId.startsWith(\"D\")) {\n\t\treturn `[DM:${ctx.userName || ctx.channelId}]`;\n\t}\n\tconst channel = ctx.channelName || ctx.channelId;\n\tconst user = ctx.userName || \"unknown\";\n\treturn `[${channel.startsWith(\"#\") ? channel : `#${channel}`}:${user}]`;\n}\n\nfunction truncate(text: string, maxLen: number): string {\n\tif (text.length <= maxLen) return text;\n\treturn text.substring(0, maxLen) + `\\n(truncated at ${maxLen} chars)`;\n}\n\nfunction formatToolArgs(args: Record<string, unknown>): string {\n\tconst lines: string[] = [];\n\n\tfor (const [key, value] of Object.entries(args)) {\n\t\t// Skip the label - it's already shown in the tool name\n\t\tif (key === \"label\") continue;\n\n\t\t// For read tool, format path with offset/limit\n\t\tif (key === \"path\" && typeof value === \"string\") {\n\t\t\tconst offset = args.offset as number | undefined;\n\t\t\tconst limit = args.limit as number | undefined;\n\t\t\tif (offset !== undefined && limit !== undefined) {\n\t\t\t\tlines.push(`${value}:${offset}-${offset + limit}`);\n\t\t\t} else {\n\t\t\t\tlines.push(value);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Skip offset/limit since we already handled them\n\t\tif (key === \"offset\" || key === \"limit\") continue;\n\n\t\t// For other values, format them\n\t\tif (typeof value === \"string\") {\n\t\t\t// Multi-line strings get indented\n\t\t\tif (value.includes(\"\\n\")) {\n\t\t\t\tlines.push(value);\n\t\t\t} else {\n\t\t\t\tlines.push(value);\n\t\t\t}\n\t\t} else {\n\t\t\tlines.push(JSON.stringify(value));\n\t\t}\n\t}\n\n\treturn lines.join(\"\\n\");\n}\n\n// User messages\nexport function logUserMessage(ctx: LogContext, text: string): void {\n\tconsole.log(chalk.green(`${timestamp()} ${formatContext(ctx)} ${text}`));\n}\n\n// Tool execution\nexport function logToolStart(ctx: LogContext, toolName: string, label: string, args: Record<string, unknown>): void {\n\tconst formattedArgs = formatToolArgs(args);\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ↳ ${toolName}: ${label}`));\n\tif (formattedArgs) {\n\t\t// Indent the args\n\t\tconst indented = formattedArgs\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => ` ${line}`)\n\t\t\t.join(\"\\n\");\n\t\tconsole.log(chalk.dim(indented));\n\t}\n}\n\nexport function logToolSuccess(ctx: LogContext, toolName: string, durationMs: number, result: string): void {\n\tconst duration = (durationMs / 1000).toFixed(1);\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✓ ${toolName} (${duration}s)`));\n\n\tconst truncated = truncate(result, 1000);\n\tif (truncated) {\n\t\tconst indented = truncated\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => ` ${line}`)\n\t\t\t.join(\"\\n\");\n\t\tconsole.log(chalk.dim(indented));\n\t}\n}\n\nexport function logToolError(ctx: LogContext, toolName: string, durationMs: number, error: string): void {\n\tconst duration = (durationMs / 1000).toFixed(1);\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✗ ${toolName} (${duration}s)`));\n\n\tconst truncated = truncate(error, 1000);\n\tconst indented = truncated\n\t\t.split(\"\\n\")\n\t\t.map((line) => ` ${line}`)\n\t\t.join(\"\\n\");\n\tconsole.log(chalk.dim(indented));\n}\n\n// Response streaming\nexport function logResponseStart(ctx: LogContext): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} → Streaming response...`));\n}\n\nexport function logThinking(ctx: LogContext, thinking: string): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💭 Thinking`));\n\tconst truncated = truncate(thinking, 1000);\n\tconst indented = truncated\n\t\t.split(\"\\n\")\n\t\t.map((line) => ` ${line}`)\n\t\t.join(\"\\n\");\n\tconsole.log(chalk.dim(indented));\n}\n\nexport function logResponse(ctx: LogContext, text: string): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💬 Response`));\n\tconst truncated = truncate(text, 1000);\n\tconst indented = truncated\n\t\t.split(\"\\n\")\n\t\t.map((line) => ` ${line}`)\n\t\t.join(\"\\n\");\n\tconsole.log(chalk.dim(indented));\n}\n\n// Attachments\nexport function logDownloadStart(ctx: LogContext, filename: string, localPath: string): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ↓ Downloading attachment`));\n\tconsole.log(chalk.dim(` ${filename} → ${localPath}`));\n}\n\nexport function logDownloadSuccess(ctx: LogContext, sizeKB: number): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✓ Downloaded (${sizeKB.toLocaleString()} KB)`));\n}\n\nexport function logDownloadError(ctx: LogContext, filename: string, error: string): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✗ Download failed`));\n\tconsole.log(chalk.dim(` ${filename}: ${error}`));\n}\n\n// Control\nexport function logStopRequest(ctx: LogContext): void {\n\tconsole.log(chalk.green(`${timestamp()} ${formatContext(ctx)} stop`));\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ⊗ Stop requested - aborting`));\n}\n\n// System\nexport function logWarning(message: string, details?: string): void {\n\tconsole.log(chalk.yellow(`${timestamp()} [system] ⚠ ${message}`));\n\tif (details) {\n\t\tconst indented = details\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => ` ${line}`)\n\t\t\t.join(\"\\n\");\n\t\tconsole.log(chalk.dim(indented));\n\t}\n}\n\nexport function logAgentError(ctx: LogContext | \"system\", error: string): void {\n\tconst context = ctx === \"system\" ? \"[system]\" : formatContext(ctx);\n\tconsole.log(chalk.yellow(`${timestamp()} ${context} ✗ Agent error`));\n\tconst indented = error\n\t\t.split(\"\\n\")\n\t\t.map((line) => ` ${line}`)\n\t\t.join(\"\\n\");\n\tconsole.log(chalk.dim(indented));\n}\n\n// Usage summary\nexport function logUsageSummary(\n\tctx: LogContext,\n\tusage: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\tcost: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number };\n\t},\n): string {\n\tconst lines: string[] = [];\n\tlines.push(\"*Usage Summary*\");\n\tlines.push(`Tokens: ${usage.input.toLocaleString()} in, ${usage.output.toLocaleString()} out`);\n\tif (usage.cacheRead > 0 || usage.cacheWrite > 0) {\n\t\tlines.push(`Cache: ${usage.cacheRead.toLocaleString()} read, ${usage.cacheWrite.toLocaleString()} write`);\n\t}\n\tlines.push(\n\t\t`Cost: $${usage.cost.input.toFixed(4)} in, $${usage.cost.output.toFixed(4)} out` +\n\t\t\t(usage.cacheRead > 0 || usage.cacheWrite > 0\n\t\t\t\t? `, $${usage.cost.cacheRead.toFixed(4)} cache read, $${usage.cost.cacheWrite.toFixed(4)} cache write`\n\t\t\t\t: \"\"),\n\t);\n\tlines.push(`*Total: $${usage.cost.total.toFixed(4)}*`);\n\n\tconst summary = lines.join(\"\\n\");\n\n\t// Log to console\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💰 Usage`));\n\tconsole.log(\n\t\tchalk.dim(\n\t\t\t` ${usage.input.toLocaleString()} in + ${usage.output.toLocaleString()} out` +\n\t\t\t\t(usage.cacheRead > 0 || usage.cacheWrite > 0\n\t\t\t\t\t? ` (${usage.cacheRead.toLocaleString()} cache read, ${usage.cacheWrite.toLocaleString()} cache write)`\n\t\t\t\t\t: \"\") +\n\t\t\t\t` = $${usage.cost.total.toFixed(4)}`,\n\t\t),\n\t);\n\n\treturn summary;\n}\n\n// Startup (no context needed)\nexport function logStartup(workingDir: string, sandbox: string): void {\n\tconsole.log(\"Starting mom bot...\");\n\tconsole.log(` Working directory: ${workingDir}`);\n\tconsole.log(` Sandbox: ${sandbox}`);\n}\n\nexport function logConnected(): void {\n\tconsole.log(\"⚡️ Mom bot connected and listening!\");\n\tconsole.log(\"\");\n}\n\nexport function logDisconnected(): void {\n\tconsole.log(\"Mom bot disconnected.\");\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"","sourcesContent":["#!/usr/bin/env node\n\nimport { join, resolve } from \"path\";\nimport { type AgentRunner, createAgentRunner } from \"./agent.js\";\nimport { parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { MomBot, type SlackContext } from \"./slack.js\";\n\nconst MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;\nconst MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;\nconst ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;\nconst ANTHROPIC_OAUTH_TOKEN = process.env.ANTHROPIC_OAUTH_TOKEN;\n\n// Parse command line arguments\nfunction parseArgs(): { workingDir: string; sandbox: SandboxConfig } {\n\tconst args = process.argv.slice(2);\n\tlet sandbox: SandboxConfig = { type: \"host\" };\n\tlet workingDir: string | undefined;\n\n\tfor (let i = 0; i < args.length; i++) {\n\t\tconst arg = args[i];\n\t\tif (arg.startsWith(\"--sandbox=\")) {\n\t\t\tsandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n\t\t} else if (arg === \"--sandbox\") {\n\t\t\tconst next = args[++i];\n\t\t\tif (!next) {\n\t\t\t\tconsole.error(\"Error: --sandbox requires a value (host or docker:<container-name>)\");\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t\tsandbox = parseSandboxArg(next);\n\t\t} else if (!arg.startsWith(\"-\")) {\n\t\t\tworkingDir = arg;\n\t\t} else {\n\t\t\tconsole.error(`Unknown option: ${arg}`);\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\tif (!workingDir) {\n\t\tconsole.error(\"Usage: mom [--sandbox=host|docker:<container-name>] <working-directory>\");\n\t\tconsole.error(\"\");\n\t\tconsole.error(\"Options:\");\n\t\tconsole.error(\" --sandbox=host Run tools directly on host (default)\");\n\t\tconsole.error(\" --sandbox=docker:<container> Run tools in Docker container\");\n\t\tconsole.error(\"\");\n\t\tconsole.error(\"Examples:\");\n\t\tconsole.error(\" mom ./data\");\n\t\tconsole.error(\" mom --sandbox=docker:mom-sandbox ./data\");\n\t\tprocess.exit(1);\n\t}\n\n\treturn { workingDir: resolve(workingDir), sandbox };\n}\n\nconst { workingDir, sandbox } = parseArgs();\n\nconsole.log(\"Starting mom bot...\");\nconsole.log(` Working directory: ${workingDir}`);\nconsole.log(` Sandbox: ${sandbox.type === \"host\" ? \"host\" : `docker:${sandbox.container}`}`);\n\nif (!MOM_SLACK_APP_TOKEN || !MOM_SLACK_BOT_TOKEN || (!ANTHROPIC_API_KEY && !ANTHROPIC_OAUTH_TOKEN)) {\n\tconsole.error(\"Missing required environment variables:\");\n\tif (!MOM_SLACK_APP_TOKEN) console.error(\" - MOM_SLACK_APP_TOKEN (xapp-...)\");\n\tif (!MOM_SLACK_BOT_TOKEN) console.error(\" - MOM_SLACK_BOT_TOKEN (xoxb-...)\");\n\tif (!ANTHROPIC_API_KEY && !ANTHROPIC_OAUTH_TOKEN) console.error(\" - ANTHROPIC_API_KEY or ANTHROPIC_OAUTH_TOKEN\");\n\tprocess.exit(1);\n}\n\n// Validate sandbox configuration\nawait validateSandbox(sandbox);\n\n// Track active agent runs per channel\nconst activeRuns = new Map<string, AgentRunner>();\n\nasync function handleMessage(ctx: SlackContext, source: \"channel\" | \"dm\"): Promise<void> {\n\tconst channelId = ctx.message.channel;\n\tconst messageText = ctx.message.text.toLowerCase().trim();\n\n\t// Check for stop command\n\tif (messageText === \"stop\") {\n\t\tconst runner = activeRuns.get(channelId);\n\t\tif (runner) {\n\t\t\tconsole.log(`Stop requested for channel ${channelId}`);\n\t\t\trunner.abort();\n\t\t\tawait ctx.respond(\"_Stopping..._\");\n\t\t} else {\n\t\t\tawait ctx.respond(\"_Nothing running._\");\n\t\t}\n\t\treturn;\n\t}\n\n\t// Check if already running in this channel\n\tif (activeRuns.has(channelId)) {\n\t\tawait ctx.respond(\"_Already working on something. Say `@mom stop` to cancel._\");\n\t\treturn;\n\t}\n\n\tconsole.log(`${source === \"channel\" ? \"Channel mention\" : \"DM\"} from <@${ctx.message.user}>: ${ctx.message.text}`);\n\tconst channelDir = join(workingDir, channelId);\n\n\tconst runner = createAgentRunner(sandbox);\n\tactiveRuns.set(channelId, runner);\n\n\tawait ctx.setTyping(true);\n\ttry {\n\t\tawait runner.run(ctx, channelDir, ctx.store);\n\t} catch (error) {\n\t\t// Don't report abort errors\n\t\tconst msg = error instanceof Error ? error.message : String(error);\n\t\tif (msg.includes(\"aborted\") || msg.includes(\"Aborted\")) {\n\t\t\t// Already said \"Stopping...\" - nothing more to say\n\t\t} else {\n\t\t\tconsole.error(\"Agent error:\", error);\n\t\t\tawait ctx.respond(`❌ Error: ${msg}`);\n\t\t}\n\t} finally {\n\t\tactiveRuns.delete(channelId);\n\t}\n}\n\nconst bot = new MomBot(\n\t{\n\t\tasync onChannelMention(ctx) {\n\t\t\tawait handleMessage(ctx, \"channel\");\n\t\t},\n\n\t\tasync onDirectMessage(ctx) {\n\t\t\tawait handleMessage(ctx, \"dm\");\n\t\t},\n\t},\n\t{\n\t\tappToken: MOM_SLACK_APP_TOKEN,\n\t\tbotToken: MOM_SLACK_BOT_TOKEN,\n\t\tworkingDir,\n\t},\n);\n\nbot.start();\n"]}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"","sourcesContent":["#!/usr/bin/env node\n\nimport { join, resolve } from \"path\";\nimport { type AgentRunner, createAgentRunner } from \"./agent.js\";\nimport * as log from \"./log.js\";\nimport { parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { MomBot, type SlackContext } from \"./slack.js\";\n\nconst MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;\nconst MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;\nconst ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;\nconst ANTHROPIC_OAUTH_TOKEN = process.env.ANTHROPIC_OAUTH_TOKEN;\n\n// Parse command line arguments\nfunction parseArgs(): { workingDir: string; sandbox: SandboxConfig } {\n\tconst args = process.argv.slice(2);\n\tlet sandbox: SandboxConfig = { type: \"host\" };\n\tlet workingDir: string | undefined;\n\n\tfor (let i = 0; i < args.length; i++) {\n\t\tconst arg = args[i];\n\t\tif (arg.startsWith(\"--sandbox=\")) {\n\t\t\tsandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n\t\t} else if (arg === \"--sandbox\") {\n\t\t\tconst next = args[++i];\n\t\t\tif (!next) {\n\t\t\t\tconsole.error(\"Error: --sandbox requires a value (host or docker:<container-name>)\");\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t\tsandbox = parseSandboxArg(next);\n\t\t} else if (!arg.startsWith(\"-\")) {\n\t\t\tworkingDir = arg;\n\t\t} else {\n\t\t\tconsole.error(`Unknown option: ${arg}`);\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\tif (!workingDir) {\n\t\tconsole.error(\"Usage: mom [--sandbox=host|docker:<container-name>] <working-directory>\");\n\t\tconsole.error(\"\");\n\t\tconsole.error(\"Options:\");\n\t\tconsole.error(\" --sandbox=host Run tools directly on host (default)\");\n\t\tconsole.error(\" --sandbox=docker:<container> Run tools in Docker container\");\n\t\tconsole.error(\"\");\n\t\tconsole.error(\"Examples:\");\n\t\tconsole.error(\" mom ./data\");\n\t\tconsole.error(\" mom --sandbox=docker:mom-sandbox ./data\");\n\t\tprocess.exit(1);\n\t}\n\n\treturn { workingDir: resolve(workingDir), sandbox };\n}\n\nconst { workingDir, sandbox } = parseArgs();\n\nlog.logStartup(workingDir, sandbox.type === \"host\" ? \"host\" : `docker:${sandbox.container}`);\n\nif (!MOM_SLACK_APP_TOKEN || !MOM_SLACK_BOT_TOKEN || (!ANTHROPIC_API_KEY && !ANTHROPIC_OAUTH_TOKEN)) {\n\tconsole.error(\"Missing required environment variables:\");\n\tif (!MOM_SLACK_APP_TOKEN) console.error(\" - MOM_SLACK_APP_TOKEN (xapp-...)\");\n\tif (!MOM_SLACK_BOT_TOKEN) console.error(\" - MOM_SLACK_BOT_TOKEN (xoxb-...)\");\n\tif (!ANTHROPIC_API_KEY && !ANTHROPIC_OAUTH_TOKEN) console.error(\" - ANTHROPIC_API_KEY or ANTHROPIC_OAUTH_TOKEN\");\n\tprocess.exit(1);\n}\n\n// Validate sandbox configuration\nawait validateSandbox(sandbox);\n\n// Track active agent runs per channel\nconst activeRuns = new Map<string, { runner: AgentRunner; context: SlackContext; stopContext?: SlackContext }>();\n\nasync function handleMessage(ctx: SlackContext, _source: \"channel\" | \"dm\"): Promise<void> {\n\tconst channelId = ctx.message.channel;\n\tconst messageText = ctx.message.text.toLowerCase().trim();\n\n\tconst logCtx = {\n\t\tchannelId: ctx.message.channel,\n\t\tuserName: ctx.message.userName,\n\t\tchannelName: ctx.channelName,\n\t};\n\n\t// Check for stop command\n\tif (messageText === \"stop\") {\n\t\tconst active = activeRuns.get(channelId);\n\t\tif (active) {\n\t\t\tlog.logStopRequest(logCtx);\n\t\t\t// Post a NEW message saying \"Stopping...\"\n\t\t\tawait ctx.respond(\"_Stopping..._\");\n\t\t\t// Store this context to update it to \"Stopped\" later\n\t\t\tactive.stopContext = ctx;\n\t\t\t// Abort the runner\n\t\t\tactive.runner.abort();\n\t\t} else {\n\t\t\tawait ctx.respond(\"_Nothing running._\");\n\t\t}\n\t\treturn;\n\t}\n\n\t// Check if already running in this channel\n\tif (activeRuns.has(channelId)) {\n\t\tawait ctx.respond(\"_Already working on something. Say `@mom stop` to cancel._\");\n\t\treturn;\n\t}\n\n\tlog.logUserMessage(logCtx, ctx.message.text);\n\tconst channelDir = join(workingDir, channelId);\n\n\tconst runner = createAgentRunner(sandbox);\n\tactiveRuns.set(channelId, { runner, context: ctx });\n\n\tawait ctx.setTyping(true);\n\tawait ctx.setWorking(true);\n\n\tconst result = await runner.run(ctx, channelDir, ctx.store);\n\n\t// Remove working indicator\n\tawait ctx.setWorking(false);\n\n\t// Handle different stop reasons\n\tconst active = activeRuns.get(channelId);\n\tif (result.stopReason === \"aborted\") {\n\t\t// Replace the STOP message with \"Stopped\"\n\t\tif (active?.stopContext) {\n\t\t\tawait active.stopContext.setWorking(false);\n\t\t\tawait active.stopContext.replaceMessage(\"_Stopped_\");\n\t\t}\n\t} else if (result.stopReason === \"error\") {\n\t\t// Agent encountered an error\n\t\tlog.logAgentError(logCtx, \"Agent stopped with error\");\n\t}\n\t// \"stop\", \"length\", \"toolUse\" are normal completions - nothing extra to do\n\n\tactiveRuns.delete(channelId);\n}\n\nconst bot = new MomBot(\n\t{\n\t\tasync onChannelMention(ctx) {\n\t\t\tawait handleMessage(ctx, \"channel\");\n\t\t},\n\n\t\tasync onDirectMessage(ctx) {\n\t\t\tawait handleMessage(ctx, \"dm\");\n\t\t},\n\t},\n\t{\n\t\tappToken: MOM_SLACK_APP_TOKEN,\n\t\tbotToken: MOM_SLACK_BOT_TOKEN,\n\t\tworkingDir,\n\t},\n);\n\nbot.start();\n"]}
package/dist/main.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { join, resolve } from "path";
3
3
  import { createAgentRunner } from "./agent.js";
4
+ import * as log from "./log.js";
4
5
  import { parseSandboxArg, validateSandbox } from "./sandbox.js";
5
6
  import { MomBot } from "./slack.js";
6
7
  const MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;
@@ -48,9 +49,7 @@ function parseArgs() {
48
49
  return { workingDir: resolve(workingDir), sandbox };
49
50
  }
50
51
  const { workingDir, sandbox } = parseArgs();
51
- console.log("Starting mom bot...");
52
- console.log(` Working directory: ${workingDir}`);
53
- console.log(` Sandbox: ${sandbox.type === "host" ? "host" : `docker:${sandbox.container}`}`);
52
+ log.logStartup(workingDir, sandbox.type === "host" ? "host" : `docker:${sandbox.container}`);
54
53
  if (!MOM_SLACK_APP_TOKEN || !MOM_SLACK_BOT_TOKEN || (!ANTHROPIC_API_KEY && !ANTHROPIC_OAUTH_TOKEN)) {
55
54
  console.error("Missing required environment variables:");
56
55
  if (!MOM_SLACK_APP_TOKEN)
@@ -65,16 +64,25 @@ if (!MOM_SLACK_APP_TOKEN || !MOM_SLACK_BOT_TOKEN || (!ANTHROPIC_API_KEY && !ANTH
65
64
  await validateSandbox(sandbox);
66
65
  // Track active agent runs per channel
67
66
  const activeRuns = new Map();
68
- async function handleMessage(ctx, source) {
67
+ async function handleMessage(ctx, _source) {
69
68
  const channelId = ctx.message.channel;
70
69
  const messageText = ctx.message.text.toLowerCase().trim();
70
+ const logCtx = {
71
+ channelId: ctx.message.channel,
72
+ userName: ctx.message.userName,
73
+ channelName: ctx.channelName,
74
+ };
71
75
  // Check for stop command
72
76
  if (messageText === "stop") {
73
- const runner = activeRuns.get(channelId);
74
- if (runner) {
75
- console.log(`Stop requested for channel ${channelId}`);
76
- runner.abort();
77
+ const active = activeRuns.get(channelId);
78
+ if (active) {
79
+ log.logStopRequest(logCtx);
80
+ // Post a NEW message saying "Stopping..."
77
81
  await ctx.respond("_Stopping..._");
82
+ // Store this context to update it to "Stopped" later
83
+ active.stopContext = ctx;
84
+ // Abort the runner
85
+ active.runner.abort();
78
86
  }
79
87
  else {
80
88
  await ctx.respond("_Nothing running._");
@@ -86,28 +94,30 @@ async function handleMessage(ctx, source) {
86
94
  await ctx.respond("_Already working on something. Say `@mom stop` to cancel._");
87
95
  return;
88
96
  }
89
- console.log(`${source === "channel" ? "Channel mention" : "DM"} from <@${ctx.message.user}>: ${ctx.message.text}`);
97
+ log.logUserMessage(logCtx, ctx.message.text);
90
98
  const channelDir = join(workingDir, channelId);
91
99
  const runner = createAgentRunner(sandbox);
92
- activeRuns.set(channelId, runner);
100
+ activeRuns.set(channelId, { runner, context: ctx });
93
101
  await ctx.setTyping(true);
94
- try {
95
- await runner.run(ctx, channelDir, ctx.store);
96
- }
97
- catch (error) {
98
- // Don't report abort errors
99
- const msg = error instanceof Error ? error.message : String(error);
100
- if (msg.includes("aborted") || msg.includes("Aborted")) {
101
- // Already said "Stopping..." - nothing more to say
102
- }
103
- else {
104
- console.error("Agent error:", error);
105
- await ctx.respond(`❌ Error: ${msg}`);
102
+ await ctx.setWorking(true);
103
+ const result = await runner.run(ctx, channelDir, ctx.store);
104
+ // Remove working indicator
105
+ await ctx.setWorking(false);
106
+ // Handle different stop reasons
107
+ const active = activeRuns.get(channelId);
108
+ if (result.stopReason === "aborted") {
109
+ // Replace the STOP message with "Stopped"
110
+ if (active?.stopContext) {
111
+ await active.stopContext.setWorking(false);
112
+ await active.stopContext.replaceMessage("_Stopped_");
106
113
  }
107
114
  }
108
- finally {
109
- activeRuns.delete(channelId);
115
+ else if (result.stopReason === "error") {
116
+ // Agent encountered an error
117
+ log.logAgentError(logCtx, "Agent stopped with error");
110
118
  }
119
+ // "stop", "length", "toolUse" are normal completions - nothing extra to do
120
+ activeRuns.delete(channelId);
111
121
  }
112
122
  const bot = new MomBot({
113
123
  async onChannelMention(ctx) {
package/dist/main.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n\nimport { join, resolve } from \"path\";\nimport { type AgentRunner, createAgentRunner } from \"./agent.js\";\nimport { parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { MomBot, type SlackContext } from \"./slack.js\";\n\nconst MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;\nconst MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;\nconst ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;\nconst ANTHROPIC_OAUTH_TOKEN = process.env.ANTHROPIC_OAUTH_TOKEN;\n\n// Parse command line arguments\nfunction parseArgs(): { workingDir: string; sandbox: SandboxConfig } {\n\tconst args = process.argv.slice(2);\n\tlet sandbox: SandboxConfig = { type: \"host\" };\n\tlet workingDir: string | undefined;\n\n\tfor (let i = 0; i < args.length; i++) {\n\t\tconst arg = args[i];\n\t\tif (arg.startsWith(\"--sandbox=\")) {\n\t\t\tsandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n\t\t} else if (arg === \"--sandbox\") {\n\t\t\tconst next = args[++i];\n\t\t\tif (!next) {\n\t\t\t\tconsole.error(\"Error: --sandbox requires a value (host or docker:<container-name>)\");\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t\tsandbox = parseSandboxArg(next);\n\t\t} else if (!arg.startsWith(\"-\")) {\n\t\t\tworkingDir = arg;\n\t\t} else {\n\t\t\tconsole.error(`Unknown option: ${arg}`);\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\tif (!workingDir) {\n\t\tconsole.error(\"Usage: mom [--sandbox=host|docker:<container-name>] <working-directory>\");\n\t\tconsole.error(\"\");\n\t\tconsole.error(\"Options:\");\n\t\tconsole.error(\" --sandbox=host Run tools directly on host (default)\");\n\t\tconsole.error(\" --sandbox=docker:<container> Run tools in Docker container\");\n\t\tconsole.error(\"\");\n\t\tconsole.error(\"Examples:\");\n\t\tconsole.error(\" mom ./data\");\n\t\tconsole.error(\" mom --sandbox=docker:mom-sandbox ./data\");\n\t\tprocess.exit(1);\n\t}\n\n\treturn { workingDir: resolve(workingDir), sandbox };\n}\n\nconst { workingDir, sandbox } = parseArgs();\n\nconsole.log(\"Starting mom bot...\");\nconsole.log(` Working directory: ${workingDir}`);\nconsole.log(` Sandbox: ${sandbox.type === \"host\" ? \"host\" : `docker:${sandbox.container}`}`);\n\nif (!MOM_SLACK_APP_TOKEN || !MOM_SLACK_BOT_TOKEN || (!ANTHROPIC_API_KEY && !ANTHROPIC_OAUTH_TOKEN)) {\n\tconsole.error(\"Missing required environment variables:\");\n\tif (!MOM_SLACK_APP_TOKEN) console.error(\" - MOM_SLACK_APP_TOKEN (xapp-...)\");\n\tif (!MOM_SLACK_BOT_TOKEN) console.error(\" - MOM_SLACK_BOT_TOKEN (xoxb-...)\");\n\tif (!ANTHROPIC_API_KEY && !ANTHROPIC_OAUTH_TOKEN) console.error(\" - ANTHROPIC_API_KEY or ANTHROPIC_OAUTH_TOKEN\");\n\tprocess.exit(1);\n}\n\n// Validate sandbox configuration\nawait validateSandbox(sandbox);\n\n// Track active agent runs per channel\nconst activeRuns = new Map<string, AgentRunner>();\n\nasync function handleMessage(ctx: SlackContext, source: \"channel\" | \"dm\"): Promise<void> {\n\tconst channelId = ctx.message.channel;\n\tconst messageText = ctx.message.text.toLowerCase().trim();\n\n\t// Check for stop command\n\tif (messageText === \"stop\") {\n\t\tconst runner = activeRuns.get(channelId);\n\t\tif (runner) {\n\t\t\tconsole.log(`Stop requested for channel ${channelId}`);\n\t\t\trunner.abort();\n\t\t\tawait ctx.respond(\"_Stopping..._\");\n\t\t} else {\n\t\t\tawait ctx.respond(\"_Nothing running._\");\n\t\t}\n\t\treturn;\n\t}\n\n\t// Check if already running in this channel\n\tif (activeRuns.has(channelId)) {\n\t\tawait ctx.respond(\"_Already working on something. Say `@mom stop` to cancel._\");\n\t\treturn;\n\t}\n\n\tconsole.log(`${source === \"channel\" ? \"Channel mention\" : \"DM\"} from <@${ctx.message.user}>: ${ctx.message.text}`);\n\tconst channelDir = join(workingDir, channelId);\n\n\tconst runner = createAgentRunner(sandbox);\n\tactiveRuns.set(channelId, runner);\n\n\tawait ctx.setTyping(true);\n\ttry {\n\t\tawait runner.run(ctx, channelDir, ctx.store);\n\t} catch (error) {\n\t\t// Don't report abort errors\n\t\tconst msg = error instanceof Error ? error.message : String(error);\n\t\tif (msg.includes(\"aborted\") || msg.includes(\"Aborted\")) {\n\t\t\t// Already said \"Stopping...\" - nothing more to say\n\t\t} else {\n\t\t\tconsole.error(\"Agent error:\", error);\n\t\t\tawait ctx.respond(`❌ Error: ${msg}`);\n\t\t}\n\t} finally {\n\t\tactiveRuns.delete(channelId);\n\t}\n}\n\nconst bot = new MomBot(\n\t{\n\t\tasync onChannelMention(ctx) {\n\t\t\tawait handleMessage(ctx, \"channel\");\n\t\t},\n\n\t\tasync onDirectMessage(ctx) {\n\t\t\tawait handleMessage(ctx, \"dm\");\n\t\t},\n\t},\n\t{\n\t\tappToken: MOM_SLACK_APP_TOKEN,\n\t\tbotToken: MOM_SLACK_BOT_TOKEN,\n\t\tworkingDir,\n\t},\n);\n\nbot.start();\n"]}
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n\nimport { join, resolve } from \"path\";\nimport { type AgentRunner, createAgentRunner } from \"./agent.js\";\nimport * as log from \"./log.js\";\nimport { parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { MomBot, type SlackContext } from \"./slack.js\";\n\nconst MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;\nconst MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;\nconst ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;\nconst ANTHROPIC_OAUTH_TOKEN = process.env.ANTHROPIC_OAUTH_TOKEN;\n\n// Parse command line arguments\nfunction parseArgs(): { workingDir: string; sandbox: SandboxConfig } {\n\tconst args = process.argv.slice(2);\n\tlet sandbox: SandboxConfig = { type: \"host\" };\n\tlet workingDir: string | undefined;\n\n\tfor (let i = 0; i < args.length; i++) {\n\t\tconst arg = args[i];\n\t\tif (arg.startsWith(\"--sandbox=\")) {\n\t\t\tsandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n\t\t} else if (arg === \"--sandbox\") {\n\t\t\tconst next = args[++i];\n\t\t\tif (!next) {\n\t\t\t\tconsole.error(\"Error: --sandbox requires a value (host or docker:<container-name>)\");\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t\tsandbox = parseSandboxArg(next);\n\t\t} else if (!arg.startsWith(\"-\")) {\n\t\t\tworkingDir = arg;\n\t\t} else {\n\t\t\tconsole.error(`Unknown option: ${arg}`);\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\tif (!workingDir) {\n\t\tconsole.error(\"Usage: mom [--sandbox=host|docker:<container-name>] <working-directory>\");\n\t\tconsole.error(\"\");\n\t\tconsole.error(\"Options:\");\n\t\tconsole.error(\" --sandbox=host Run tools directly on host (default)\");\n\t\tconsole.error(\" --sandbox=docker:<container> Run tools in Docker container\");\n\t\tconsole.error(\"\");\n\t\tconsole.error(\"Examples:\");\n\t\tconsole.error(\" mom ./data\");\n\t\tconsole.error(\" mom --sandbox=docker:mom-sandbox ./data\");\n\t\tprocess.exit(1);\n\t}\n\n\treturn { workingDir: resolve(workingDir), sandbox };\n}\n\nconst { workingDir, sandbox } = parseArgs();\n\nlog.logStartup(workingDir, sandbox.type === \"host\" ? \"host\" : `docker:${sandbox.container}`);\n\nif (!MOM_SLACK_APP_TOKEN || !MOM_SLACK_BOT_TOKEN || (!ANTHROPIC_API_KEY && !ANTHROPIC_OAUTH_TOKEN)) {\n\tconsole.error(\"Missing required environment variables:\");\n\tif (!MOM_SLACK_APP_TOKEN) console.error(\" - MOM_SLACK_APP_TOKEN (xapp-...)\");\n\tif (!MOM_SLACK_BOT_TOKEN) console.error(\" - MOM_SLACK_BOT_TOKEN (xoxb-...)\");\n\tif (!ANTHROPIC_API_KEY && !ANTHROPIC_OAUTH_TOKEN) console.error(\" - ANTHROPIC_API_KEY or ANTHROPIC_OAUTH_TOKEN\");\n\tprocess.exit(1);\n}\n\n// Validate sandbox configuration\nawait validateSandbox(sandbox);\n\n// Track active agent runs per channel\nconst activeRuns = new Map<string, { runner: AgentRunner; context: SlackContext; stopContext?: SlackContext }>();\n\nasync function handleMessage(ctx: SlackContext, _source: \"channel\" | \"dm\"): Promise<void> {\n\tconst channelId = ctx.message.channel;\n\tconst messageText = ctx.message.text.toLowerCase().trim();\n\n\tconst logCtx = {\n\t\tchannelId: ctx.message.channel,\n\t\tuserName: ctx.message.userName,\n\t\tchannelName: ctx.channelName,\n\t};\n\n\t// Check for stop command\n\tif (messageText === \"stop\") {\n\t\tconst active = activeRuns.get(channelId);\n\t\tif (active) {\n\t\t\tlog.logStopRequest(logCtx);\n\t\t\t// Post a NEW message saying \"Stopping...\"\n\t\t\tawait ctx.respond(\"_Stopping..._\");\n\t\t\t// Store this context to update it to \"Stopped\" later\n\t\t\tactive.stopContext = ctx;\n\t\t\t// Abort the runner\n\t\t\tactive.runner.abort();\n\t\t} else {\n\t\t\tawait ctx.respond(\"_Nothing running._\");\n\t\t}\n\t\treturn;\n\t}\n\n\t// Check if already running in this channel\n\tif (activeRuns.has(channelId)) {\n\t\tawait ctx.respond(\"_Already working on something. Say `@mom stop` to cancel._\");\n\t\treturn;\n\t}\n\n\tlog.logUserMessage(logCtx, ctx.message.text);\n\tconst channelDir = join(workingDir, channelId);\n\n\tconst runner = createAgentRunner(sandbox);\n\tactiveRuns.set(channelId, { runner, context: ctx });\n\n\tawait ctx.setTyping(true);\n\tawait ctx.setWorking(true);\n\n\tconst result = await runner.run(ctx, channelDir, ctx.store);\n\n\t// Remove working indicator\n\tawait ctx.setWorking(false);\n\n\t// Handle different stop reasons\n\tconst active = activeRuns.get(channelId);\n\tif (result.stopReason === \"aborted\") {\n\t\t// Replace the STOP message with \"Stopped\"\n\t\tif (active?.stopContext) {\n\t\t\tawait active.stopContext.setWorking(false);\n\t\t\tawait active.stopContext.replaceMessage(\"_Stopped_\");\n\t\t}\n\t} else if (result.stopReason === \"error\") {\n\t\t// Agent encountered an error\n\t\tlog.logAgentError(logCtx, \"Agent stopped with error\");\n\t}\n\t// \"stop\", \"length\", \"toolUse\" are normal completions - nothing extra to do\n\n\tactiveRuns.delete(channelId);\n}\n\nconst bot = new MomBot(\n\t{\n\t\tasync onChannelMention(ctx) {\n\t\t\tawait handleMessage(ctx, \"channel\");\n\t\t},\n\n\t\tasync onDirectMessage(ctx) {\n\t\t\tawait handleMessage(ctx, \"dm\");\n\t\t},\n\t},\n\t{\n\t\tappToken: MOM_SLACK_APP_TOKEN,\n\t\tbotToken: MOM_SLACK_BOT_TOKEN,\n\t\tworkingDir,\n\t},\n);\n\nbot.start();\n"]}
package/dist/slack.d.ts CHANGED
@@ -3,21 +3,27 @@ export interface SlackMessage {
3
3
  text: string;
4
4
  rawText: string;
5
5
  user: string;
6
+ userName?: string;
6
7
  channel: string;
7
8
  ts: string;
8
9
  attachments: Attachment[];
9
10
  }
10
11
  export interface SlackContext {
11
12
  message: SlackMessage;
13
+ channelName?: string;
12
14
  store: ChannelStore;
13
15
  /** Send/update the main message (accumulates text) */
14
16
  respond(text: string): Promise<void>;
17
+ /** Replace the entire message text (not append) */
18
+ replaceMessage(text: string): Promise<void>;
15
19
  /** Post a message in the thread under the main message (for verbose details) */
16
20
  respondInThread(text: string): Promise<void>;
17
21
  /** Show/hide typing indicator */
18
22
  setTyping(isTyping: boolean): Promise<void>;
19
23
  /** Upload a file to the channel */
20
24
  uploadFile(filePath: string, title?: string): Promise<void>;
25
+ /** Set working state (adds/removes working indicator emoji) */
26
+ setWorking(working: boolean): Promise<void>;
21
27
  }
22
28
  export interface MomHandler {
23
29
  onChannelMention(ctx: SlackContext): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"slack.d.ts","sourceRoot":"","sources":["../src/slack.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE3D,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,UAAU,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC5B,OAAO,EAAE,YAAY,CAAC;IACtB,KAAK,EAAE,YAAY,CAAC;IACpB,sDAAsD;IACtD,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,gFAAgF;IAChF,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,iCAAiC;IACjC,SAAS,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,mCAAmC;IACnC,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,UAAU;IAC1B,gBAAgB,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,eAAe,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,MAAM;IAClB,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,SAAS,CAAuB;IACxC,SAAgB,KAAK,EAAE,YAAY,CAAC;IACpC,OAAO,CAAC,SAAS,CAAqE;IAEtF,YAAY,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAUpD;YAEa,WAAW;IAmBzB,OAAO,CAAC,kBAAkB;YAqEZ,UAAU;IAqBxB,OAAO,CAAC,aAAa;IAyGf,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAK3B;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAG1B;CACD","sourcesContent":["import { SocketModeClient } from \"@slack/socket-mode\";\nimport { WebClient } from \"@slack/web-api\";\nimport { readFileSync } from \"fs\";\nimport { basename } from \"path\";\nimport { type Attachment, ChannelStore } from \"./store.js\";\n\nexport interface SlackMessage {\n\ttext: string; // message content (mentions stripped)\n\trawText: string; // original text with mentions\n\tuser: string; // user ID\n\tchannel: string; // channel ID\n\tts: string; // timestamp (for threading)\n\tattachments: Attachment[]; // file attachments\n}\n\nexport interface SlackContext {\n\tmessage: SlackMessage;\n\tstore: ChannelStore;\n\t/** Send/update the main message (accumulates text) */\n\trespond(text: string): Promise<void>;\n\t/** Post a message in the thread under the main message (for verbose details) */\n\trespondInThread(text: string): Promise<void>;\n\t/** Show/hide typing indicator */\n\tsetTyping(isTyping: boolean): Promise<void>;\n\t/** Upload a file to the channel */\n\tuploadFile(filePath: string, title?: string): Promise<void>;\n}\n\nexport interface MomHandler {\n\tonChannelMention(ctx: SlackContext): Promise<void>;\n\tonDirectMessage(ctx: SlackContext): Promise<void>;\n}\n\nexport interface MomBotConfig {\n\tappToken: string;\n\tbotToken: string;\n\tworkingDir: string; // directory for channel data and attachments\n}\n\nexport class MomBot {\n\tprivate socketClient: SocketModeClient;\n\tprivate webClient: WebClient;\n\tprivate handler: MomHandler;\n\tprivate botUserId: string | null = null;\n\tpublic readonly store: ChannelStore;\n\tprivate userCache: Map<string, { userName: string; displayName: string }> = new Map();\n\n\tconstructor(handler: MomHandler, config: MomBotConfig) {\n\t\tthis.handler = handler;\n\t\tthis.socketClient = new SocketModeClient({ appToken: config.appToken });\n\t\tthis.webClient = new WebClient(config.botToken);\n\t\tthis.store = new ChannelStore({\n\t\t\tworkingDir: config.workingDir,\n\t\t\tbotToken: config.botToken,\n\t\t});\n\n\t\tthis.setupEventHandlers();\n\t}\n\n\tprivate async getUserInfo(userId: string): Promise<{ userName: string; displayName: string }> {\n\t\tif (this.userCache.has(userId)) {\n\t\t\treturn this.userCache.get(userId)!;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await this.webClient.users.info({ user: userId });\n\t\t\tconst user = result.user as { name?: string; real_name?: string };\n\t\t\tconst info = {\n\t\t\t\tuserName: user?.name || userId,\n\t\t\t\tdisplayName: user?.real_name || user?.name || userId,\n\t\t\t};\n\t\t\tthis.userCache.set(userId, info);\n\t\t\treturn info;\n\t\t} catch {\n\t\t\treturn { userName: userId, displayName: userId };\n\t\t}\n\t}\n\n\tprivate setupEventHandlers(): void {\n\t\t// Handle @mentions in channels\n\t\tthis.socketClient.on(\"app_mention\", async ({ event, ack }) => {\n\t\t\tawait ack();\n\n\t\t\tconst slackEvent = event as {\n\t\t\t\ttext: string;\n\t\t\t\tchannel: string;\n\t\t\t\tuser: string;\n\t\t\t\tts: string;\n\t\t\t\tfiles?: Array<{ name: string; url_private_download?: string; url_private?: string }>;\n\t\t\t};\n\n\t\t\t// Log the mention (message event may not fire for app_mention)\n\t\t\tawait this.logMessage(slackEvent);\n\n\t\t\tconst ctx = this.createContext(slackEvent);\n\t\t\tawait this.handler.onChannelMention(ctx);\n\t\t});\n\n\t\t// Handle all messages (for logging) and DMs (for triggering handler)\n\t\tthis.socketClient.on(\"message\", async ({ event, ack }) => {\n\t\t\tawait ack();\n\n\t\t\tconst slackEvent = event as {\n\t\t\t\ttext?: string;\n\t\t\t\tchannel: string;\n\t\t\t\tuser?: string;\n\t\t\t\tts: string;\n\t\t\t\tchannel_type?: string;\n\t\t\t\tsubtype?: string;\n\t\t\t\tbot_id?: string;\n\t\t\t\tfiles?: Array<{ name: string; url_private_download?: string; url_private?: string }>;\n\t\t\t};\n\n\t\t\t// Ignore bot messages\n\t\t\tif (slackEvent.bot_id) return;\n\t\t\t// Ignore message edits, etc. (but allow file_share)\n\t\t\tif (slackEvent.subtype !== undefined && slackEvent.subtype !== \"file_share\") return;\n\t\t\t// Ignore if no user\n\t\t\tif (!slackEvent.user) return;\n\t\t\t// Ignore messages from the bot itself\n\t\t\tif (slackEvent.user === this.botUserId) return;\n\t\t\t// Ignore if no text AND no files\n\t\t\tif (!slackEvent.text && (!slackEvent.files || slackEvent.files.length === 0)) return;\n\n\t\t\t// Log ALL messages (channel and DM)\n\t\t\tawait this.logMessage({\n\t\t\t\ttext: slackEvent.text || \"\",\n\t\t\t\tchannel: slackEvent.channel,\n\t\t\t\tuser: slackEvent.user,\n\t\t\t\tts: slackEvent.ts,\n\t\t\t\tfiles: slackEvent.files,\n\t\t\t});\n\n\t\t\t// Only trigger handler for DMs (channel mentions are handled by app_mention event)\n\t\t\tif (slackEvent.channel_type === \"im\") {\n\t\t\t\tconst ctx = this.createContext({\n\t\t\t\t\ttext: slackEvent.text || \"\",\n\t\t\t\t\tchannel: slackEvent.channel,\n\t\t\t\t\tuser: slackEvent.user,\n\t\t\t\t\tts: slackEvent.ts,\n\t\t\t\t\tfiles: slackEvent.files,\n\t\t\t\t});\n\t\t\t\tawait this.handler.onDirectMessage(ctx);\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate async logMessage(event: {\n\t\ttext: string;\n\t\tchannel: string;\n\t\tuser: string;\n\t\tts: string;\n\t\tfiles?: Array<{ name: string; url_private_download?: string; url_private?: string }>;\n\t}): Promise<void> {\n\t\tconst attachments = event.files ? this.store.processAttachments(event.channel, event.files, event.ts) : [];\n\t\tconst { userName, displayName } = await this.getUserInfo(event.user);\n\n\t\tawait this.store.logMessage(event.channel, {\n\t\t\tts: event.ts,\n\t\t\tuser: event.user,\n\t\t\tuserName,\n\t\t\tdisplayName,\n\t\t\ttext: event.text,\n\t\t\tattachments,\n\t\t\tisBot: false,\n\t\t});\n\t}\n\n\tprivate createContext(event: {\n\t\ttext: string;\n\t\tchannel: string;\n\t\tuser: string;\n\t\tts: string;\n\t\tfiles?: Array<{ name: string; url_private_download?: string; url_private?: string }>;\n\t}): SlackContext {\n\t\tconst rawText = event.text;\n\t\tconst text = rawText.replace(/<@[A-Z0-9]+>/gi, \"\").trim();\n\n\t\t// Process attachments (for context, already logged by message handler)\n\t\tconst attachments = event.files ? this.store.processAttachments(event.channel, event.files, event.ts) : [];\n\n\t\t// Track the single message for this run\n\t\tlet messageTs: string | null = null;\n\t\tlet accumulatedText = \"\";\n\t\tlet isThinking = true; // Track if we're still in \"thinking\" state\n\t\tlet updatePromise: Promise<void> = Promise.resolve();\n\n\t\treturn {\n\t\t\tmessage: {\n\t\t\t\ttext,\n\t\t\t\trawText,\n\t\t\t\tuser: event.user,\n\t\t\t\tchannel: event.channel,\n\t\t\t\tts: event.ts,\n\t\t\t\tattachments,\n\t\t\t},\n\t\t\tstore: this.store,\n\t\t\trespond: async (responseText: string) => {\n\t\t\t\t// Queue updates to avoid race conditions\n\t\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\t\tif (isThinking) {\n\t\t\t\t\t\t// First real response replaces \"Thinking...\"\n\t\t\t\t\t\taccumulatedText = responseText;\n\t\t\t\t\t\tisThinking = false;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Subsequent responses get appended\n\t\t\t\t\t\taccumulatedText += \"\\n\" + responseText;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (messageTs) {\n\t\t\t\t\t\t// Update existing message\n\t\t\t\t\t\tawait this.webClient.chat.update({\n\t\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\t\tts: messageTs,\n\t\t\t\t\t\t\ttext: accumulatedText,\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Post initial message\n\t\t\t\t\t\tconst result = await this.webClient.chat.postMessage({\n\t\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\t\ttext: accumulatedText,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tmessageTs = result.ts as string;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Log the response\n\t\t\t\t\tawait this.store.logBotResponse(event.channel, responseText, messageTs!);\n\t\t\t\t});\n\n\t\t\t\tawait updatePromise;\n\t\t\t},\n\t\t\trespondInThread: async (threadText: string) => {\n\t\t\t\t// Queue thread posts to maintain order\n\t\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\t\tif (!messageTs) {\n\t\t\t\t\t\t// No main message yet, just skip\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\t// Post in thread under the main message\n\t\t\t\t\tawait this.webClient.chat.postMessage({\n\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\tthread_ts: messageTs,\n\t\t\t\t\t\ttext: threadText,\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t\tawait updatePromise;\n\t\t\t},\n\t\t\tsetTyping: async (isTyping: boolean) => {\n\t\t\t\tif (isTyping && !messageTs) {\n\t\t\t\t\t// Post initial \"thinking\" message\n\t\t\t\t\taccumulatedText = \"_Thinking..._\";\n\t\t\t\t\tconst result = await this.webClient.chat.postMessage({\n\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\ttext: accumulatedText,\n\t\t\t\t\t});\n\t\t\t\t\tmessageTs = result.ts as string;\n\t\t\t\t}\n\t\t\t\t// We don't delete/clear anymore - message persists and gets updated\n\t\t\t},\n\t\t\tuploadFile: async (filePath: string, title?: string) => {\n\t\t\t\tconst fileName = title || basename(filePath);\n\t\t\t\tconst fileContent = readFileSync(filePath);\n\n\t\t\t\tawait this.webClient.files.uploadV2({\n\t\t\t\t\tchannel_id: event.channel,\n\t\t\t\t\tfile: fileContent,\n\t\t\t\t\tfilename: fileName,\n\t\t\t\t\ttitle: fileName,\n\t\t\t\t});\n\t\t\t},\n\t\t};\n\t}\n\n\tasync start(): Promise<void> {\n\t\tconst auth = await this.webClient.auth.test();\n\t\tthis.botUserId = auth.user_id as string;\n\t\tawait this.socketClient.start();\n\t\tconsole.log(\"⚡️ Mom bot connected and listening!\");\n\t}\n\n\tasync stop(): Promise<void> {\n\t\tawait this.socketClient.disconnect();\n\t\tconsole.log(\"Mom bot disconnected.\");\n\t}\n}\n"]}
1
+ {"version":3,"file":"slack.d.ts","sourceRoot":"","sources":["../src/slack.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE3D,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,UAAU,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC5B,OAAO,EAAE,YAAY,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,YAAY,CAAC;IACpB,sDAAsD;IACtD,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,mDAAmD;IACnD,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,gFAAgF;IAChF,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,iCAAiC;IACjC,SAAS,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,mCAAmC;IACnC,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,+DAA+D;IAC/D,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,UAAU;IAC1B,gBAAgB,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,eAAe,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,MAAM;IAClB,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,SAAS,CAAuB;IACxC,SAAgB,KAAK,EAAE,YAAY,CAAC;IACpC,OAAO,CAAC,SAAS,CAAqE;IAEtF,YAAY,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAUpD;YAEa,WAAW;IAmBzB,OAAO,CAAC,kBAAkB;YAqEZ,UAAU;YAsBV,aAAa;IAsKrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAK3B;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAG1B;CACD","sourcesContent":["import { SocketModeClient } from \"@slack/socket-mode\";\nimport { WebClient } from \"@slack/web-api\";\nimport { readFileSync } from \"fs\";\nimport { basename } from \"path\";\nimport * as log from \"./log.js\";\nimport { type Attachment, ChannelStore } from \"./store.js\";\n\nexport interface SlackMessage {\n\ttext: string; // message content (mentions stripped)\n\trawText: string; // original text with mentions\n\tuser: string; // user ID\n\tuserName?: string; // user handle\n\tchannel: string; // channel ID\n\tts: string; // timestamp (for threading)\n\tattachments: Attachment[]; // file attachments\n}\n\nexport interface SlackContext {\n\tmessage: SlackMessage;\n\tchannelName?: string; // channel name for logging (e.g., #dev-team)\n\tstore: ChannelStore;\n\t/** Send/update the main message (accumulates text) */\n\trespond(text: string): Promise<void>;\n\t/** Replace the entire message text (not append) */\n\treplaceMessage(text: string): Promise<void>;\n\t/** Post a message in the thread under the main message (for verbose details) */\n\trespondInThread(text: string): Promise<void>;\n\t/** Show/hide typing indicator */\n\tsetTyping(isTyping: boolean): Promise<void>;\n\t/** Upload a file to the channel */\n\tuploadFile(filePath: string, title?: string): Promise<void>;\n\t/** Set working state (adds/removes working indicator emoji) */\n\tsetWorking(working: boolean): Promise<void>;\n}\n\nexport interface MomHandler {\n\tonChannelMention(ctx: SlackContext): Promise<void>;\n\tonDirectMessage(ctx: SlackContext): Promise<void>;\n}\n\nexport interface MomBotConfig {\n\tappToken: string;\n\tbotToken: string;\n\tworkingDir: string; // directory for channel data and attachments\n}\n\nexport class MomBot {\n\tprivate socketClient: SocketModeClient;\n\tprivate webClient: WebClient;\n\tprivate handler: MomHandler;\n\tprivate botUserId: string | null = null;\n\tpublic readonly store: ChannelStore;\n\tprivate userCache: Map<string, { userName: string; displayName: string }> = new Map();\n\n\tconstructor(handler: MomHandler, config: MomBotConfig) {\n\t\tthis.handler = handler;\n\t\tthis.socketClient = new SocketModeClient({ appToken: config.appToken });\n\t\tthis.webClient = new WebClient(config.botToken);\n\t\tthis.store = new ChannelStore({\n\t\t\tworkingDir: config.workingDir,\n\t\t\tbotToken: config.botToken,\n\t\t});\n\n\t\tthis.setupEventHandlers();\n\t}\n\n\tprivate async getUserInfo(userId: string): Promise<{ userName: string; displayName: string }> {\n\t\tif (this.userCache.has(userId)) {\n\t\t\treturn this.userCache.get(userId)!;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await this.webClient.users.info({ user: userId });\n\t\t\tconst user = result.user as { name?: string; real_name?: string };\n\t\t\tconst info = {\n\t\t\t\tuserName: user?.name || userId,\n\t\t\t\tdisplayName: user?.real_name || user?.name || userId,\n\t\t\t};\n\t\t\tthis.userCache.set(userId, info);\n\t\t\treturn info;\n\t\t} catch {\n\t\t\treturn { userName: userId, displayName: userId };\n\t\t}\n\t}\n\n\tprivate setupEventHandlers(): void {\n\t\t// Handle @mentions in channels\n\t\tthis.socketClient.on(\"app_mention\", async ({ event, ack }) => {\n\t\t\tawait ack();\n\n\t\t\tconst slackEvent = event as {\n\t\t\t\ttext: string;\n\t\t\t\tchannel: string;\n\t\t\t\tuser: string;\n\t\t\t\tts: string;\n\t\t\t\tfiles?: Array<{ name: string; url_private_download?: string; url_private?: string }>;\n\t\t\t};\n\n\t\t\t// Log the mention (message event may not fire for app_mention)\n\t\t\tawait this.logMessage(slackEvent);\n\n\t\t\tconst ctx = await this.createContext(slackEvent);\n\t\t\tawait this.handler.onChannelMention(ctx);\n\t\t});\n\n\t\t// Handle all messages (for logging) and DMs (for triggering handler)\n\t\tthis.socketClient.on(\"message\", async ({ event, ack }) => {\n\t\t\tawait ack();\n\n\t\t\tconst slackEvent = event as {\n\t\t\t\ttext?: string;\n\t\t\t\tchannel: string;\n\t\t\t\tuser?: string;\n\t\t\t\tts: string;\n\t\t\t\tchannel_type?: string;\n\t\t\t\tsubtype?: string;\n\t\t\t\tbot_id?: string;\n\t\t\t\tfiles?: Array<{ name: string; url_private_download?: string; url_private?: string }>;\n\t\t\t};\n\n\t\t\t// Ignore bot messages\n\t\t\tif (slackEvent.bot_id) return;\n\t\t\t// Ignore message edits, etc. (but allow file_share)\n\t\t\tif (slackEvent.subtype !== undefined && slackEvent.subtype !== \"file_share\") return;\n\t\t\t// Ignore if no user\n\t\t\tif (!slackEvent.user) return;\n\t\t\t// Ignore messages from the bot itself\n\t\t\tif (slackEvent.user === this.botUserId) return;\n\t\t\t// Ignore if no text AND no files\n\t\t\tif (!slackEvent.text && (!slackEvent.files || slackEvent.files.length === 0)) return;\n\n\t\t\t// Log ALL messages (channel and DM)\n\t\t\tawait this.logMessage({\n\t\t\t\ttext: slackEvent.text || \"\",\n\t\t\t\tchannel: slackEvent.channel,\n\t\t\t\tuser: slackEvent.user,\n\t\t\t\tts: slackEvent.ts,\n\t\t\t\tfiles: slackEvent.files,\n\t\t\t});\n\n\t\t\t// Only trigger handler for DMs (channel mentions are handled by app_mention event)\n\t\t\tif (slackEvent.channel_type === \"im\") {\n\t\t\t\tconst ctx = await this.createContext({\n\t\t\t\t\ttext: slackEvent.text || \"\",\n\t\t\t\t\tchannel: slackEvent.channel,\n\t\t\t\t\tuser: slackEvent.user,\n\t\t\t\t\tts: slackEvent.ts,\n\t\t\t\t\tfiles: slackEvent.files,\n\t\t\t\t});\n\t\t\t\tawait this.handler.onDirectMessage(ctx);\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate async logMessage(event: {\n\t\ttext: string;\n\t\tchannel: string;\n\t\tuser: string;\n\t\tts: string;\n\t\tfiles?: Array<{ name: string; url_private_download?: string; url_private?: string }>;\n\t}): Promise<void> {\n\t\tconst attachments = event.files ? this.store.processAttachments(event.channel, event.files, event.ts) : [];\n\t\tconst { userName, displayName } = await this.getUserInfo(event.user);\n\n\t\tawait this.store.logMessage(event.channel, {\n\t\t\tdate: new Date(parseFloat(event.ts) * 1000).toISOString(),\n\t\t\tts: event.ts,\n\t\t\tuser: event.user,\n\t\t\tuserName,\n\t\t\tdisplayName,\n\t\t\ttext: event.text,\n\t\t\tattachments,\n\t\t\tisBot: false,\n\t\t});\n\t}\n\n\tprivate async createContext(event: {\n\t\ttext: string;\n\t\tchannel: string;\n\t\tuser: string;\n\t\tts: string;\n\t\tfiles?: Array<{ name: string; url_private_download?: string; url_private?: string }>;\n\t}): Promise<SlackContext> {\n\t\tconst rawText = event.text;\n\t\tconst text = rawText.replace(/<@[A-Z0-9]+>/gi, \"\").trim();\n\n\t\t// Get user info for logging\n\t\tconst { userName } = await this.getUserInfo(event.user);\n\n\t\t// Get channel name for logging (best effort)\n\t\tlet channelName: string | undefined;\n\t\ttry {\n\t\t\tif (event.channel.startsWith(\"C\")) {\n\t\t\t\tconst result = await this.webClient.conversations.info({ channel: event.channel });\n\t\t\t\tchannelName = result.channel?.name ? `#${result.channel.name}` : undefined;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore errors - we'll just use the channel ID\n\t\t}\n\n\t\t// Process attachments (for context, already logged by message handler)\n\t\tconst attachments = event.files ? this.store.processAttachments(event.channel, event.files, event.ts) : [];\n\n\t\t// Track the single message for this run\n\t\tlet messageTs: string | null = null;\n\t\tlet accumulatedText = \"\";\n\t\tlet isThinking = true; // Track if we're still in \"thinking\" state\n\t\tlet isWorking = true; // Track if still processing\n\t\tconst workingIndicator = \" ...\";\n\t\tlet updatePromise: Promise<void> = Promise.resolve();\n\n\t\treturn {\n\t\t\tmessage: {\n\t\t\t\ttext,\n\t\t\t\trawText,\n\t\t\t\tuser: event.user,\n\t\t\t\tuserName,\n\t\t\t\tchannel: event.channel,\n\t\t\t\tts: event.ts,\n\t\t\t\tattachments,\n\t\t\t},\n\t\t\tchannelName,\n\t\t\tstore: this.store,\n\t\t\trespond: async (responseText: string) => {\n\t\t\t\t// Queue updates to avoid race conditions\n\t\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\t\tif (isThinking) {\n\t\t\t\t\t\t// First real response replaces \"Thinking...\"\n\t\t\t\t\t\taccumulatedText = responseText;\n\t\t\t\t\t\tisThinking = false;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Subsequent responses get appended\n\t\t\t\t\t\taccumulatedText += \"\\n\" + responseText;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Add working indicator if still working\n\t\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\n\t\t\t\t\tif (messageTs) {\n\t\t\t\t\t\t// Update existing message\n\t\t\t\t\t\tawait this.webClient.chat.update({\n\t\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\t\tts: messageTs,\n\t\t\t\t\t\t\ttext: displayText,\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Post initial message\n\t\t\t\t\t\tconst result = await this.webClient.chat.postMessage({\n\t\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\t\ttext: displayText,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tmessageTs = result.ts as string;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Log the response\n\t\t\t\t\tawait this.store.logBotResponse(event.channel, responseText, messageTs!);\n\t\t\t\t});\n\n\t\t\t\tawait updatePromise;\n\t\t\t},\n\t\t\trespondInThread: async (threadText: string) => {\n\t\t\t\t// Queue thread posts to maintain order\n\t\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\t\tif (!messageTs) {\n\t\t\t\t\t\t// No main message yet, just skip\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\t// Post in thread under the main message\n\t\t\t\t\tawait this.webClient.chat.postMessage({\n\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\tthread_ts: messageTs,\n\t\t\t\t\t\ttext: threadText,\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t\tawait updatePromise;\n\t\t\t},\n\t\t\tsetTyping: async (isTyping: boolean) => {\n\t\t\t\tif (isTyping && !messageTs) {\n\t\t\t\t\t// Post initial \"thinking\" message (... auto-appended by working indicator)\n\t\t\t\t\taccumulatedText = \"_Thinking_\";\n\t\t\t\t\tconst result = await this.webClient.chat.postMessage({\n\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\ttext: accumulatedText,\n\t\t\t\t\t});\n\t\t\t\t\tmessageTs = result.ts as string;\n\t\t\t\t}\n\t\t\t\t// We don't delete/clear anymore - message persists and gets updated\n\t\t\t},\n\t\t\tuploadFile: async (filePath: string, title?: string) => {\n\t\t\t\tconst fileName = title || basename(filePath);\n\t\t\t\tconst fileContent = readFileSync(filePath);\n\n\t\t\t\tawait this.webClient.files.uploadV2({\n\t\t\t\t\tchannel_id: event.channel,\n\t\t\t\t\tfile: fileContent,\n\t\t\t\t\tfilename: fileName,\n\t\t\t\t\ttitle: fileName,\n\t\t\t\t});\n\t\t\t},\n\t\t\treplaceMessage: async (text: string) => {\n\t\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\t\t// Replace the accumulated text entirely\n\t\t\t\t\taccumulatedText = text;\n\n\t\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\n\t\t\t\t\tif (messageTs) {\n\t\t\t\t\t\tawait this.webClient.chat.update({\n\t\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\t\tts: messageTs,\n\t\t\t\t\t\t\ttext: displayText,\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Post initial message\n\t\t\t\t\t\tconst result = await this.webClient.chat.postMessage({\n\t\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\t\ttext: displayText,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tmessageTs = result.ts as string;\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tawait updatePromise;\n\t\t\t},\n\t\t\tsetWorking: async (working: boolean) => {\n\t\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\t\tisWorking = working;\n\n\t\t\t\t\t// If we have a message, update it to add/remove indicator\n\t\t\t\t\tif (messageTs) {\n\t\t\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\t\t\t\t\t\tawait this.webClient.chat.update({\n\t\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\t\tts: messageTs,\n\t\t\t\t\t\t\ttext: displayText,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tawait updatePromise;\n\t\t\t},\n\t\t};\n\t}\n\n\tasync start(): Promise<void> {\n\t\tconst auth = await this.webClient.auth.test();\n\t\tthis.botUserId = auth.user_id as string;\n\t\tawait this.socketClient.start();\n\t\tlog.logConnected();\n\t}\n\n\tasync stop(): Promise<void> {\n\t\tawait this.socketClient.disconnect();\n\t\tlog.logDisconnected();\n\t}\n}\n"]}