@mariozechner/pi-mom 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +183 -0
  3. package/dist/agent.d.ts +9 -0
  4. package/dist/agent.d.ts.map +1 -0
  5. package/dist/agent.js +248 -0
  6. package/dist/agent.js.map +1 -0
  7. package/dist/main.d.ts +3 -0
  8. package/dist/main.d.ts.map +1 -0
  9. package/dist/main.js +125 -0
  10. package/dist/main.js.map +1 -0
  11. package/dist/sandbox.d.ts +34 -0
  12. package/dist/sandbox.d.ts.map +1 -0
  13. package/dist/sandbox.js +183 -0
  14. package/dist/sandbox.js.map +1 -0
  15. package/dist/slack.d.ts +46 -0
  16. package/dist/slack.d.ts.map +1 -0
  17. package/dist/slack.js +208 -0
  18. package/dist/slack.js.map +1 -0
  19. package/dist/store.d.ts +52 -0
  20. package/dist/store.d.ts.map +1 -0
  21. package/dist/store.js +124 -0
  22. package/dist/store.js.map +1 -0
  23. package/dist/tools/attach.d.ts +10 -0
  24. package/dist/tools/attach.d.ts.map +1 -0
  25. package/dist/tools/attach.js +34 -0
  26. package/dist/tools/attach.js.map +1 -0
  27. package/dist/tools/bash.d.ts +10 -0
  28. package/dist/tools/bash.d.ts.map +1 -0
  29. package/dist/tools/bash.js +30 -0
  30. package/dist/tools/bash.js.map +1 -0
  31. package/dist/tools/edit.d.ts +11 -0
  32. package/dist/tools/edit.d.ts.map +1 -0
  33. package/dist/tools/edit.js +131 -0
  34. package/dist/tools/edit.js.map +1 -0
  35. package/dist/tools/index.d.ts +5 -0
  36. package/dist/tools/index.d.ts.map +1 -0
  37. package/dist/tools/index.js +16 -0
  38. package/dist/tools/index.js.map +1 -0
  39. package/dist/tools/read.d.ts +11 -0
  40. package/dist/tools/read.d.ts.map +1 -0
  41. package/dist/tools/read.js +102 -0
  42. package/dist/tools/read.js.map +1 -0
  43. package/dist/tools/write.d.ts +10 -0
  44. package/dist/tools/write.d.ts.map +1 -0
  45. package/dist/tools/write.js +33 -0
  46. package/dist/tools/write.js.map +1 -0
  47. package/package.json +52 -0
@@ -0,0 +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"]}
@@ -0,0 +1,34 @@
1
+ export type SandboxConfig = {
2
+ type: "host";
3
+ } | {
4
+ type: "docker";
5
+ container: string;
6
+ };
7
+ export declare function parseSandboxArg(value: string): SandboxConfig;
8
+ export declare function validateSandbox(config: SandboxConfig): Promise<void>;
9
+ /**
10
+ * Create an executor that runs commands either on host or in Docker container
11
+ */
12
+ export declare function createExecutor(config: SandboxConfig): Executor;
13
+ export interface Executor {
14
+ /**
15
+ * Execute a bash command
16
+ */
17
+ exec(command: string, options?: ExecOptions): Promise<ExecResult>;
18
+ /**
19
+ * Get the workspace path prefix for this executor
20
+ * Host: returns the actual path
21
+ * Docker: returns /workspace
22
+ */
23
+ getWorkspacePath(hostPath: string): string;
24
+ }
25
+ export interface ExecOptions {
26
+ timeout?: number;
27
+ signal?: AbortSignal;
28
+ }
29
+ export interface ExecResult {
30
+ stdout: string;
31
+ stderr: string;
32
+ code: number;
33
+ }
34
+ //# sourceMappingURL=sandbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,aAAa,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAErF,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CAc5D;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA4B1E;AAoBD;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,GAAG,QAAQ,CAK9D;AAED,MAAM,WAAW,QAAQ;IACxB;;OAEG;IACH,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAElE;;;;OAIG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3C;AAED,MAAM,WAAW,WAAW;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACb","sourcesContent":["import { spawn } from \"child_process\";\n\nexport type SandboxConfig = { type: \"host\" } | { type: \"docker\"; container: string };\n\nexport function parseSandboxArg(value: string): SandboxConfig {\n\tif (value === \"host\") {\n\t\treturn { type: \"host\" };\n\t}\n\tif (value.startsWith(\"docker:\")) {\n\t\tconst container = value.slice(\"docker:\".length);\n\t\tif (!container) {\n\t\t\tconsole.error(\"Error: docker sandbox requires container name (e.g., docker:mom-sandbox)\");\n\t\t\tprocess.exit(1);\n\t\t}\n\t\treturn { type: \"docker\", container };\n\t}\n\tconsole.error(`Error: Invalid sandbox type '${value}'. Use 'host' or 'docker:<container-name>'`);\n\tprocess.exit(1);\n}\n\nexport async function validateSandbox(config: SandboxConfig): Promise<void> {\n\tif (config.type === \"host\") {\n\t\treturn;\n\t}\n\n\t// Check if Docker is available\n\ttry {\n\t\tawait execSimple(\"docker\", [\"--version\"]);\n\t} catch {\n\t\tconsole.error(\"Error: Docker is not installed or not in PATH\");\n\t\tprocess.exit(1);\n\t}\n\n\t// Check if container exists and is running\n\ttry {\n\t\tconst result = await execSimple(\"docker\", [\"inspect\", \"-f\", \"{{.State.Running}}\", config.container]);\n\t\tif (result.trim() !== \"true\") {\n\t\t\tconsole.error(`Error: Container '${config.container}' is not running.`);\n\t\t\tconsole.error(`Start it with: docker start ${config.container}`);\n\t\t\tprocess.exit(1);\n\t\t}\n\t} catch {\n\t\tconsole.error(`Error: Container '${config.container}' does not exist.`);\n\t\tconsole.error(\"Create it with: ./docker.sh create <data-dir>\");\n\t\tprocess.exit(1);\n\t}\n\n\tconsole.log(` Docker container '${config.container}' is running.`);\n}\n\nfunction execSimple(cmd: string, args: string[]): Promise<string> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst child = spawn(cmd, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tchild.stdout?.on(\"data\", (d) => {\n\t\t\tstdout += d;\n\t\t});\n\t\tchild.stderr?.on(\"data\", (d) => {\n\t\t\tstderr += d;\n\t\t});\n\t\tchild.on(\"close\", (code) => {\n\t\t\tif (code === 0) resolve(stdout);\n\t\t\telse reject(new Error(stderr || `Exit code ${code}`));\n\t\t});\n\t});\n}\n\n/**\n * Create an executor that runs commands either on host or in Docker container\n */\nexport function createExecutor(config: SandboxConfig): Executor {\n\tif (config.type === \"host\") {\n\t\treturn new HostExecutor();\n\t}\n\treturn new DockerExecutor(config.container);\n}\n\nexport interface Executor {\n\t/**\n\t * Execute a bash command\n\t */\n\texec(command: string, options?: ExecOptions): Promise<ExecResult>;\n\n\t/**\n\t * Get the workspace path prefix for this executor\n\t * Host: returns the actual path\n\t * Docker: returns /workspace\n\t */\n\tgetWorkspacePath(hostPath: string): string;\n}\n\nexport interface ExecOptions {\n\ttimeout?: number;\n\tsignal?: AbortSignal;\n}\n\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n}\n\nclass HostExecutor implements Executor {\n\tasync exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst shell = process.platform === \"win32\" ? \"cmd\" : \"sh\";\n\t\t\tconst shellArgs = process.platform === \"win32\" ? [\"/c\"] : [\"-c\"];\n\n\t\t\tconst child = spawn(shell, [...shellArgs, command], {\n\t\t\t\tdetached: true,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t});\n\n\t\t\tlet stdout = \"\";\n\t\t\tlet stderr = \"\";\n\t\t\tlet timedOut = false;\n\n\t\t\tconst timeoutHandle =\n\t\t\t\toptions?.timeout && options.timeout > 0\n\t\t\t\t\t? setTimeout(() => {\n\t\t\t\t\t\t\ttimedOut = true;\n\t\t\t\t\t\t\tkillProcessTree(child.pid!);\n\t\t\t\t\t\t}, options.timeout * 1000)\n\t\t\t\t\t: undefined;\n\n\t\t\tconst onAbort = () => {\n\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t};\n\n\t\t\tif (options?.signal) {\n\t\t\t\tif (options.signal.aborted) {\n\t\t\t\t\tonAbort();\n\t\t\t\t} else {\n\t\t\t\t\toptions.signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tchild.stdout?.on(\"data\", (data) => {\n\t\t\t\tstdout += data.toString();\n\t\t\t\tif (stdout.length > 10 * 1024 * 1024) {\n\t\t\t\t\tstdout = stdout.slice(0, 10 * 1024 * 1024);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tchild.stderr?.on(\"data\", (data) => {\n\t\t\t\tstderr += data.toString();\n\t\t\t\tif (stderr.length > 10 * 1024 * 1024) {\n\t\t\t\t\tstderr = stderr.slice(0, 10 * 1024 * 1024);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\tif (options?.signal) {\n\t\t\t\t\toptions.signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t}\n\n\t\t\t\tif (options?.signal?.aborted) {\n\t\t\t\t\treject(new Error(`${stdout}\\n${stderr}\\nCommand aborted`.trim()));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (timedOut) {\n\t\t\t\t\treject(new Error(`${stdout}\\n${stderr}\\nCommand timed out after ${options?.timeout} seconds`.trim()));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tresolve({ stdout, stderr, code: code ?? 0 });\n\t\t\t});\n\t\t});\n\t}\n\n\tgetWorkspacePath(hostPath: string): string {\n\t\treturn hostPath;\n\t}\n}\n\nclass DockerExecutor implements Executor {\n\tconstructor(private container: string) {}\n\n\tasync exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n\t\t// Wrap command for docker exec\n\t\tconst dockerCmd = `docker exec ${this.container} sh -c ${shellEscape(command)}`;\n\t\tconst hostExecutor = new HostExecutor();\n\t\treturn hostExecutor.exec(dockerCmd, options);\n\t}\n\n\tgetWorkspacePath(_hostPath: string): string {\n\t\t// Docker container sees /workspace\n\t\treturn \"/workspace\";\n\t}\n}\n\nfunction killProcessTree(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\ttry {\n\t\t\tspawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n\t\t\t\tstdio: \"ignore\",\n\t\t\t\tdetached: true,\n\t\t\t});\n\t\t} catch {\n\t\t\t// Ignore errors\n\t\t}\n\t} else {\n\t\ttry {\n\t\t\tprocess.kill(-pid, \"SIGKILL\");\n\t\t} catch {\n\t\t\ttry {\n\t\t\t\tprocess.kill(pid, \"SIGKILL\");\n\t\t\t} catch {\n\t\t\t\t// Process already dead\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction shellEscape(s: string): string {\n\t// Escape for passing to sh -c\n\treturn `'${s.replace(/'/g, \"'\\\\''\")}'`;\n}\n"]}
@@ -0,0 +1,183 @@
1
+ import { spawn } from "child_process";
2
+ export function parseSandboxArg(value) {
3
+ if (value === "host") {
4
+ return { type: "host" };
5
+ }
6
+ if (value.startsWith("docker:")) {
7
+ const container = value.slice("docker:".length);
8
+ if (!container) {
9
+ console.error("Error: docker sandbox requires container name (e.g., docker:mom-sandbox)");
10
+ process.exit(1);
11
+ }
12
+ return { type: "docker", container };
13
+ }
14
+ console.error(`Error: Invalid sandbox type '${value}'. Use 'host' or 'docker:<container-name>'`);
15
+ process.exit(1);
16
+ }
17
+ export async function validateSandbox(config) {
18
+ if (config.type === "host") {
19
+ return;
20
+ }
21
+ // Check if Docker is available
22
+ try {
23
+ await execSimple("docker", ["--version"]);
24
+ }
25
+ catch {
26
+ console.error("Error: Docker is not installed or not in PATH");
27
+ process.exit(1);
28
+ }
29
+ // Check if container exists and is running
30
+ try {
31
+ const result = await execSimple("docker", ["inspect", "-f", "{{.State.Running}}", config.container]);
32
+ if (result.trim() !== "true") {
33
+ console.error(`Error: Container '${config.container}' is not running.`);
34
+ console.error(`Start it with: docker start ${config.container}`);
35
+ process.exit(1);
36
+ }
37
+ }
38
+ catch {
39
+ console.error(`Error: Container '${config.container}' does not exist.`);
40
+ console.error("Create it with: ./docker.sh create <data-dir>");
41
+ process.exit(1);
42
+ }
43
+ console.log(` Docker container '${config.container}' is running.`);
44
+ }
45
+ function execSimple(cmd, args) {
46
+ return new Promise((resolve, reject) => {
47
+ const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
48
+ let stdout = "";
49
+ let stderr = "";
50
+ child.stdout?.on("data", (d) => {
51
+ stdout += d;
52
+ });
53
+ child.stderr?.on("data", (d) => {
54
+ stderr += d;
55
+ });
56
+ child.on("close", (code) => {
57
+ if (code === 0)
58
+ resolve(stdout);
59
+ else
60
+ reject(new Error(stderr || `Exit code ${code}`));
61
+ });
62
+ });
63
+ }
64
+ /**
65
+ * Create an executor that runs commands either on host or in Docker container
66
+ */
67
+ export function createExecutor(config) {
68
+ if (config.type === "host") {
69
+ return new HostExecutor();
70
+ }
71
+ return new DockerExecutor(config.container);
72
+ }
73
+ class HostExecutor {
74
+ async exec(command, options) {
75
+ return new Promise((resolve, reject) => {
76
+ const shell = process.platform === "win32" ? "cmd" : "sh";
77
+ const shellArgs = process.platform === "win32" ? ["/c"] : ["-c"];
78
+ const child = spawn(shell, [...shellArgs, command], {
79
+ detached: true,
80
+ stdio: ["ignore", "pipe", "pipe"],
81
+ });
82
+ let stdout = "";
83
+ let stderr = "";
84
+ let timedOut = false;
85
+ const timeoutHandle = options?.timeout && options.timeout > 0
86
+ ? setTimeout(() => {
87
+ timedOut = true;
88
+ killProcessTree(child.pid);
89
+ }, options.timeout * 1000)
90
+ : undefined;
91
+ const onAbort = () => {
92
+ if (child.pid)
93
+ killProcessTree(child.pid);
94
+ };
95
+ if (options?.signal) {
96
+ if (options.signal.aborted) {
97
+ onAbort();
98
+ }
99
+ else {
100
+ options.signal.addEventListener("abort", onAbort, { once: true });
101
+ }
102
+ }
103
+ child.stdout?.on("data", (data) => {
104
+ stdout += data.toString();
105
+ if (stdout.length > 10 * 1024 * 1024) {
106
+ stdout = stdout.slice(0, 10 * 1024 * 1024);
107
+ }
108
+ });
109
+ child.stderr?.on("data", (data) => {
110
+ stderr += data.toString();
111
+ if (stderr.length > 10 * 1024 * 1024) {
112
+ stderr = stderr.slice(0, 10 * 1024 * 1024);
113
+ }
114
+ });
115
+ child.on("close", (code) => {
116
+ if (timeoutHandle)
117
+ clearTimeout(timeoutHandle);
118
+ if (options?.signal) {
119
+ options.signal.removeEventListener("abort", onAbort);
120
+ }
121
+ if (options?.signal?.aborted) {
122
+ reject(new Error(`${stdout}\n${stderr}\nCommand aborted`.trim()));
123
+ return;
124
+ }
125
+ if (timedOut) {
126
+ reject(new Error(`${stdout}\n${stderr}\nCommand timed out after ${options?.timeout} seconds`.trim()));
127
+ return;
128
+ }
129
+ resolve({ stdout, stderr, code: code ?? 0 });
130
+ });
131
+ });
132
+ }
133
+ getWorkspacePath(hostPath) {
134
+ return hostPath;
135
+ }
136
+ }
137
+ class DockerExecutor {
138
+ container;
139
+ constructor(container) {
140
+ this.container = container;
141
+ }
142
+ async exec(command, options) {
143
+ // Wrap command for docker exec
144
+ const dockerCmd = `docker exec ${this.container} sh -c ${shellEscape(command)}`;
145
+ const hostExecutor = new HostExecutor();
146
+ return hostExecutor.exec(dockerCmd, options);
147
+ }
148
+ getWorkspacePath(_hostPath) {
149
+ // Docker container sees /workspace
150
+ return "/workspace";
151
+ }
152
+ }
153
+ function killProcessTree(pid) {
154
+ if (process.platform === "win32") {
155
+ try {
156
+ spawn("taskkill", ["/F", "/T", "/PID", String(pid)], {
157
+ stdio: "ignore",
158
+ detached: true,
159
+ });
160
+ }
161
+ catch {
162
+ // Ignore errors
163
+ }
164
+ }
165
+ else {
166
+ try {
167
+ process.kill(-pid, "SIGKILL");
168
+ }
169
+ catch {
170
+ try {
171
+ process.kill(pid, "SIGKILL");
172
+ }
173
+ catch {
174
+ // Process already dead
175
+ }
176
+ }
177
+ }
178
+ }
179
+ function shellEscape(s) {
180
+ // Escape for passing to sh -c
181
+ return `'${s.replace(/'/g, "'\\''")}'`;
182
+ }
183
+ //# sourceMappingURL=sandbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"child_process\";\n\nexport type SandboxConfig = { type: \"host\" } | { type: \"docker\"; container: string };\n\nexport function parseSandboxArg(value: string): SandboxConfig {\n\tif (value === \"host\") {\n\t\treturn { type: \"host\" };\n\t}\n\tif (value.startsWith(\"docker:\")) {\n\t\tconst container = value.slice(\"docker:\".length);\n\t\tif (!container) {\n\t\t\tconsole.error(\"Error: docker sandbox requires container name (e.g., docker:mom-sandbox)\");\n\t\t\tprocess.exit(1);\n\t\t}\n\t\treturn { type: \"docker\", container };\n\t}\n\tconsole.error(`Error: Invalid sandbox type '${value}'. Use 'host' or 'docker:<container-name>'`);\n\tprocess.exit(1);\n}\n\nexport async function validateSandbox(config: SandboxConfig): Promise<void> {\n\tif (config.type === \"host\") {\n\t\treturn;\n\t}\n\n\t// Check if Docker is available\n\ttry {\n\t\tawait execSimple(\"docker\", [\"--version\"]);\n\t} catch {\n\t\tconsole.error(\"Error: Docker is not installed or not in PATH\");\n\t\tprocess.exit(1);\n\t}\n\n\t// Check if container exists and is running\n\ttry {\n\t\tconst result = await execSimple(\"docker\", [\"inspect\", \"-f\", \"{{.State.Running}}\", config.container]);\n\t\tif (result.trim() !== \"true\") {\n\t\t\tconsole.error(`Error: Container '${config.container}' is not running.`);\n\t\t\tconsole.error(`Start it with: docker start ${config.container}`);\n\t\t\tprocess.exit(1);\n\t\t}\n\t} catch {\n\t\tconsole.error(`Error: Container '${config.container}' does not exist.`);\n\t\tconsole.error(\"Create it with: ./docker.sh create <data-dir>\");\n\t\tprocess.exit(1);\n\t}\n\n\tconsole.log(` Docker container '${config.container}' is running.`);\n}\n\nfunction execSimple(cmd: string, args: string[]): Promise<string> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst child = spawn(cmd, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tchild.stdout?.on(\"data\", (d) => {\n\t\t\tstdout += d;\n\t\t});\n\t\tchild.stderr?.on(\"data\", (d) => {\n\t\t\tstderr += d;\n\t\t});\n\t\tchild.on(\"close\", (code) => {\n\t\t\tif (code === 0) resolve(stdout);\n\t\t\telse reject(new Error(stderr || `Exit code ${code}`));\n\t\t});\n\t});\n}\n\n/**\n * Create an executor that runs commands either on host or in Docker container\n */\nexport function createExecutor(config: SandboxConfig): Executor {\n\tif (config.type === \"host\") {\n\t\treturn new HostExecutor();\n\t}\n\treturn new DockerExecutor(config.container);\n}\n\nexport interface Executor {\n\t/**\n\t * Execute a bash command\n\t */\n\texec(command: string, options?: ExecOptions): Promise<ExecResult>;\n\n\t/**\n\t * Get the workspace path prefix for this executor\n\t * Host: returns the actual path\n\t * Docker: returns /workspace\n\t */\n\tgetWorkspacePath(hostPath: string): string;\n}\n\nexport interface ExecOptions {\n\ttimeout?: number;\n\tsignal?: AbortSignal;\n}\n\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n}\n\nclass HostExecutor implements Executor {\n\tasync exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst shell = process.platform === \"win32\" ? \"cmd\" : \"sh\";\n\t\t\tconst shellArgs = process.platform === \"win32\" ? [\"/c\"] : [\"-c\"];\n\n\t\t\tconst child = spawn(shell, [...shellArgs, command], {\n\t\t\t\tdetached: true,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t});\n\n\t\t\tlet stdout = \"\";\n\t\t\tlet stderr = \"\";\n\t\t\tlet timedOut = false;\n\n\t\t\tconst timeoutHandle =\n\t\t\t\toptions?.timeout && options.timeout > 0\n\t\t\t\t\t? setTimeout(() => {\n\t\t\t\t\t\t\ttimedOut = true;\n\t\t\t\t\t\t\tkillProcessTree(child.pid!);\n\t\t\t\t\t\t}, options.timeout * 1000)\n\t\t\t\t\t: undefined;\n\n\t\t\tconst onAbort = () => {\n\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t};\n\n\t\t\tif (options?.signal) {\n\t\t\t\tif (options.signal.aborted) {\n\t\t\t\t\tonAbort();\n\t\t\t\t} else {\n\t\t\t\t\toptions.signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tchild.stdout?.on(\"data\", (data) => {\n\t\t\t\tstdout += data.toString();\n\t\t\t\tif (stdout.length > 10 * 1024 * 1024) {\n\t\t\t\t\tstdout = stdout.slice(0, 10 * 1024 * 1024);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tchild.stderr?.on(\"data\", (data) => {\n\t\t\t\tstderr += data.toString();\n\t\t\t\tif (stderr.length > 10 * 1024 * 1024) {\n\t\t\t\t\tstderr = stderr.slice(0, 10 * 1024 * 1024);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\tif (options?.signal) {\n\t\t\t\t\toptions.signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t}\n\n\t\t\t\tif (options?.signal?.aborted) {\n\t\t\t\t\treject(new Error(`${stdout}\\n${stderr}\\nCommand aborted`.trim()));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (timedOut) {\n\t\t\t\t\treject(new Error(`${stdout}\\n${stderr}\\nCommand timed out after ${options?.timeout} seconds`.trim()));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tresolve({ stdout, stderr, code: code ?? 0 });\n\t\t\t});\n\t\t});\n\t}\n\n\tgetWorkspacePath(hostPath: string): string {\n\t\treturn hostPath;\n\t}\n}\n\nclass DockerExecutor implements Executor {\n\tconstructor(private container: string) {}\n\n\tasync exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n\t\t// Wrap command for docker exec\n\t\tconst dockerCmd = `docker exec ${this.container} sh -c ${shellEscape(command)}`;\n\t\tconst hostExecutor = new HostExecutor();\n\t\treturn hostExecutor.exec(dockerCmd, options);\n\t}\n\n\tgetWorkspacePath(_hostPath: string): string {\n\t\t// Docker container sees /workspace\n\t\treturn \"/workspace\";\n\t}\n}\n\nfunction killProcessTree(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\ttry {\n\t\t\tspawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n\t\t\t\tstdio: \"ignore\",\n\t\t\t\tdetached: true,\n\t\t\t});\n\t\t} catch {\n\t\t\t// Ignore errors\n\t\t}\n\t} else {\n\t\ttry {\n\t\t\tprocess.kill(-pid, \"SIGKILL\");\n\t\t} catch {\n\t\t\ttry {\n\t\t\t\tprocess.kill(pid, \"SIGKILL\");\n\t\t\t} catch {\n\t\t\t\t// Process already dead\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction shellEscape(s: string): string {\n\t// Escape for passing to sh -c\n\treturn `'${s.replace(/'/g, \"'\\\\''\")}'`;\n}\n"]}
@@ -0,0 +1,46 @@
1
+ import { type Attachment, ChannelStore } from "./store.js";
2
+ export interface SlackMessage {
3
+ text: string;
4
+ rawText: string;
5
+ user: string;
6
+ channel: string;
7
+ ts: string;
8
+ attachments: Attachment[];
9
+ }
10
+ export interface SlackContext {
11
+ message: SlackMessage;
12
+ store: ChannelStore;
13
+ /** Send/update the main message (accumulates text) */
14
+ respond(text: string): Promise<void>;
15
+ /** Post a message in the thread under the main message (for verbose details) */
16
+ respondInThread(text: string): Promise<void>;
17
+ /** Show/hide typing indicator */
18
+ setTyping(isTyping: boolean): Promise<void>;
19
+ /** Upload a file to the channel */
20
+ uploadFile(filePath: string, title?: string): Promise<void>;
21
+ }
22
+ export interface MomHandler {
23
+ onChannelMention(ctx: SlackContext): Promise<void>;
24
+ onDirectMessage(ctx: SlackContext): Promise<void>;
25
+ }
26
+ export interface MomBotConfig {
27
+ appToken: string;
28
+ botToken: string;
29
+ workingDir: string;
30
+ }
31
+ export declare class MomBot {
32
+ private socketClient;
33
+ private webClient;
34
+ private handler;
35
+ private botUserId;
36
+ readonly store: ChannelStore;
37
+ private userCache;
38
+ constructor(handler: MomHandler, config: MomBotConfig);
39
+ private getUserInfo;
40
+ private setupEventHandlers;
41
+ private logMessage;
42
+ private createContext;
43
+ start(): Promise<void>;
44
+ stop(): Promise<void>;
45
+ }
46
+ //# sourceMappingURL=slack.d.ts.map
@@ -0,0 +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"]}
package/dist/slack.js ADDED
@@ -0,0 +1,208 @@
1
+ import { SocketModeClient } from "@slack/socket-mode";
2
+ import { WebClient } from "@slack/web-api";
3
+ import { readFileSync } from "fs";
4
+ import { basename } from "path";
5
+ import { ChannelStore } from "./store.js";
6
+ export class MomBot {
7
+ socketClient;
8
+ webClient;
9
+ handler;
10
+ botUserId = null;
11
+ store;
12
+ userCache = new Map();
13
+ constructor(handler, config) {
14
+ this.handler = handler;
15
+ this.socketClient = new SocketModeClient({ appToken: config.appToken });
16
+ this.webClient = new WebClient(config.botToken);
17
+ this.store = new ChannelStore({
18
+ workingDir: config.workingDir,
19
+ botToken: config.botToken,
20
+ });
21
+ this.setupEventHandlers();
22
+ }
23
+ async getUserInfo(userId) {
24
+ if (this.userCache.has(userId)) {
25
+ return this.userCache.get(userId);
26
+ }
27
+ try {
28
+ const result = await this.webClient.users.info({ user: userId });
29
+ const user = result.user;
30
+ const info = {
31
+ userName: user?.name || userId,
32
+ displayName: user?.real_name || user?.name || userId,
33
+ };
34
+ this.userCache.set(userId, info);
35
+ return info;
36
+ }
37
+ catch {
38
+ return { userName: userId, displayName: userId };
39
+ }
40
+ }
41
+ setupEventHandlers() {
42
+ // Handle @mentions in channels
43
+ this.socketClient.on("app_mention", async ({ event, ack }) => {
44
+ await ack();
45
+ const slackEvent = event;
46
+ // Log the mention (message event may not fire for app_mention)
47
+ await this.logMessage(slackEvent);
48
+ const ctx = this.createContext(slackEvent);
49
+ await this.handler.onChannelMention(ctx);
50
+ });
51
+ // Handle all messages (for logging) and DMs (for triggering handler)
52
+ this.socketClient.on("message", async ({ event, ack }) => {
53
+ await ack();
54
+ const slackEvent = event;
55
+ // Ignore bot messages
56
+ if (slackEvent.bot_id)
57
+ return;
58
+ // Ignore message edits, etc. (but allow file_share)
59
+ if (slackEvent.subtype !== undefined && slackEvent.subtype !== "file_share")
60
+ return;
61
+ // Ignore if no user
62
+ if (!slackEvent.user)
63
+ return;
64
+ // Ignore messages from the bot itself
65
+ if (slackEvent.user === this.botUserId)
66
+ return;
67
+ // Ignore if no text AND no files
68
+ if (!slackEvent.text && (!slackEvent.files || slackEvent.files.length === 0))
69
+ return;
70
+ // Log ALL messages (channel and DM)
71
+ await this.logMessage({
72
+ text: slackEvent.text || "",
73
+ channel: slackEvent.channel,
74
+ user: slackEvent.user,
75
+ ts: slackEvent.ts,
76
+ files: slackEvent.files,
77
+ });
78
+ // Only trigger handler for DMs (channel mentions are handled by app_mention event)
79
+ if (slackEvent.channel_type === "im") {
80
+ const ctx = this.createContext({
81
+ text: slackEvent.text || "",
82
+ channel: slackEvent.channel,
83
+ user: slackEvent.user,
84
+ ts: slackEvent.ts,
85
+ files: slackEvent.files,
86
+ });
87
+ await this.handler.onDirectMessage(ctx);
88
+ }
89
+ });
90
+ }
91
+ async logMessage(event) {
92
+ const attachments = event.files ? this.store.processAttachments(event.channel, event.files, event.ts) : [];
93
+ const { userName, displayName } = await this.getUserInfo(event.user);
94
+ await this.store.logMessage(event.channel, {
95
+ ts: event.ts,
96
+ user: event.user,
97
+ userName,
98
+ displayName,
99
+ text: event.text,
100
+ attachments,
101
+ isBot: false,
102
+ });
103
+ }
104
+ createContext(event) {
105
+ const rawText = event.text;
106
+ const text = rawText.replace(/<@[A-Z0-9]+>/gi, "").trim();
107
+ // Process attachments (for context, already logged by message handler)
108
+ const attachments = event.files ? this.store.processAttachments(event.channel, event.files, event.ts) : [];
109
+ // Track the single message for this run
110
+ let messageTs = null;
111
+ let accumulatedText = "";
112
+ let isThinking = true; // Track if we're still in "thinking" state
113
+ let updatePromise = Promise.resolve();
114
+ return {
115
+ message: {
116
+ text,
117
+ rawText,
118
+ user: event.user,
119
+ channel: event.channel,
120
+ ts: event.ts,
121
+ attachments,
122
+ },
123
+ store: this.store,
124
+ respond: async (responseText) => {
125
+ // Queue updates to avoid race conditions
126
+ updatePromise = updatePromise.then(async () => {
127
+ if (isThinking) {
128
+ // First real response replaces "Thinking..."
129
+ accumulatedText = responseText;
130
+ isThinking = false;
131
+ }
132
+ else {
133
+ // Subsequent responses get appended
134
+ accumulatedText += "\n" + responseText;
135
+ }
136
+ if (messageTs) {
137
+ // Update existing message
138
+ await this.webClient.chat.update({
139
+ channel: event.channel,
140
+ ts: messageTs,
141
+ text: accumulatedText,
142
+ });
143
+ }
144
+ else {
145
+ // Post initial message
146
+ const result = await this.webClient.chat.postMessage({
147
+ channel: event.channel,
148
+ text: accumulatedText,
149
+ });
150
+ messageTs = result.ts;
151
+ }
152
+ // Log the response
153
+ await this.store.logBotResponse(event.channel, responseText, messageTs);
154
+ });
155
+ await updatePromise;
156
+ },
157
+ respondInThread: async (threadText) => {
158
+ // Queue thread posts to maintain order
159
+ updatePromise = updatePromise.then(async () => {
160
+ if (!messageTs) {
161
+ // No main message yet, just skip
162
+ return;
163
+ }
164
+ // Post in thread under the main message
165
+ await this.webClient.chat.postMessage({
166
+ channel: event.channel,
167
+ thread_ts: messageTs,
168
+ text: threadText,
169
+ });
170
+ });
171
+ await updatePromise;
172
+ },
173
+ setTyping: async (isTyping) => {
174
+ if (isTyping && !messageTs) {
175
+ // Post initial "thinking" message
176
+ accumulatedText = "_Thinking..._";
177
+ const result = await this.webClient.chat.postMessage({
178
+ channel: event.channel,
179
+ text: accumulatedText,
180
+ });
181
+ messageTs = result.ts;
182
+ }
183
+ // We don't delete/clear anymore - message persists and gets updated
184
+ },
185
+ uploadFile: async (filePath, title) => {
186
+ const fileName = title || basename(filePath);
187
+ const fileContent = readFileSync(filePath);
188
+ await this.webClient.files.uploadV2({
189
+ channel_id: event.channel,
190
+ file: fileContent,
191
+ filename: fileName,
192
+ title: fileName,
193
+ });
194
+ },
195
+ };
196
+ }
197
+ async start() {
198
+ const auth = await this.webClient.auth.test();
199
+ this.botUserId = auth.user_id;
200
+ await this.socketClient.start();
201
+ console.log("⚡️ Mom bot connected and listening!");
202
+ }
203
+ async stop() {
204
+ await this.socketClient.disconnect();
205
+ console.log("Mom bot disconnected.");
206
+ }
207
+ }
208
+ //# sourceMappingURL=slack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack.js","sourceRoot":"","sources":["../src/slack.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AAChC,OAAO,EAAmB,YAAY,EAAE,MAAM,YAAY,CAAC;AAmC3D,MAAM,OAAO,MAAM;IACV,YAAY,CAAmB;IAC/B,SAAS,CAAY;IACrB,OAAO,CAAa;IACpB,SAAS,GAAkB,IAAI,CAAC;IACxB,KAAK,CAAe;IAC5B,SAAS,GAA2D,IAAI,GAAG,EAAE,CAAC;IAEtF,YAAY,OAAmB,EAAE,MAAoB,EAAE;QACtD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,YAAY,GAAG,IAAI,gBAAgB,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;QACxE,IAAI,CAAC,SAAS,GAAG,IAAI,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAChD,IAAI,CAAC,KAAK,GAAG,IAAI,YAAY,CAAC;YAC7B,UAAU,EAAE,MAAM,CAAC,UAAU;YAC7B,QAAQ,EAAE,MAAM,CAAC,QAAQ;SACzB,CAAC,CAAC;QAEH,IAAI,CAAC,kBAAkB,EAAE,CAAC;IAAA,CAC1B;IAEO,KAAK,CAAC,WAAW,CAAC,MAAc,EAAsD;QAC7F,IAAI,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YAChC,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAE,CAAC;QACpC,CAAC;QAED,IAAI,CAAC;YACJ,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;YACjE,MAAM,IAAI,GAAG,MAAM,CAAC,IAA6C,CAAC;YAClE,MAAM,IAAI,GAAG;gBACZ,QAAQ,EAAE,IAAI,EAAE,IAAI,IAAI,MAAM;gBAC9B,WAAW,EAAE,IAAI,EAAE,SAAS,IAAI,IAAI,EAAE,IAAI,IAAI,MAAM;aACpD,CAAC;YACF,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;YACjC,OAAO,IAAI,CAAC;QACb,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC;QAClD,CAAC;IAAA,CACD;IAEO,kBAAkB,GAAS;QAClC,+BAA+B;QAC/B,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC,aAAa,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC;YAC7D,MAAM,GAAG,EAAE,CAAC;YAEZ,MAAM,UAAU,GAAG,KAMlB,CAAC;YAEF,+DAA+D;YAC/D,MAAM,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;YAElC,MAAM,GAAG,GAAG,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;YAC3C,MAAM,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;QAAA,CACzC,CAAC,CAAC;QAEH,qEAAqE;QACrE,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC;YACzD,MAAM,GAAG,EAAE,CAAC;YAEZ,MAAM,UAAU,GAAG,KASlB,CAAC;YAEF,sBAAsB;YACtB,IAAI,UAAU,CAAC,MAAM;gBAAE,OAAO;YAC9B,oDAAoD;YACpD,IAAI,UAAU,CAAC,OAAO,KAAK,SAAS,IAAI,UAAU,CAAC,OAAO,KAAK,YAAY;gBAAE,OAAO;YACpF,oBAAoB;YACpB,IAAI,CAAC,UAAU,CAAC,IAAI;gBAAE,OAAO;YAC7B,sCAAsC;YACtC,IAAI,UAAU,CAAC,IAAI,KAAK,IAAI,CAAC,SAAS;gBAAE,OAAO;YAC/C,iCAAiC;YACjC,IAAI,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,CAAC,UAAU,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC;gBAAE,OAAO;YAErF,oCAAoC;YACpC,MAAM,IAAI,CAAC,UAAU,CAAC;gBACrB,IAAI,EAAE,UAAU,CAAC,IAAI,IAAI,EAAE;gBAC3B,OAAO,EAAE,UAAU,CAAC,OAAO;gBAC3B,IAAI,EAAE,UAAU,CAAC,IAAI;gBACrB,EAAE,EAAE,UAAU,CAAC,EAAE;gBACjB,KAAK,EAAE,UAAU,CAAC,KAAK;aACvB,CAAC,CAAC;YAEH,mFAAmF;YACnF,IAAI,UAAU,CAAC,YAAY,KAAK,IAAI,EAAE,CAAC;gBACtC,MAAM,GAAG,GAAG,IAAI,CAAC,aAAa,CAAC;oBAC9B,IAAI,EAAE,UAAU,CAAC,IAAI,IAAI,EAAE;oBAC3B,OAAO,EAAE,UAAU,CAAC,OAAO;oBAC3B,IAAI,EAAE,UAAU,CAAC,IAAI;oBACrB,EAAE,EAAE,UAAU,CAAC,EAAE;oBACjB,KAAK,EAAE,UAAU,CAAC,KAAK;iBACvB,CAAC,CAAC;gBACH,MAAM,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;YACzC,CAAC;QAAA,CACD,CAAC,CAAC;IAAA,CACH;IAEO,KAAK,CAAC,UAAU,CAAC,KAMxB,EAAiB;QACjB,MAAM,WAAW,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3G,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAErE,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,EAAE;YAC1C,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,QAAQ;YACR,WAAW;YACX,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,WAAW;YACX,KAAK,EAAE,KAAK;SACZ,CAAC,CAAC;IAAA,CACH;IAEO,aAAa,CAAC,KAMrB,EAAgB;QAChB,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC;QAC3B,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAE1D,uEAAuE;QACvE,MAAM,WAAW,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAE3G,wCAAwC;QACxC,IAAI,SAAS,GAAkB,IAAI,CAAC;QACpC,IAAI,eAAe,GAAG,EAAE,CAAC;QACzB,IAAI,UAAU,GAAG,IAAI,CAAC,CAAC,2CAA2C;QAClE,IAAI,aAAa,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;QAErD,OAAO;YACN,OAAO,EAAE;gBACR,IAAI;gBACJ,OAAO;gBACP,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,EAAE,EAAE,KAAK,CAAC,EAAE;gBACZ,WAAW;aACX;YACD,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,OAAO,EAAE,KAAK,EAAE,YAAoB,EAAE,EAAE,CAAC;gBACxC,yCAAyC;gBACzC,aAAa,GAAG,aAAa,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;oBAC9C,IAAI,UAAU,EAAE,CAAC;wBAChB,6CAA6C;wBAC7C,eAAe,GAAG,YAAY,CAAC;wBAC/B,UAAU,GAAG,KAAK,CAAC;oBACpB,CAAC;yBAAM,CAAC;wBACP,oCAAoC;wBACpC,eAAe,IAAI,IAAI,GAAG,YAAY,CAAC;oBACxC,CAAC;oBAED,IAAI,SAAS,EAAE,CAAC;wBACf,0BAA0B;wBAC1B,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC;4BAChC,OAAO,EAAE,KAAK,CAAC,OAAO;4BACtB,EAAE,EAAE,SAAS;4BACb,IAAI,EAAE,eAAe;yBACrB,CAAC,CAAC;oBACJ,CAAC;yBAAM,CAAC;wBACP,uBAAuB;wBACvB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC;4BACpD,OAAO,EAAE,KAAK,CAAC,OAAO;4BACtB,IAAI,EAAE,eAAe;yBACrB,CAAC,CAAC;wBACH,SAAS,GAAG,MAAM,CAAC,EAAY,CAAC;oBACjC,CAAC;oBAED,mBAAmB;oBACnB,MAAM,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,OAAO,EAAE,YAAY,EAAE,SAAU,CAAC,CAAC;gBAAA,CACzE,CAAC,CAAC;gBAEH,MAAM,aAAa,CAAC;YAAA,CACpB;YACD,eAAe,EAAE,KAAK,EAAE,UAAkB,EAAE,EAAE,CAAC;gBAC9C,uCAAuC;gBACvC,aAAa,GAAG,aAAa,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;oBAC9C,IAAI,CAAC,SAAS,EAAE,CAAC;wBAChB,iCAAiC;wBACjC,OAAO;oBACR,CAAC;oBACD,wCAAwC;oBACxC,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC;wBACrC,OAAO,EAAE,KAAK,CAAC,OAAO;wBACtB,SAAS,EAAE,SAAS;wBACpB,IAAI,EAAE,UAAU;qBAChB,CAAC,CAAC;gBAAA,CACH,CAAC,CAAC;gBACH,MAAM,aAAa,CAAC;YAAA,CACpB;YACD,SAAS,EAAE,KAAK,EAAE,QAAiB,EAAE,EAAE,CAAC;gBACvC,IAAI,QAAQ,IAAI,CAAC,SAAS,EAAE,CAAC;oBAC5B,kCAAkC;oBAClC,eAAe,GAAG,eAAe,CAAC;oBAClC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC;wBACpD,OAAO,EAAE,KAAK,CAAC,OAAO;wBACtB,IAAI,EAAE,eAAe;qBACrB,CAAC,CAAC;oBACH,SAAS,GAAG,MAAM,CAAC,EAAY,CAAC;gBACjC,CAAC;gBACD,oEAAoE;YADnE,CAED;YACD,UAAU,EAAE,KAAK,EAAE,QAAgB,EAAE,KAAc,EAAE,EAAE,CAAC;gBACvD,MAAM,QAAQ,GAAG,KAAK,IAAI,QAAQ,CAAC,QAAQ,CAAC,CAAC;gBAC7C,MAAM,WAAW,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;gBAE3C,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC;oBACnC,UAAU,EAAE,KAAK,CAAC,OAAO;oBACzB,IAAI,EAAE,WAAW;oBACjB,QAAQ,EAAE,QAAQ;oBAClB,KAAK,EAAE,QAAQ;iBACf,CAAC,CAAC;YAAA,CACH;SACD,CAAC;IAAA,CACF;IAED,KAAK,CAAC,KAAK,GAAkB;QAC5B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAC9C,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,OAAiB,CAAC;QACxC,MAAM,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;QAChC,OAAO,CAAC,GAAG,CAAC,yCAAqC,CAAC,CAAC;IAAA,CACnD;IAED,KAAK,CAAC,IAAI,GAAkB;QAC3B,MAAM,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE,CAAC;QACrC,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;IAAA,CACrC;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"]}