@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.
- package/CHANGELOG.md +17 -0
- package/README.md +183 -0
- package/dist/agent.d.ts +9 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +248 -0
- package/dist/agent.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +125 -0
- package/dist/main.js.map +1 -0
- package/dist/sandbox.d.ts +34 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +183 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/slack.d.ts +46 -0
- package/dist/slack.d.ts.map +1 -0
- package/dist/slack.js +208 -0
- package/dist/slack.js.map +1 -0
- package/dist/store.d.ts +52 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +124 -0
- package/dist/store.js.map +1 -0
- package/dist/tools/attach.d.ts +10 -0
- package/dist/tools/attach.d.ts.map +1 -0
- package/dist/tools/attach.js +34 -0
- package/dist/tools/attach.js.map +1 -0
- package/dist/tools/bash.d.ts +10 -0
- package/dist/tools/bash.d.ts.map +1 -0
- package/dist/tools/bash.js +30 -0
- package/dist/tools/bash.js.map +1 -0
- package/dist/tools/edit.d.ts +11 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +131 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +16 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/read.d.ts +11 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +102 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/write.d.ts +10 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +33 -0
- package/dist/tools/write.js.map +1 -0
- 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
|
package/dist/agent.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|