From 30964e0c33da5dc984b7547807358185ad9d281a Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 26 Nov 2025 01:48:41 +0100 Subject: [PATCH] mom: Thread-based tool details, improved README, fixed message ordering --- packages/mom/README.md | 182 +++++++++++++++++++++++++++++++++++++- packages/mom/src/agent.ts | 25 +++++- packages/mom/src/slack.ts | 22 ++++- 3 files changed, 224 insertions(+), 5 deletions(-) diff --git a/packages/mom/README.md b/packages/mom/README.md index e54561ce..0c3d3b70 100644 --- a/packages/mom/README.md +++ b/packages/mom/README.md @@ -1,3 +1,183 @@ # @mariozechner/pi-mom -Slack bot that delegates channel messages to a pi coding agent instance. +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. + +## Features + +- **Slack Integration**: Responds to @mentions in channels and DMs +- **Full Bash Access**: Execute any command, install tools, configure credentials +- **File Operations**: Read, write, and edit files +- **Docker Sandbox**: Optional isolation to protect your host machine +- **Persistent Workspace**: Each channel gets its own workspace that persists across conversations +- **Thread-Based Details**: Clean main messages with verbose tool details in threads + +## Installation + +```bash +npm install @mariozechner/pi-mom +``` + +## Quick Start + +```bash +# Set environment variables +export MOM_SLACK_APP_TOKEN=xapp-... +export MOM_SLACK_BOT_TOKEN=xoxb-... +export ANTHROPIC_API_KEY=sk-ant-... +# or use your Claude Pro/Max subscription +# to get the token install Claude Code and run claude setup-token +export ANTHROPIC_OAUTH_TOKEN=sk-ant-... + +# Run mom +mom ./data +``` + +## Slack App Setup + +1. Create a new Slack app at https://api.slack.com/apps +2. Enable **Socket Mode** (Settings → Socket Mode → Enable) +3. Generate an **App-Level Token** with `connections:write` scope → this is `MOM_SLACK_APP_TOKEN` +4. Add **Bot Token Scopes** (OAuth & Permissions): + - `app_mentions:read` + - `channels:history` + - `channels:read` + - `chat:write` + - `files:read` + - `files:write` + - `im:history` + - `im:read` + - `im:write` + - `users:read` +5. **Subscribe to Bot Events** (Event Subscriptions): + - `app_mention` + - `message.channels` + - `message.im` +6. Install the app to your workspace → get the **Bot User OAuth Token** → this is `MOM_SLACK_BOT_TOKEN` + +## Usage + +### Host Mode (Default) + +Run tools directly on your machine: + +```bash +mom ./data +``` + +### Docker Sandbox Mode + +Isolate mom in a container to protect your host: + +```bash +# Create the sandbox container +./docker.sh create ./data + +# Run mom with sandbox +mom --sandbox=docker:mom-sandbox ./data +``` + +### Talking to Mom + +In Slack: +``` +@mom what's in the current directory? +@mom clone the repo https://github.com/example/repo and find all TODO comments +@mom install htop and show me system stats +``` + +Mom will: +1. Show brief status updates in the main message +2. Post detailed tool calls and results in a thread +3. Provide a final response + +### Stopping Mom + +If mom is working on something and you need to stop: +``` +@mom stop +``` + +## CLI Options + +```bash +mom [options] + +Options: + --sandbox=host Run tools on host (default) + --sandbox=docker: Run tools in Docker container +``` + +## Docker Sandbox + +The Docker sandbox treats the container as mom's personal computer: + +- **Persistent**: Install tools with `apk add`, configure credentials - changes persist +- **Isolated**: Mom can only access `/workspace` (your data directory) +- **Self-Managing**: Mom can install what she needs and ask for credentials + +### Container Management + +```bash +./docker.sh create # Create and start container +./docker.sh start # Start existing container +./docker.sh stop # Stop container +./docker.sh remove # Remove container +./docker.sh status # Check if running +./docker.sh shell # Open shell in container +``` + +### Example Flow + +``` +User: @mom check the spine-runtimes repo on GitHub +Mom: I need gh CLI. Installing... + (runs: apk add github-cli) +Mom: I need a GitHub token. Please provide one. +User: ghp_xxxx... +Mom: (configures gh auth) +Mom: Done. Here's the repo info... +``` + +## Workspace Structure + +Each Slack channel gets its own workspace: + +``` +./data/ + └── C123ABC/ # Channel ID + ├── log.jsonl # Message history (managed by mom) + ├── attachments/ # Files shared in channel + └── scratch/ # Mom's working directory +``` + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `MOM_SLACK_APP_TOKEN` | Slack app-level token (xapp-...) | +| `MOM_SLACK_BOT_TOKEN` | Slack bot token (xoxb-...) | +| `ANTHROPIC_API_KEY` | Anthropic API key | +| `ANTHROPIC_OAUTH_TOKEN` | Alternative: Anthropic OAuth token | + +## Security Considerations + +**Host Mode**: Mom has full access to your machine. Only use in trusted environments. + +**Docker Mode**: Mom is isolated to the container. She can: +- Read/write files in `/workspace` (your data dir) +- Make network requests +- Install packages in the container + +She cannot: +- Access files outside `/workspace` +- Access your host credentials +- Affect your host system + +**Recommendations**: +1. Use Docker mode for shared Slack workspaces +2. Create a dedicated GitHub bot account with limited repo access +3. Only share necessary credentials with mom + +## License + +MIT diff --git a/packages/mom/src/agent.ts b/packages/mom/src/agent.ts index dcea457a..b6d27bac 100644 --- a/packages/mom/src/agent.ts +++ b/packages/mom/src/agent.ts @@ -160,6 +160,9 @@ export function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner { }), }); + // Track pending tool calls to pair args with results + const pendingTools = new Map(); + // Subscribe to events agent.subscribe(async (event: AgentEvent) => { switch (event.type) { @@ -167,6 +170,9 @@ export function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner { const args = event.args as { label?: string }; const label = args.label || event.toolName; + // Store args to pair with result later + pendingTools.set(event.toolCallId, { toolName: event.toolName, args: event.args }); + // Log to console console.log(`\n[Tool] ${event.toolName}: ${JSON.stringify(event.args)}`); @@ -179,13 +185,15 @@ export function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner { isBot: true, }); - // Show only label to user (italic) + // Show label in main message only await ctx.respond(`_${label}_`); break; } case "tool_execution_end": { const resultStr = typeof event.result === "string" ? event.result : JSON.stringify(event.result); + const pending = pendingTools.get(event.toolCallId); + pendingTools.delete(event.toolCallId); // Log to console console.log(`[Tool Result] ${event.isError ? "ERROR: " : ""}${truncate(resultStr, 1000)}\n`); @@ -199,7 +207,20 @@ export function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner { isBot: true, }); - // Show brief status to user (only on error) + // Post args + result together in thread + const argsStr = pending ? JSON.stringify(pending.args, null, 2) : "(args not found)"; + const threadResult = truncate(resultStr, 2000); + await ctx.respondInThread( + `*[${event.toolName}]* ${event.isError ? "❌" : "✓"}\n` + + "```\n" + + argsStr + + "\n```\n" + + "*Result:*\n```\n" + + threadResult + + "\n```", + ); + + // Show brief error in main message if failed if (event.isError) { await ctx.respond(`_Error: ${truncate(resultStr, 200)}_`); } diff --git a/packages/mom/src/slack.ts b/packages/mom/src/slack.ts index 4146e852..d6e5a4f4 100644 --- a/packages/mom/src/slack.ts +++ b/packages/mom/src/slack.ts @@ -16,9 +16,11 @@ export interface SlackMessage { export interface SlackContext { message: SlackMessage; store: ChannelStore; - /** Send a new message */ + /** Send/update the main message (accumulates text) */ respond(text: string): Promise; - /** Show/hide typing indicator. If text is provided to respond() after setTyping(true), it updates the typing message instead of posting new. */ + /** Post a message in the thread under the main message (for verbose details) */ + respondInThread(text: string): Promise; + /** Show/hide typing indicator */ setTyping(isTyping: boolean): Promise; /** Upload a file to the channel */ uploadFile(filePath: string, title?: string): Promise; @@ -227,6 +229,22 @@ export class MomBot { await updatePromise; }, + respondInThread: async (threadText: string) => { + // Queue thread posts to maintain order + updatePromise = updatePromise.then(async () => { + if (!messageTs) { + // No main message yet, just skip + return; + } + // Post in thread under the main message + await this.webClient.chat.postMessage({ + channel: event.channel, + thread_ts: messageTs, + text: threadText, + }); + }); + await updatePromise; + }, setTyping: async (isTyping: boolean) => { if (isTyping && !messageTs) { // Post initial "thinking" message