@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
package/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ ## [0.9.4] - 2025-11-26
4
+
5
+ ### Added
6
+
7
+ - Initial release of Mom Slack bot
8
+ - Slack integration with @mentions and DMs
9
+ - Docker sandbox mode for isolated execution
10
+ - Bash tool with full shell access
11
+ - Read, write, edit file tools
12
+ - Attach tool for sharing files in Slack
13
+ - Thread-based tool details (clean main messages, verbose details in threads)
14
+ - Single accumulated message per agent run
15
+ - Stop command (`@mom stop`) to abort running tasks
16
+ - Persistent workspace per channel with scratchpad directory
17
+ - Streaming console output for monitoring
package/README.md ADDED
@@ -0,0 +1,183 @@
1
+ # @mariozechner/pi-mom
2
+
3
+ A Slack bot powered by Claude that can execute bash commands, read/write files, and interact with your development environment. Designed to be your helpful team assistant.
4
+
5
+ ## Features
6
+
7
+ - **Slack Integration**: Responds to @mentions in channels and DMs
8
+ - **Full Bash Access**: Execute any command, install tools, configure credentials
9
+ - **File Operations**: Read, write, and edit files
10
+ - **Docker Sandbox**: Optional isolation to protect your host machine
11
+ - **Persistent Workspace**: Each channel gets its own workspace that persists across conversations
12
+ - **Thread-Based Details**: Clean main messages with verbose tool details in threads
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @mariozechner/pi-mom
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```bash
23
+ # Set environment variables
24
+ export MOM_SLACK_APP_TOKEN=xapp-...
25
+ export MOM_SLACK_BOT_TOKEN=xoxb-...
26
+ export ANTHROPIC_API_KEY=sk-ant-...
27
+ # or use your Claude Pro/Max subscription
28
+ # to get the token install Claude Code and run claude setup-token
29
+ export ANTHROPIC_OAUTH_TOKEN=sk-ant-...
30
+
31
+ # Run mom
32
+ mom ./data
33
+ ```
34
+
35
+ ## Slack App Setup
36
+
37
+ 1. Create a new Slack app at https://api.slack.com/apps
38
+ 2. Enable **Socket Mode** (Settings → Socket Mode → Enable)
39
+ 3. Generate an **App-Level Token** with `connections:write` scope → this is `MOM_SLACK_APP_TOKEN`
40
+ 4. Add **Bot Token Scopes** (OAuth & Permissions):
41
+ - `app_mentions:read`
42
+ - `channels:history`
43
+ - `channels:read`
44
+ - `chat:write`
45
+ - `files:read`
46
+ - `files:write`
47
+ - `im:history`
48
+ - `im:read`
49
+ - `im:write`
50
+ - `users:read`
51
+ 5. **Subscribe to Bot Events** (Event Subscriptions):
52
+ - `app_mention`
53
+ - `message.channels`
54
+ - `message.im`
55
+ 6. Install the app to your workspace → get the **Bot User OAuth Token** → this is `MOM_SLACK_BOT_TOKEN`
56
+
57
+ ## Usage
58
+
59
+ ### Host Mode (Default)
60
+
61
+ Run tools directly on your machine:
62
+
63
+ ```bash
64
+ mom ./data
65
+ ```
66
+
67
+ ### Docker Sandbox Mode
68
+
69
+ Isolate mom in a container to protect your host:
70
+
71
+ ```bash
72
+ # Create the sandbox container
73
+ ./docker.sh create ./data
74
+
75
+ # Run mom with sandbox
76
+ mom --sandbox=docker:mom-sandbox ./data
77
+ ```
78
+
79
+ ### Talking to Mom
80
+
81
+ In Slack:
82
+ ```
83
+ @mom what's in the current directory?
84
+ @mom clone the repo https://github.com/example/repo and find all TODO comments
85
+ @mom install htop and show me system stats
86
+ ```
87
+
88
+ Mom will:
89
+ 1. Show brief status updates in the main message
90
+ 2. Post detailed tool calls and results in a thread
91
+ 3. Provide a final response
92
+
93
+ ### Stopping Mom
94
+
95
+ If mom is working on something and you need to stop:
96
+ ```
97
+ @mom stop
98
+ ```
99
+
100
+ ## CLI Options
101
+
102
+ ```bash
103
+ mom [options] <working-directory>
104
+
105
+ Options:
106
+ --sandbox=host Run tools on host (default)
107
+ --sandbox=docker:<name> Run tools in Docker container
108
+ ```
109
+
110
+ ## Docker Sandbox
111
+
112
+ The Docker sandbox treats the container as mom's personal computer:
113
+
114
+ - **Persistent**: Install tools with `apk add`, configure credentials - changes persist
115
+ - **Isolated**: Mom can only access `/workspace` (your data directory)
116
+ - **Self-Managing**: Mom can install what she needs and ask for credentials
117
+
118
+ ### Container Management
119
+
120
+ ```bash
121
+ ./docker.sh create <data-dir> # Create and start container
122
+ ./docker.sh start # Start existing container
123
+ ./docker.sh stop # Stop container
124
+ ./docker.sh remove # Remove container
125
+ ./docker.sh status # Check if running
126
+ ./docker.sh shell # Open shell in container
127
+ ```
128
+
129
+ ### Example Flow
130
+
131
+ ```
132
+ User: @mom check the spine-runtimes repo on GitHub
133
+ Mom: I need gh CLI. Installing...
134
+ (runs: apk add github-cli)
135
+ Mom: I need a GitHub token. Please provide one.
136
+ User: ghp_xxxx...
137
+ Mom: (configures gh auth)
138
+ Mom: Done. Here's the repo info...
139
+ ```
140
+
141
+ ## Workspace Structure
142
+
143
+ Each Slack channel gets its own workspace:
144
+
145
+ ```
146
+ ./data/
147
+ └── C123ABC/ # Channel ID
148
+ ├── log.jsonl # Message history (managed by mom)
149
+ ├── attachments/ # Files shared in channel
150
+ └── scratch/ # Mom's working directory
151
+ ```
152
+
153
+ ## Environment Variables
154
+
155
+ | Variable | Description |
156
+ |----------|-------------|
157
+ | `MOM_SLACK_APP_TOKEN` | Slack app-level token (xapp-...) |
158
+ | `MOM_SLACK_BOT_TOKEN` | Slack bot token (xoxb-...) |
159
+ | `ANTHROPIC_API_KEY` | Anthropic API key |
160
+ | `ANTHROPIC_OAUTH_TOKEN` | Alternative: Anthropic OAuth token |
161
+
162
+ ## Security Considerations
163
+
164
+ **Host Mode**: Mom has full access to your machine. Only use in trusted environments.
165
+
166
+ **Docker Mode**: Mom is isolated to the container. She can:
167
+ - Read/write files in `/workspace` (your data dir)
168
+ - Make network requests
169
+ - Install packages in the container
170
+
171
+ She cannot:
172
+ - Access files outside `/workspace`
173
+ - Access your host credentials
174
+ - Affect your host system
175
+
176
+ **Recommendations**:
177
+ 1. Use Docker mode for shared Slack workspaces
178
+ 2. Create a dedicated GitHub bot account with limited repo access
179
+ 3. Only share necessary credentials with mom
180
+
181
+ ## License
182
+
183
+ MIT
@@ -0,0 +1,9 @@
1
+ import { type SandboxConfig } from "./sandbox.js";
2
+ import type { SlackContext } from "./slack.js";
3
+ import type { ChannelStore } from "./store.js";
4
+ export interface AgentRunner {
5
+ run(ctx: SlackContext, channelDir: string, store: ChannelStore): Promise<void>;
6
+ abort(): void;
7
+ }
8
+ export declare function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner;
9
+ //# sourceMappingURL=agent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAMA,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAM/C,MAAM,WAAW,WAAW;IAC3B,GAAG,CAAC,GAAG,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,KAAK,IAAI,IAAI,CAAC;CACd;AA4GD,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,aAAa,GAAG,WAAW,CAoJ3E","sourcesContent":["import { Agent, type AgentEvent, ProviderTransport } from \"@mariozechner/pi-agent-core\";\nimport { getModel } from \"@mariozechner/pi-ai\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { mkdir } from \"fs/promises\";\nimport { join } from \"path\";\n\nimport { createExecutor, type SandboxConfig } from \"./sandbox.js\";\nimport type { SlackContext } from \"./slack.js\";\nimport type { ChannelStore } from \"./store.js\";\nimport { createMomTools, setUploadFunction } from \"./tools/index.js\";\n\n// Hardcoded model for now\nconst model = getModel(\"anthropic\", \"claude-opus-4-5\");\n\nexport interface AgentRunner {\n\trun(ctx: SlackContext, channelDir: string, store: ChannelStore): Promise<void>;\n\tabort(): void;\n}\n\nfunction getAnthropicApiKey(): string {\n\tconst key = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;\n\tif (!key) {\n\t\tthrow new Error(\"ANTHROPIC_OAUTH_TOKEN or ANTHROPIC_API_KEY must be set\");\n\t}\n\treturn key;\n}\n\nfunction getRecentMessages(channelDir: string, count: number): string {\n\tconst logPath = join(channelDir, \"log.jsonl\");\n\tif (!existsSync(logPath)) {\n\t\treturn \"(no message history yet)\";\n\t}\n\n\tconst content = readFileSync(logPath, \"utf-8\");\n\tconst lines = content.trim().split(\"\\n\").filter(Boolean);\n\tconst recentLines = lines.slice(-count);\n\n\tif (recentLines.length === 0) {\n\t\treturn \"(no message history yet)\";\n\t}\n\n\treturn recentLines.join(\"\\n\");\n}\n\nfunction buildSystemPrompt(\n\tworkspacePath: string,\n\tchannelId: string,\n\trecentMessages: string,\n\tsandboxConfig: SandboxConfig,\n): string {\n\tconst channelPath = `${workspacePath}/${channelId}`;\n\tconst isDocker = sandboxConfig.type === \"docker\";\n\n\tconst envDescription = isDocker\n\t\t? `You are running inside a Docker container (Alpine Linux).\n- Install tools with: apk add <package>\n- Your changes persist across sessions\n- You have full control over this container`\n\t\t: `You are running directly on the host machine.\n- Be careful with system modifications\n- Use the system's package manager if needed`;\n\n\treturn `You are mom, a helpful Slack bot assistant.\n\n## Communication Style\n- Be concise and professional\n- Do not use emojis unless the user communicates informally with you\n- Get to the point quickly\n- If you need clarification, ask directly\n- Use Slack's mrkdwn format (NOT standard Markdown):\n - Bold: *text* (single asterisks)\n - Italic: _text_\n - Strikethrough: ~text~\n - Code: \\`code\\`\n - Code block: \\`\\`\\`code\\`\\`\\`\n - Links: <url|text>\n - Do NOT use **double asterisks** or [markdown](links)\n\n## Your Environment\n${envDescription}\n\n## Your Workspace\nYour working directory is: ${channelPath}\n\n### Scratchpad\nUse ${channelPath}/scratch/ for temporary work like cloning repos, generating files, etc.\nThis directory persists across conversations, so you can reference previous work.\n\n### Channel Data (read-only, managed by the system)\n- Message history: ${channelPath}/log.jsonl\n- Attachments from users: ${channelPath}/attachments/\n\nYou can:\n- Configure tools and save credentials in your home directory\n- Create files and directories in your scratchpad\n\n### Recent Messages (last 50)\n${recentMessages}\n\n## Tools\nYou have access to: bash, read, edit, write, attach tools.\n- bash: Run shell commands (this is your main tool)\n- read: Read files\n- edit: Edit files surgically\n- write: Create/overwrite files\n- attach: Share a file with the user in Slack\n\nEach tool requires a \"label\" parameter - brief description shown to the user.\n\n## Guidelines\n- Be concise and helpful\n- Use bash for most operations\n- If you need a tool, install it\n- If you need credentials, ask the user\n\n## CRITICAL\n- DO NOT USE EMOJIS. KEEP YOUR RESPONSES AS SHORT AS POSSIBLE.\n`;\n}\n\nfunction truncate(text: string, maxLen: number): string {\n\tif (text.length <= maxLen) return text;\n\treturn text.substring(0, maxLen - 3) + \"...\";\n}\n\nexport function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {\n\tlet agent: Agent | null = null;\n\tconst executor = createExecutor(sandboxConfig);\n\n\treturn {\n\t\tasync run(ctx: SlackContext, channelDir: string, store: ChannelStore): Promise<void> {\n\t\t\t// Ensure channel directory exists\n\t\t\tawait mkdir(channelDir, { recursive: true });\n\n\t\t\tconst channelId = ctx.message.channel;\n\t\t\tconst workspacePath = executor.getWorkspacePath(channelDir.replace(`/${channelId}`, \"\"));\n\t\t\tconst recentMessages = getRecentMessages(channelDir, 50);\n\t\t\tconst systemPrompt = buildSystemPrompt(workspacePath, channelId, recentMessages, sandboxConfig);\n\n\t\t\t// Set up file upload function for the attach tool\n\t\t\t// For Docker, we need to translate paths back to host\n\t\t\tsetUploadFunction(async (filePath: string, title?: string) => {\n\t\t\t\tconst hostPath = translateToHostPath(filePath, channelDir, workspacePath, channelId);\n\t\t\t\tawait ctx.uploadFile(hostPath, title);\n\t\t\t});\n\n\t\t\t// Create tools with executor\n\t\t\tconst tools = createMomTools(executor);\n\n\t\t\t// Create ephemeral agent\n\t\t\tagent = new Agent({\n\t\t\t\tinitialState: {\n\t\t\t\t\tsystemPrompt,\n\t\t\t\t\tmodel,\n\t\t\t\t\tthinkingLevel: \"off\",\n\t\t\t\t\ttools,\n\t\t\t\t},\n\t\t\t\ttransport: new ProviderTransport({\n\t\t\t\t\tgetApiKey: async () => getAnthropicApiKey(),\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\t// Track pending tool calls to pair args with results\n\t\t\tconst pendingTools = new Map<string, { toolName: string; args: unknown }>();\n\n\t\t\t// Subscribe to events\n\t\t\tagent.subscribe(async (event: AgentEvent) => {\n\t\t\t\tswitch (event.type) {\n\t\t\t\t\tcase \"tool_execution_start\": {\n\t\t\t\t\t\tconst args = event.args as { label?: string };\n\t\t\t\t\t\tconst label = args.label || event.toolName;\n\n\t\t\t\t\t\t// Store args to pair with result later\n\t\t\t\t\t\tpendingTools.set(event.toolCallId, { toolName: event.toolName, args: event.args });\n\n\t\t\t\t\t\t// Log to console\n\t\t\t\t\t\tconsole.log(`\\n[Tool] ${event.toolName}: ${JSON.stringify(event.args)}`);\n\n\t\t\t\t\t\t// Log to jsonl\n\t\t\t\t\t\tawait store.logMessage(ctx.message.channel, {\n\t\t\t\t\t\t\tts: Date.now().toString(),\n\t\t\t\t\t\t\tuser: \"bot\",\n\t\t\t\t\t\t\ttext: `[Tool] ${event.toolName}: ${JSON.stringify(event.args)}`,\n\t\t\t\t\t\t\tattachments: [],\n\t\t\t\t\t\t\tisBot: true,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Show label in main message only\n\t\t\t\t\t\tawait ctx.respond(`_${label}_`);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"tool_execution_end\": {\n\t\t\t\t\t\tconst resultStr = typeof event.result === \"string\" ? event.result : JSON.stringify(event.result);\n\t\t\t\t\t\tconst pending = pendingTools.get(event.toolCallId);\n\t\t\t\t\t\tpendingTools.delete(event.toolCallId);\n\n\t\t\t\t\t\t// Log to console\n\t\t\t\t\t\tconsole.log(`[Tool Result] ${event.isError ? \"ERROR: \" : \"\"}${truncate(resultStr, 1000)}\\n`);\n\n\t\t\t\t\t\t// Log to jsonl\n\t\t\t\t\t\tawait store.logMessage(ctx.message.channel, {\n\t\t\t\t\t\t\tts: Date.now().toString(),\n\t\t\t\t\t\t\tuser: \"bot\",\n\t\t\t\t\t\t\ttext: `[Tool Result] ${event.toolName}: ${event.isError ? \"ERROR: \" : \"\"}${truncate(resultStr, 1000)}`,\n\t\t\t\t\t\t\tattachments: [],\n\t\t\t\t\t\t\tisBot: true,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Post args + result together in thread\n\t\t\t\t\t\tconst argsStr = pending ? JSON.stringify(pending.args, null, 2) : \"(args not found)\";\n\t\t\t\t\t\tconst threadResult = truncate(resultStr, 2000);\n\t\t\t\t\t\tawait ctx.respondInThread(\n\t\t\t\t\t\t\t`*[${event.toolName}]* ${event.isError ? \"❌\" : \"✓\"}\\n` +\n\t\t\t\t\t\t\t\t\"```\\n\" +\n\t\t\t\t\t\t\t\targsStr +\n\t\t\t\t\t\t\t\t\"\\n```\\n\" +\n\t\t\t\t\t\t\t\t\"*Result:*\\n```\\n\" +\n\t\t\t\t\t\t\t\tthreadResult +\n\t\t\t\t\t\t\t\t\"\\n```\",\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// Show brief error in main message if failed\n\t\t\t\t\t\tif (event.isError) {\n\t\t\t\t\t\t\tawait ctx.respond(`_Error: ${truncate(resultStr, 200)}_`);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"message_update\": {\n\t\t\t\t\t\tconst ev = event.assistantMessageEvent;\n\t\t\t\t\t\t// Stream deltas to console\n\t\t\t\t\t\tif (ev.type === \"text_delta\") {\n\t\t\t\t\t\t\tprocess.stdout.write(ev.delta);\n\t\t\t\t\t\t} else if (ev.type === \"thinking_delta\") {\n\t\t\t\t\t\t\tprocess.stdout.write(ev.delta);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"message_start\":\n\t\t\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\t\t\tprocess.stdout.write(\"\\n\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase \"message_end\":\n\t\t\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\t\t\tprocess.stdout.write(\"\\n\");\n\t\t\t\t\t\t\t// Extract text from assistant message\n\t\t\t\t\t\t\tconst content = event.message.content;\n\t\t\t\t\t\t\tlet text = \"\";\n\t\t\t\t\t\t\tfor (const part of content) {\n\t\t\t\t\t\t\t\tif (part.type === \"text\") {\n\t\t\t\t\t\t\t\t\ttext += part.text;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (text.trim()) {\n\t\t\t\t\t\t\t\tawait ctx.respond(text);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Run the agent with user's message\n\t\t\tawait agent.prompt(ctx.message.text || \"(attached files)\");\n\t\t},\n\n\t\tabort(): void {\n\t\t\tagent?.abort();\n\t\t},\n\t};\n}\n\n/**\n * Translate container path back to host path for file operations\n */\nfunction translateToHostPath(\n\tcontainerPath: string,\n\tchannelDir: string,\n\tworkspacePath: string,\n\tchannelId: string,\n): string {\n\tif (workspacePath === \"/workspace\") {\n\t\t// Docker mode - translate /workspace/channelId/... to host path\n\t\tconst prefix = `/workspace/${channelId}/`;\n\t\tif (containerPath.startsWith(prefix)) {\n\t\t\treturn join(channelDir, containerPath.slice(prefix.length));\n\t\t}\n\t\t// Maybe it's just /workspace/...\n\t\tif (containerPath.startsWith(\"/workspace/\")) {\n\t\t\treturn join(channelDir, \"..\", containerPath.slice(\"/workspace/\".length));\n\t\t}\n\t}\n\t// Host mode or already a host path\n\treturn containerPath;\n}\n"]}
package/dist/agent.js ADDED
@@ -0,0 +1,248 @@
1
+ import { Agent, ProviderTransport } from "@mariozechner/pi-agent-core";
2
+ import { getModel } from "@mariozechner/pi-ai";
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { mkdir } from "fs/promises";
5
+ import { join } from "path";
6
+ import { createExecutor } from "./sandbox.js";
7
+ import { createMomTools, setUploadFunction } from "./tools/index.js";
8
+ // Hardcoded model for now
9
+ const model = getModel("anthropic", "claude-opus-4-5");
10
+ function getAnthropicApiKey() {
11
+ const key = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;
12
+ if (!key) {
13
+ throw new Error("ANTHROPIC_OAUTH_TOKEN or ANTHROPIC_API_KEY must be set");
14
+ }
15
+ return key;
16
+ }
17
+ function getRecentMessages(channelDir, count) {
18
+ const logPath = join(channelDir, "log.jsonl");
19
+ if (!existsSync(logPath)) {
20
+ return "(no message history yet)";
21
+ }
22
+ const content = readFileSync(logPath, "utf-8");
23
+ const lines = content.trim().split("\n").filter(Boolean);
24
+ const recentLines = lines.slice(-count);
25
+ if (recentLines.length === 0) {
26
+ return "(no message history yet)";
27
+ }
28
+ return recentLines.join("\n");
29
+ }
30
+ function buildSystemPrompt(workspacePath, channelId, recentMessages, sandboxConfig) {
31
+ const channelPath = `${workspacePath}/${channelId}`;
32
+ const isDocker = sandboxConfig.type === "docker";
33
+ const envDescription = isDocker
34
+ ? `You are running inside a Docker container (Alpine Linux).
35
+ - Install tools with: apk add <package>
36
+ - Your changes persist across sessions
37
+ - You have full control over this container`
38
+ : `You are running directly on the host machine.
39
+ - Be careful with system modifications
40
+ - Use the system's package manager if needed`;
41
+ return `You are mom, a helpful Slack bot assistant.
42
+
43
+ ## Communication Style
44
+ - Be concise and professional
45
+ - Do not use emojis unless the user communicates informally with you
46
+ - Get to the point quickly
47
+ - If you need clarification, ask directly
48
+ - Use Slack's mrkdwn format (NOT standard Markdown):
49
+ - Bold: *text* (single asterisks)
50
+ - Italic: _text_
51
+ - Strikethrough: ~text~
52
+ - Code: \`code\`
53
+ - Code block: \`\`\`code\`\`\`
54
+ - Links: <url|text>
55
+ - Do NOT use **double asterisks** or [markdown](links)
56
+
57
+ ## Your Environment
58
+ ${envDescription}
59
+
60
+ ## Your Workspace
61
+ Your working directory is: ${channelPath}
62
+
63
+ ### Scratchpad
64
+ Use ${channelPath}/scratch/ for temporary work like cloning repos, generating files, etc.
65
+ This directory persists across conversations, so you can reference previous work.
66
+
67
+ ### Channel Data (read-only, managed by the system)
68
+ - Message history: ${channelPath}/log.jsonl
69
+ - Attachments from users: ${channelPath}/attachments/
70
+
71
+ You can:
72
+ - Configure tools and save credentials in your home directory
73
+ - Create files and directories in your scratchpad
74
+
75
+ ### Recent Messages (last 50)
76
+ ${recentMessages}
77
+
78
+ ## Tools
79
+ You have access to: bash, read, edit, write, attach tools.
80
+ - bash: Run shell commands (this is your main tool)
81
+ - read: Read files
82
+ - edit: Edit files surgically
83
+ - write: Create/overwrite files
84
+ - attach: Share a file with the user in Slack
85
+
86
+ Each tool requires a "label" parameter - brief description shown to the user.
87
+
88
+ ## Guidelines
89
+ - Be concise and helpful
90
+ - Use bash for most operations
91
+ - If you need a tool, install it
92
+ - If you need credentials, ask the user
93
+
94
+ ## CRITICAL
95
+ - DO NOT USE EMOJIS. KEEP YOUR RESPONSES AS SHORT AS POSSIBLE.
96
+ `;
97
+ }
98
+ function truncate(text, maxLen) {
99
+ if (text.length <= maxLen)
100
+ return text;
101
+ return text.substring(0, maxLen - 3) + "...";
102
+ }
103
+ export function createAgentRunner(sandboxConfig) {
104
+ let agent = null;
105
+ const executor = createExecutor(sandboxConfig);
106
+ return {
107
+ async run(ctx, channelDir, store) {
108
+ // Ensure channel directory exists
109
+ await mkdir(channelDir, { recursive: true });
110
+ const channelId = ctx.message.channel;
111
+ const workspacePath = executor.getWorkspacePath(channelDir.replace(`/${channelId}`, ""));
112
+ const recentMessages = getRecentMessages(channelDir, 50);
113
+ const systemPrompt = buildSystemPrompt(workspacePath, channelId, recentMessages, sandboxConfig);
114
+ // Set up file upload function for the attach tool
115
+ // For Docker, we need to translate paths back to host
116
+ setUploadFunction(async (filePath, title) => {
117
+ const hostPath = translateToHostPath(filePath, channelDir, workspacePath, channelId);
118
+ await ctx.uploadFile(hostPath, title);
119
+ });
120
+ // Create tools with executor
121
+ const tools = createMomTools(executor);
122
+ // Create ephemeral agent
123
+ agent = new Agent({
124
+ initialState: {
125
+ systemPrompt,
126
+ model,
127
+ thinkingLevel: "off",
128
+ tools,
129
+ },
130
+ transport: new ProviderTransport({
131
+ getApiKey: async () => getAnthropicApiKey(),
132
+ }),
133
+ });
134
+ // Track pending tool calls to pair args with results
135
+ const pendingTools = new Map();
136
+ // Subscribe to events
137
+ agent.subscribe(async (event) => {
138
+ switch (event.type) {
139
+ case "tool_execution_start": {
140
+ const args = event.args;
141
+ const label = args.label || event.toolName;
142
+ // Store args to pair with result later
143
+ pendingTools.set(event.toolCallId, { toolName: event.toolName, args: event.args });
144
+ // Log to console
145
+ console.log(`\n[Tool] ${event.toolName}: ${JSON.stringify(event.args)}`);
146
+ // Log to jsonl
147
+ await store.logMessage(ctx.message.channel, {
148
+ ts: Date.now().toString(),
149
+ user: "bot",
150
+ text: `[Tool] ${event.toolName}: ${JSON.stringify(event.args)}`,
151
+ attachments: [],
152
+ isBot: true,
153
+ });
154
+ // Show label in main message only
155
+ await ctx.respond(`_${label}_`);
156
+ break;
157
+ }
158
+ case "tool_execution_end": {
159
+ const resultStr = typeof event.result === "string" ? event.result : JSON.stringify(event.result);
160
+ const pending = pendingTools.get(event.toolCallId);
161
+ pendingTools.delete(event.toolCallId);
162
+ // Log to console
163
+ console.log(`[Tool Result] ${event.isError ? "ERROR: " : ""}${truncate(resultStr, 1000)}\n`);
164
+ // Log to jsonl
165
+ await store.logMessage(ctx.message.channel, {
166
+ ts: Date.now().toString(),
167
+ user: "bot",
168
+ text: `[Tool Result] ${event.toolName}: ${event.isError ? "ERROR: " : ""}${truncate(resultStr, 1000)}`,
169
+ attachments: [],
170
+ isBot: true,
171
+ });
172
+ // Post args + result together in thread
173
+ const argsStr = pending ? JSON.stringify(pending.args, null, 2) : "(args not found)";
174
+ const threadResult = truncate(resultStr, 2000);
175
+ await ctx.respondInThread(`*[${event.toolName}]* ${event.isError ? "❌" : "✓"}\n` +
176
+ "```\n" +
177
+ argsStr +
178
+ "\n```\n" +
179
+ "*Result:*\n```\n" +
180
+ threadResult +
181
+ "\n```");
182
+ // Show brief error in main message if failed
183
+ if (event.isError) {
184
+ await ctx.respond(`_Error: ${truncate(resultStr, 200)}_`);
185
+ }
186
+ break;
187
+ }
188
+ case "message_update": {
189
+ const ev = event.assistantMessageEvent;
190
+ // Stream deltas to console
191
+ if (ev.type === "text_delta") {
192
+ process.stdout.write(ev.delta);
193
+ }
194
+ else if (ev.type === "thinking_delta") {
195
+ process.stdout.write(ev.delta);
196
+ }
197
+ break;
198
+ }
199
+ case "message_start":
200
+ if (event.message.role === "assistant") {
201
+ process.stdout.write("\n");
202
+ }
203
+ break;
204
+ case "message_end":
205
+ if (event.message.role === "assistant") {
206
+ process.stdout.write("\n");
207
+ // Extract text from assistant message
208
+ const content = event.message.content;
209
+ let text = "";
210
+ for (const part of content) {
211
+ if (part.type === "text") {
212
+ text += part.text;
213
+ }
214
+ }
215
+ if (text.trim()) {
216
+ await ctx.respond(text);
217
+ }
218
+ }
219
+ break;
220
+ }
221
+ });
222
+ // Run the agent with user's message
223
+ await agent.prompt(ctx.message.text || "(attached files)");
224
+ },
225
+ abort() {
226
+ agent?.abort();
227
+ },
228
+ };
229
+ }
230
+ /**
231
+ * Translate container path back to host path for file operations
232
+ */
233
+ function translateToHostPath(containerPath, channelDir, workspacePath, channelId) {
234
+ if (workspacePath === "/workspace") {
235
+ // Docker mode - translate /workspace/channelId/... to host path
236
+ const prefix = `/workspace/${channelId}/`;
237
+ if (containerPath.startsWith(prefix)) {
238
+ return join(channelDir, containerPath.slice(prefix.length));
239
+ }
240
+ // Maybe it's just /workspace/...
241
+ if (containerPath.startsWith("/workspace/")) {
242
+ return join(channelDir, "..", containerPath.slice("/workspace/".length));
243
+ }
244
+ }
245
+ // Host mode or already a host path
246
+ return containerPath;
247
+ }
248
+ //# sourceMappingURL=agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.js","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAmB,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AACxF,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAC9C,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAAE,cAAc,EAAsB,MAAM,cAAc,CAAC;AAGlE,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErE,0BAA0B;AAC1B,MAAM,KAAK,GAAG,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC,CAAC;AAOvD,SAAS,kBAAkB,GAAW;IACrC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;IAC/E,IAAI,CAAC,GAAG,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,GAAG,CAAC;AAAA,CACX;AAED,SAAS,iBAAiB,CAAC,UAAkB,EAAE,KAAa,EAAU;IACrE,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;IAC9C,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC1B,OAAO,0BAA0B,CAAC;IACnC,CAAC;IAED,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC/C,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACzD,MAAM,WAAW,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC;IAExC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,OAAO,0BAA0B,CAAC;IACnC,CAAC;IAED,OAAO,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAAA,CAC9B;AAED,SAAS,iBAAiB,CACzB,aAAqB,EACrB,SAAiB,EACjB,cAAsB,EACtB,aAA4B,EACnB;IACT,MAAM,WAAW,GAAG,GAAG,aAAa,IAAI,SAAS,EAAE,CAAC;IACpD,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,KAAK,QAAQ,CAAC;IAEjD,MAAM,cAAc,GAAG,QAAQ;QAC9B,CAAC,CAAC;;;4CAGwC;QAC1C,CAAC,CAAC;;6CAEyC,CAAC;IAE7C,OAAO;;;;;;;;;;;;;;;;;EAiBN,cAAc;;;6BAGa,WAAW;;;MAGlC,WAAW;;;;qBAII,WAAW;4BACJ,WAAW;;;;;;;EAOrC,cAAc;;;;;;;;;;;;;;;;;;;;CAoBf,CAAC;AAAA,CACD;AAED,SAAS,QAAQ,CAAC,IAAY,EAAE,MAAc,EAAU;IACvD,IAAI,IAAI,CAAC,MAAM,IAAI,MAAM;QAAE,OAAO,IAAI,CAAC;IACvC,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC;AAAA,CAC7C;AAED,MAAM,UAAU,iBAAiB,CAAC,aAA4B,EAAe;IAC5E,IAAI,KAAK,GAAiB,IAAI,CAAC;IAC/B,MAAM,QAAQ,GAAG,cAAc,CAAC,aAAa,CAAC,CAAC;IAE/C,OAAO;QACN,KAAK,CAAC,GAAG,CAAC,GAAiB,EAAE,UAAkB,EAAE,KAAmB,EAAiB;YACpF,kCAAkC;YAClC,MAAM,KAAK,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAE7C,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC;YACtC,MAAM,aAAa,GAAG,QAAQ,CAAC,gBAAgB,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,SAAS,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;YACzF,MAAM,cAAc,GAAG,iBAAiB,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;YACzD,MAAM,YAAY,GAAG,iBAAiB,CAAC,aAAa,EAAE,SAAS,EAAE,cAAc,EAAE,aAAa,CAAC,CAAC;YAEhG,kDAAkD;YAClD,sDAAsD;YACtD,iBAAiB,CAAC,KAAK,EAAE,QAAgB,EAAE,KAAc,EAAE,EAAE,CAAC;gBAC7D,MAAM,QAAQ,GAAG,mBAAmB,CAAC,QAAQ,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,CAAC,CAAC;gBACrF,MAAM,GAAG,CAAC,UAAU,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;YAAA,CACtC,CAAC,CAAC;YAEH,6BAA6B;YAC7B,MAAM,KAAK,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;YAEvC,yBAAyB;YACzB,KAAK,GAAG,IAAI,KAAK,CAAC;gBACjB,YAAY,EAAE;oBACb,YAAY;oBACZ,KAAK;oBACL,aAAa,EAAE,KAAK;oBACpB,KAAK;iBACL;gBACD,SAAS,EAAE,IAAI,iBAAiB,CAAC;oBAChC,SAAS,EAAE,KAAK,IAAI,EAAE,CAAC,kBAAkB,EAAE;iBAC3C,CAAC;aACF,CAAC,CAAC;YAEH,qDAAqD;YACrD,MAAM,YAAY,GAAG,IAAI,GAAG,EAA+C,CAAC;YAE5E,sBAAsB;YACtB,KAAK,CAAC,SAAS,CAAC,KAAK,EAAE,KAAiB,EAAE,EAAE,CAAC;gBAC5C,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;oBACpB,KAAK,sBAAsB,EAAE,CAAC;wBAC7B,MAAM,IAAI,GAAG,KAAK,CAAC,IAA0B,CAAC;wBAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,QAAQ,CAAC;wBAE3C,uCAAuC;wBACvC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,EAAE,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;wBAEnF,iBAAiB;wBACjB,OAAO,CAAC,GAAG,CAAC,YAAY,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;wBAEzE,eAAe;wBACf,MAAM,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE;4BAC3C,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;4BACzB,IAAI,EAAE,KAAK;4BACX,IAAI,EAAE,UAAU,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE;4BAC/D,WAAW,EAAE,EAAE;4BACf,KAAK,EAAE,IAAI;yBACX,CAAC,CAAC;wBAEH,kCAAkC;wBAClC,MAAM,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC;wBAChC,MAAM;oBACP,CAAC;oBAED,KAAK,oBAAoB,EAAE,CAAC;wBAC3B,MAAM,SAAS,GAAG,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;wBACjG,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;wBACnD,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;wBAEtC,iBAAiB;wBACjB,OAAO,CAAC,GAAG,CAAC,iBAAiB,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;wBAE7F,eAAe;wBACf,MAAM,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE;4BAC3C,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;4BACzB,IAAI,EAAE,KAAK;4BACX,IAAI,EAAE,iBAAiB,KAAK,CAAC,QAAQ,KAAK,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE;4BACtG,WAAW,EAAE,EAAE;4BACf,KAAK,EAAE,IAAI;yBACX,CAAC,CAAC;wBAEH,wCAAwC;wBACxC,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC;wBACrF,MAAM,YAAY,GAAG,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;wBAC/C,MAAM,GAAG,CAAC,eAAe,CACxB,KAAK,KAAK,CAAC,QAAQ,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAG,CAAC,CAAC,CAAC,KAAG,IAAI;4BACrD,OAAO;4BACP,OAAO;4BACP,SAAS;4BACT,kBAAkB;4BAClB,YAAY;4BACZ,OAAO,CACR,CAAC;wBAEF,6CAA6C;wBAC7C,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;4BACnB,MAAM,GAAG,CAAC,OAAO,CAAC,WAAW,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;wBAC3D,CAAC;wBACD,MAAM;oBACP,CAAC;oBAED,KAAK,gBAAgB,EAAE,CAAC;wBACvB,MAAM,EAAE,GAAG,KAAK,CAAC,qBAAqB,CAAC;wBACvC,2BAA2B;wBAC3B,IAAI,EAAE,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;4BAC9B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;wBAChC,CAAC;6BAAM,IAAI,EAAE,CAAC,IAAI,KAAK,gBAAgB,EAAE,CAAC;4BACzC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;wBAChC,CAAC;wBACD,MAAM;oBACP,CAAC;oBAED,KAAK,eAAe;wBACnB,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;4BACxC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;wBAC5B,CAAC;wBACD,MAAM;oBAEP,KAAK,aAAa;wBACjB,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;4BACxC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;4BAC3B,sCAAsC;4BACtC,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;4BACtC,IAAI,IAAI,GAAG,EAAE,CAAC;4BACd,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;gCAC5B,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;oCAC1B,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC;gCACnB,CAAC;4BACF,CAAC;4BACD,IAAI,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;gCACjB,MAAM,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;4BACzB,CAAC;wBACF,CAAC;wBACD,MAAM;gBACR,CAAC;YAAA,CACD,CAAC,CAAC;YAEH,oCAAoC;YACpC,MAAM,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,kBAAkB,CAAC,CAAC;QAAA,CAC3D;QAED,KAAK,GAAS;YACb,KAAK,EAAE,KAAK,EAAE,CAAC;QAAA,CACf;KACD,CAAC;AAAA,CACF;AAED;;GAEG;AACH,SAAS,mBAAmB,CAC3B,aAAqB,EACrB,UAAkB,EAClB,aAAqB,EACrB,SAAiB,EACR;IACT,IAAI,aAAa,KAAK,YAAY,EAAE,CAAC;QACpC,gEAAgE;QAChE,MAAM,MAAM,GAAG,cAAc,SAAS,GAAG,CAAC;QAC1C,IAAI,aAAa,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YACtC,OAAO,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;QAC7D,CAAC;QACD,iCAAiC;QACjC,IAAI,aAAa,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;YAC7C,OAAO,IAAI,CAAC,UAAU,EAAE,IAAI,EAAE,aAAa,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;QAC1E,CAAC;IACF,CAAC;IACD,mCAAmC;IACnC,OAAO,aAAa,CAAC;AAAA,CACrB","sourcesContent":["import { Agent, type AgentEvent, ProviderTransport } from \"@mariozechner/pi-agent-core\";\nimport { getModel } from \"@mariozechner/pi-ai\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { mkdir } from \"fs/promises\";\nimport { join } from \"path\";\n\nimport { createExecutor, type SandboxConfig } from \"./sandbox.js\";\nimport type { SlackContext } from \"./slack.js\";\nimport type { ChannelStore } from \"./store.js\";\nimport { createMomTools, setUploadFunction } from \"./tools/index.js\";\n\n// Hardcoded model for now\nconst model = getModel(\"anthropic\", \"claude-opus-4-5\");\n\nexport interface AgentRunner {\n\trun(ctx: SlackContext, channelDir: string, store: ChannelStore): Promise<void>;\n\tabort(): void;\n}\n\nfunction getAnthropicApiKey(): string {\n\tconst key = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;\n\tif (!key) {\n\t\tthrow new Error(\"ANTHROPIC_OAUTH_TOKEN or ANTHROPIC_API_KEY must be set\");\n\t}\n\treturn key;\n}\n\nfunction getRecentMessages(channelDir: string, count: number): string {\n\tconst logPath = join(channelDir, \"log.jsonl\");\n\tif (!existsSync(logPath)) {\n\t\treturn \"(no message history yet)\";\n\t}\n\n\tconst content = readFileSync(logPath, \"utf-8\");\n\tconst lines = content.trim().split(\"\\n\").filter(Boolean);\n\tconst recentLines = lines.slice(-count);\n\n\tif (recentLines.length === 0) {\n\t\treturn \"(no message history yet)\";\n\t}\n\n\treturn recentLines.join(\"\\n\");\n}\n\nfunction buildSystemPrompt(\n\tworkspacePath: string,\n\tchannelId: string,\n\trecentMessages: string,\n\tsandboxConfig: SandboxConfig,\n): string {\n\tconst channelPath = `${workspacePath}/${channelId}`;\n\tconst isDocker = sandboxConfig.type === \"docker\";\n\n\tconst envDescription = isDocker\n\t\t? `You are running inside a Docker container (Alpine Linux).\n- Install tools with: apk add <package>\n- Your changes persist across sessions\n- You have full control over this container`\n\t\t: `You are running directly on the host machine.\n- Be careful with system modifications\n- Use the system's package manager if needed`;\n\n\treturn `You are mom, a helpful Slack bot assistant.\n\n## Communication Style\n- Be concise and professional\n- Do not use emojis unless the user communicates informally with you\n- Get to the point quickly\n- If you need clarification, ask directly\n- Use Slack's mrkdwn format (NOT standard Markdown):\n - Bold: *text* (single asterisks)\n - Italic: _text_\n - Strikethrough: ~text~\n - Code: \\`code\\`\n - Code block: \\`\\`\\`code\\`\\`\\`\n - Links: <url|text>\n - Do NOT use **double asterisks** or [markdown](links)\n\n## Your Environment\n${envDescription}\n\n## Your Workspace\nYour working directory is: ${channelPath}\n\n### Scratchpad\nUse ${channelPath}/scratch/ for temporary work like cloning repos, generating files, etc.\nThis directory persists across conversations, so you can reference previous work.\n\n### Channel Data (read-only, managed by the system)\n- Message history: ${channelPath}/log.jsonl\n- Attachments from users: ${channelPath}/attachments/\n\nYou can:\n- Configure tools and save credentials in your home directory\n- Create files and directories in your scratchpad\n\n### Recent Messages (last 50)\n${recentMessages}\n\n## Tools\nYou have access to: bash, read, edit, write, attach tools.\n- bash: Run shell commands (this is your main tool)\n- read: Read files\n- edit: Edit files surgically\n- write: Create/overwrite files\n- attach: Share a file with the user in Slack\n\nEach tool requires a \"label\" parameter - brief description shown to the user.\n\n## Guidelines\n- Be concise and helpful\n- Use bash for most operations\n- If you need a tool, install it\n- If you need credentials, ask the user\n\n## CRITICAL\n- DO NOT USE EMOJIS. KEEP YOUR RESPONSES AS SHORT AS POSSIBLE.\n`;\n}\n\nfunction truncate(text: string, maxLen: number): string {\n\tif (text.length <= maxLen) return text;\n\treturn text.substring(0, maxLen - 3) + \"...\";\n}\n\nexport function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {\n\tlet agent: Agent | null = null;\n\tconst executor = createExecutor(sandboxConfig);\n\n\treturn {\n\t\tasync run(ctx: SlackContext, channelDir: string, store: ChannelStore): Promise<void> {\n\t\t\t// Ensure channel directory exists\n\t\t\tawait mkdir(channelDir, { recursive: true });\n\n\t\t\tconst channelId = ctx.message.channel;\n\t\t\tconst workspacePath = executor.getWorkspacePath(channelDir.replace(`/${channelId}`, \"\"));\n\t\t\tconst recentMessages = getRecentMessages(channelDir, 50);\n\t\t\tconst systemPrompt = buildSystemPrompt(workspacePath, channelId, recentMessages, sandboxConfig);\n\n\t\t\t// Set up file upload function for the attach tool\n\t\t\t// For Docker, we need to translate paths back to host\n\t\t\tsetUploadFunction(async (filePath: string, title?: string) => {\n\t\t\t\tconst hostPath = translateToHostPath(filePath, channelDir, workspacePath, channelId);\n\t\t\t\tawait ctx.uploadFile(hostPath, title);\n\t\t\t});\n\n\t\t\t// Create tools with executor\n\t\t\tconst tools = createMomTools(executor);\n\n\t\t\t// Create ephemeral agent\n\t\t\tagent = new Agent({\n\t\t\t\tinitialState: {\n\t\t\t\t\tsystemPrompt,\n\t\t\t\t\tmodel,\n\t\t\t\t\tthinkingLevel: \"off\",\n\t\t\t\t\ttools,\n\t\t\t\t},\n\t\t\t\ttransport: new ProviderTransport({\n\t\t\t\t\tgetApiKey: async () => getAnthropicApiKey(),\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\t// Track pending tool calls to pair args with results\n\t\t\tconst pendingTools = new Map<string, { toolName: string; args: unknown }>();\n\n\t\t\t// Subscribe to events\n\t\t\tagent.subscribe(async (event: AgentEvent) => {\n\t\t\t\tswitch (event.type) {\n\t\t\t\t\tcase \"tool_execution_start\": {\n\t\t\t\t\t\tconst args = event.args as { label?: string };\n\t\t\t\t\t\tconst label = args.label || event.toolName;\n\n\t\t\t\t\t\t// Store args to pair with result later\n\t\t\t\t\t\tpendingTools.set(event.toolCallId, { toolName: event.toolName, args: event.args });\n\n\t\t\t\t\t\t// Log to console\n\t\t\t\t\t\tconsole.log(`\\n[Tool] ${event.toolName}: ${JSON.stringify(event.args)}`);\n\n\t\t\t\t\t\t// Log to jsonl\n\t\t\t\t\t\tawait store.logMessage(ctx.message.channel, {\n\t\t\t\t\t\t\tts: Date.now().toString(),\n\t\t\t\t\t\t\tuser: \"bot\",\n\t\t\t\t\t\t\ttext: `[Tool] ${event.toolName}: ${JSON.stringify(event.args)}`,\n\t\t\t\t\t\t\tattachments: [],\n\t\t\t\t\t\t\tisBot: true,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Show label in main message only\n\t\t\t\t\t\tawait ctx.respond(`_${label}_`);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"tool_execution_end\": {\n\t\t\t\t\t\tconst resultStr = typeof event.result === \"string\" ? event.result : JSON.stringify(event.result);\n\t\t\t\t\t\tconst pending = pendingTools.get(event.toolCallId);\n\t\t\t\t\t\tpendingTools.delete(event.toolCallId);\n\n\t\t\t\t\t\t// Log to console\n\t\t\t\t\t\tconsole.log(`[Tool Result] ${event.isError ? \"ERROR: \" : \"\"}${truncate(resultStr, 1000)}\\n`);\n\n\t\t\t\t\t\t// Log to jsonl\n\t\t\t\t\t\tawait store.logMessage(ctx.message.channel, {\n\t\t\t\t\t\t\tts: Date.now().toString(),\n\t\t\t\t\t\t\tuser: \"bot\",\n\t\t\t\t\t\t\ttext: `[Tool Result] ${event.toolName}: ${event.isError ? \"ERROR: \" : \"\"}${truncate(resultStr, 1000)}`,\n\t\t\t\t\t\t\tattachments: [],\n\t\t\t\t\t\t\tisBot: true,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Post args + result together in thread\n\t\t\t\t\t\tconst argsStr = pending ? JSON.stringify(pending.args, null, 2) : \"(args not found)\";\n\t\t\t\t\t\tconst threadResult = truncate(resultStr, 2000);\n\t\t\t\t\t\tawait ctx.respondInThread(\n\t\t\t\t\t\t\t`*[${event.toolName}]* ${event.isError ? \"❌\" : \"✓\"}\\n` +\n\t\t\t\t\t\t\t\t\"```\\n\" +\n\t\t\t\t\t\t\t\targsStr +\n\t\t\t\t\t\t\t\t\"\\n```\\n\" +\n\t\t\t\t\t\t\t\t\"*Result:*\\n```\\n\" +\n\t\t\t\t\t\t\t\tthreadResult +\n\t\t\t\t\t\t\t\t\"\\n```\",\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// Show brief error in main message if failed\n\t\t\t\t\t\tif (event.isError) {\n\t\t\t\t\t\t\tawait ctx.respond(`_Error: ${truncate(resultStr, 200)}_`);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"message_update\": {\n\t\t\t\t\t\tconst ev = event.assistantMessageEvent;\n\t\t\t\t\t\t// Stream deltas to console\n\t\t\t\t\t\tif (ev.type === \"text_delta\") {\n\t\t\t\t\t\t\tprocess.stdout.write(ev.delta);\n\t\t\t\t\t\t} else if (ev.type === \"thinking_delta\") {\n\t\t\t\t\t\t\tprocess.stdout.write(ev.delta);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"message_start\":\n\t\t\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\t\t\tprocess.stdout.write(\"\\n\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase \"message_end\":\n\t\t\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\t\t\tprocess.stdout.write(\"\\n\");\n\t\t\t\t\t\t\t// Extract text from assistant message\n\t\t\t\t\t\t\tconst content = event.message.content;\n\t\t\t\t\t\t\tlet text = \"\";\n\t\t\t\t\t\t\tfor (const part of content) {\n\t\t\t\t\t\t\t\tif (part.type === \"text\") {\n\t\t\t\t\t\t\t\t\ttext += part.text;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (text.trim()) {\n\t\t\t\t\t\t\t\tawait ctx.respond(text);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Run the agent with user's message\n\t\t\tawait agent.prompt(ctx.message.text || \"(attached files)\");\n\t\t},\n\n\t\tabort(): void {\n\t\t\tagent?.abort();\n\t\t},\n\t};\n}\n\n/**\n * Translate container path back to host path for file operations\n */\nfunction translateToHostPath(\n\tcontainerPath: string,\n\tchannelDir: string,\n\tworkspacePath: string,\n\tchannelId: string,\n): string {\n\tif (workspacePath === \"/workspace\") {\n\t\t// Docker mode - translate /workspace/channelId/... to host path\n\t\tconst prefix = `/workspace/${channelId}/`;\n\t\tif (containerPath.startsWith(prefix)) {\n\t\t\treturn join(channelDir, containerPath.slice(prefix.length));\n\t\t}\n\t\t// Maybe it's just /workspace/...\n\t\tif (containerPath.startsWith(\"/workspace/\")) {\n\t\t\treturn join(channelDir, \"..\", containerPath.slice(\"/workspace/\".length));\n\t\t}\n\t}\n\t// Host mode or already a host path\n\treturn containerPath;\n}\n"]}
package/dist/main.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=main.d.ts.map
@@ -0,0 +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"]}
package/dist/main.js ADDED
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ import { join, resolve } from "path";
3
+ import { createAgentRunner } from "./agent.js";
4
+ import { parseSandboxArg, validateSandbox } from "./sandbox.js";
5
+ import { MomBot } from "./slack.js";
6
+ const MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;
7
+ const MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;
8
+ const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
9
+ const ANTHROPIC_OAUTH_TOKEN = process.env.ANTHROPIC_OAUTH_TOKEN;
10
+ // Parse command line arguments
11
+ function parseArgs() {
12
+ const args = process.argv.slice(2);
13
+ let sandbox = { type: "host" };
14
+ let workingDir;
15
+ for (let i = 0; i < args.length; i++) {
16
+ const arg = args[i];
17
+ if (arg.startsWith("--sandbox=")) {
18
+ sandbox = parseSandboxArg(arg.slice("--sandbox=".length));
19
+ }
20
+ else if (arg === "--sandbox") {
21
+ const next = args[++i];
22
+ if (!next) {
23
+ console.error("Error: --sandbox requires a value (host or docker:<container-name>)");
24
+ process.exit(1);
25
+ }
26
+ sandbox = parseSandboxArg(next);
27
+ }
28
+ else if (!arg.startsWith("-")) {
29
+ workingDir = arg;
30
+ }
31
+ else {
32
+ console.error(`Unknown option: ${arg}`);
33
+ process.exit(1);
34
+ }
35
+ }
36
+ if (!workingDir) {
37
+ console.error("Usage: mom [--sandbox=host|docker:<container-name>] <working-directory>");
38
+ console.error("");
39
+ console.error("Options:");
40
+ console.error(" --sandbox=host Run tools directly on host (default)");
41
+ console.error(" --sandbox=docker:<container> Run tools in Docker container");
42
+ console.error("");
43
+ console.error("Examples:");
44
+ console.error(" mom ./data");
45
+ console.error(" mom --sandbox=docker:mom-sandbox ./data");
46
+ process.exit(1);
47
+ }
48
+ return { workingDir: resolve(workingDir), sandbox };
49
+ }
50
+ 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}`}`);
54
+ if (!MOM_SLACK_APP_TOKEN || !MOM_SLACK_BOT_TOKEN || (!ANTHROPIC_API_KEY && !ANTHROPIC_OAUTH_TOKEN)) {
55
+ console.error("Missing required environment variables:");
56
+ if (!MOM_SLACK_APP_TOKEN)
57
+ console.error(" - MOM_SLACK_APP_TOKEN (xapp-...)");
58
+ if (!MOM_SLACK_BOT_TOKEN)
59
+ console.error(" - MOM_SLACK_BOT_TOKEN (xoxb-...)");
60
+ if (!ANTHROPIC_API_KEY && !ANTHROPIC_OAUTH_TOKEN)
61
+ console.error(" - ANTHROPIC_API_KEY or ANTHROPIC_OAUTH_TOKEN");
62
+ process.exit(1);
63
+ }
64
+ // Validate sandbox configuration
65
+ await validateSandbox(sandbox);
66
+ // Track active agent runs per channel
67
+ const activeRuns = new Map();
68
+ async function handleMessage(ctx, source) {
69
+ const channelId = ctx.message.channel;
70
+ const messageText = ctx.message.text.toLowerCase().trim();
71
+ // Check for stop command
72
+ if (messageText === "stop") {
73
+ const runner = activeRuns.get(channelId);
74
+ if (runner) {
75
+ console.log(`Stop requested for channel ${channelId}`);
76
+ runner.abort();
77
+ await ctx.respond("_Stopping..._");
78
+ }
79
+ else {
80
+ await ctx.respond("_Nothing running._");
81
+ }
82
+ return;
83
+ }
84
+ // Check if already running in this channel
85
+ if (activeRuns.has(channelId)) {
86
+ await ctx.respond("_Already working on something. Say `@mom stop` to cancel._");
87
+ return;
88
+ }
89
+ console.log(`${source === "channel" ? "Channel mention" : "DM"} from <@${ctx.message.user}>: ${ctx.message.text}`);
90
+ const channelDir = join(workingDir, channelId);
91
+ const runner = createAgentRunner(sandbox);
92
+ activeRuns.set(channelId, runner);
93
+ 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}`);
106
+ }
107
+ }
108
+ finally {
109
+ activeRuns.delete(channelId);
110
+ }
111
+ }
112
+ const bot = new MomBot({
113
+ async onChannelMention(ctx) {
114
+ await handleMessage(ctx, "channel");
115
+ },
116
+ async onDirectMessage(ctx) {
117
+ await handleMessage(ctx, "dm");
118
+ },
119
+ }, {
120
+ appToken: MOM_SLACK_APP_TOKEN,
121
+ botToken: MOM_SLACK_BOT_TOKEN,
122
+ workingDir,
123
+ });
124
+ bot.start();
125
+ //# sourceMappingURL=main.js.map