@pollit/twin-dev-bot 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +415 -0
- package/bin/twindevbot.js +22 -0
- package/dist/action-payload-store.d.ts +22 -0
- package/dist/action-payload-store.js +54 -0
- package/dist/active-runners.d.ts +44 -0
- package/dist/active-runners.js +114 -0
- package/dist/channel-store.d.ts +16 -0
- package/dist/channel-store.js +91 -0
- package/dist/claude/active-runners.d.ts +44 -0
- package/dist/claude/active-runners.js +114 -0
- package/dist/claude/claude-runner.d.ts +57 -0
- package/dist/claude/claude-runner.js +210 -0
- package/dist/claude/session-manager.d.ts +62 -0
- package/dist/claude/session-manager.js +247 -0
- package/dist/claude-runner.d.ts +57 -0
- package/dist/claude-runner.js +210 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +271 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +49 -0
- package/dist/conversation-store.d.ts +53 -0
- package/dist/conversation-store.js +173 -0
- package/dist/core/config.d.ts +9 -0
- package/dist/core/config.js +49 -0
- package/dist/core/logger.d.ts +34 -0
- package/dist/core/logger.js +110 -0
- package/dist/core/paths.d.ts +11 -0
- package/dist/core/paths.js +18 -0
- package/dist/core/platform.d.ts +18 -0
- package/dist/core/platform.js +33 -0
- package/dist/daemon/index.d.ts +3 -0
- package/dist/daemon/index.js +14 -0
- package/dist/daemon/macos.d.ts +8 -0
- package/dist/daemon/macos.js +150 -0
- package/dist/daemon/types.d.ts +9 -0
- package/dist/daemon/types.js +1 -0
- package/dist/daemon/windows.d.ts +8 -0
- package/dist/daemon/windows.js +137 -0
- package/dist/handlers/claude-command.d.ts +2 -0
- package/dist/handlers/claude-command.js +634 -0
- package/dist/handlers/claude-runner-setup.d.ts +16 -0
- package/dist/handlers/claude-runner-setup.js +445 -0
- package/dist/handlers/index.d.ts +3 -0
- package/dist/handlers/index.js +3 -0
- package/dist/handlers/init-handlers.d.ts +2 -0
- package/dist/handlers/init-handlers.js +189 -0
- package/dist/handlers/question-handlers.d.ts +2 -0
- package/dist/handlers/question-handlers.js +835 -0
- package/dist/i18n/en.d.ts +150 -0
- package/dist/i18n/en.js +163 -0
- package/dist/i18n/index.d.ts +20 -0
- package/dist/i18n/index.js +31 -0
- package/dist/i18n/ko.d.ts +1 -0
- package/dist/i18n/ko.js +141 -0
- package/dist/logger.d.ts +34 -0
- package/dist/logger.js +110 -0
- package/dist/multi-select-state.d.ts +58 -0
- package/dist/multi-select-state.js +151 -0
- package/dist/paths.d.ts +11 -0
- package/dist/paths.js +18 -0
- package/dist/pending-questions.d.ts +53 -0
- package/dist/pending-questions.js +139 -0
- package/dist/platform.d.ts +18 -0
- package/dist/platform.js +33 -0
- package/dist/progress-tracker.d.ts +47 -0
- package/dist/progress-tracker.js +218 -0
- package/dist/question-blocks.d.ts +27 -0
- package/dist/question-blocks.js +235 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +83 -0
- package/dist/session-manager.d.ts +62 -0
- package/dist/session-manager.js +247 -0
- package/dist/setup.d.ts +5 -0
- package/dist/setup.js +132 -0
- package/dist/slack/progress-tracker.d.ts +47 -0
- package/dist/slack/progress-tracker.js +218 -0
- package/dist/slack/question-blocks.d.ts +27 -0
- package/dist/slack/question-blocks.js +235 -0
- package/dist/stores/action-payload-store.d.ts +22 -0
- package/dist/stores/action-payload-store.js +54 -0
- package/dist/stores/channel-store.d.ts +16 -0
- package/dist/stores/channel-store.js +91 -0
- package/dist/stores/multi-select-state.d.ts +58 -0
- package/dist/stores/multi-select-state.js +151 -0
- package/dist/stores/pending-questions.d.ts +53 -0
- package/dist/stores/pending-questions.js +139 -0
- package/dist/stores/workspace-store.d.ts +27 -0
- package/dist/stores/workspace-store.js +160 -0
- package/dist/templates.d.ts +23 -0
- package/dist/templates.js +292 -0
- package/dist/types/claude-stream.d.ts +116 -0
- package/dist/types/claude-stream.js +3 -0
- package/dist/types/conversation.d.ts +16 -0
- package/dist/types/conversation.js +4 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/types/slack.d.ts +51 -0
- package/dist/types/slack.js +1 -0
- package/dist/utils/display-width.d.ts +8 -0
- package/dist/utils/display-width.js +33 -0
- package/dist/utils/safe-async.d.ts +6 -0
- package/dist/utils/safe-async.js +14 -0
- package/dist/utils/slack-message.d.ts +73 -0
- package/dist/utils/slack-message.js +220 -0
- package/dist/utils/slack-rate-limit.d.ts +5 -0
- package/dist/utils/slack-rate-limit.js +49 -0
- package/dist/workspace-store.d.ts +27 -0
- package/dist/workspace-store.js +160 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
# TwinDevBot
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="logo.png" alt="TwinDevBot" width="150" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
A **Slack bot** that lets you develop with **Claude Code** through Slack conversations — **from anywhere**.
|
|
8
|
+
|
|
9
|
+
TwinDevBot connects your Slack workspace to Claude Code running on your machine. You send messages in Slack threads, and Claude Code works on your local codebase in real time.
|
|
10
|
+
|
|
11
|
+
## When is this useful?
|
|
12
|
+
|
|
13
|
+
- You want to give Claude Code development tasks **remotely** (e.g. from your phone or another computer)
|
|
14
|
+
- You want to manage multiple projects through **separate Slack channels**
|
|
15
|
+
- You want to manage multiple tasks for a single project — each task lives in its own **Slack thread**
|
|
16
|
+
- You want Claude Code to work **autonomously** on tasks while you're away (Autopilot mode)
|
|
17
|
+
|
|
18
|
+
> [!CAUTION]
|
|
19
|
+
> **TwinDevBot launches Claude Code with the `--dangerously-skip-permissions` flag.** This means Claude Code can read, write, and execute files on your machine **without asking for permission**.
|
|
20
|
+
>
|
|
21
|
+
> Only use TwinDevBot if you fully understand what this means. The source code is fully open on GitHub. **No one is responsible for any incidents caused by using TwinDevBot.**
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
Before you begin, make sure you have the following:
|
|
28
|
+
|
|
29
|
+
1. **Node.js 18 or later**
|
|
30
|
+
- Check by running `node --version` in your terminal
|
|
31
|
+
- If not installed, download from [nodejs.org](https://nodejs.org)
|
|
32
|
+
|
|
33
|
+
2. **Claude Code CLI**
|
|
34
|
+
- The `claude` command must be available in your terminal
|
|
35
|
+
- Install with: `npm install -g @anthropic-ai/claude-code`
|
|
36
|
+
- Verify by running: `claude --version`
|
|
37
|
+
|
|
38
|
+
3. **macOS for background service**
|
|
39
|
+
- `twindevbot start --daemon`, `stop`, and `status` are supported on macOS (launchd)
|
|
40
|
+
- On other platforms, run in the foreground with `twindevbot start`
|
|
41
|
+
|
|
42
|
+
4. **A Slack workspace** where you have permission to install apps
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Step 1: Create a Slack App
|
|
47
|
+
|
|
48
|
+
You need to create a Slack App in your workspace. This is a one-time setup.
|
|
49
|
+
|
|
50
|
+
### 1.1 Create the app
|
|
51
|
+
|
|
52
|
+
1. Go to [https://api.slack.com/apps](https://api.slack.com/apps)
|
|
53
|
+
2. Click **"Create New App"**
|
|
54
|
+
3. Choose **"From scratch"**
|
|
55
|
+
4. Enter an app name (e.g. `TwinDevBot`) and select your workspace
|
|
56
|
+
5. Click **"Create App"**
|
|
57
|
+
|
|
58
|
+
### 1.2 Enable Socket Mode
|
|
59
|
+
|
|
60
|
+
1. In the left sidebar, click **"Socket Mode"**
|
|
61
|
+
2. Toggle **"Enable Socket Mode"** to ON
|
|
62
|
+
3. You will be asked to create an **App-Level Token**:
|
|
63
|
+
- Add the `connections:write` scope
|
|
64
|
+
- Click **"Generate"**
|
|
65
|
+
4. **Copy the token** (starts with `xapp-`) — you will need this later
|
|
66
|
+
|
|
67
|
+
### 1.3 Add a Slash Command
|
|
68
|
+
|
|
69
|
+
1. In the left sidebar, click **"Slash Commands"**
|
|
70
|
+
2. Click **"Create New Command"**
|
|
71
|
+
3. Fill in:
|
|
72
|
+
- **Command:** `/twindevbot`
|
|
73
|
+
- **Short Description:** `TwinDevBot Commands`
|
|
74
|
+
- **Usage Hint:** `init | task | new | stop`
|
|
75
|
+
4. Click **"Save"**
|
|
76
|
+
|
|
77
|
+
### 1.4 Set Bot Permissions
|
|
78
|
+
|
|
79
|
+
1. In the left sidebar, click **"OAuth & Permissions"**
|
|
80
|
+
2. Scroll down to **"Scopes"** → **"Bot Token Scopes"**
|
|
81
|
+
3. Click **"Add an OAuth Scope"** and add all of the following:
|
|
82
|
+
|
|
83
|
+
| Scope | What it's for |
|
|
84
|
+
| ------------------ | -------------------------------------- |
|
|
85
|
+
| `chat:write` | Send messages to channels |
|
|
86
|
+
| `commands` | Handle the `/twindevbot` slash command |
|
|
87
|
+
| `reactions:write` | Add emoji reactions to show progress |
|
|
88
|
+
| `channels:history` | Read messages in public channels |
|
|
89
|
+
| `groups:history` | Read messages in private channels |
|
|
90
|
+
|
|
91
|
+
### 1.5 Enable Event Subscriptions
|
|
92
|
+
|
|
93
|
+
1. In the left sidebar, click **"Event Subscriptions"**
|
|
94
|
+
2. Toggle **"Enable Events"** to ON
|
|
95
|
+
3. Expand **"Subscribe to bot events"**
|
|
96
|
+
4. Click **"Add Bot User Event"** and add:
|
|
97
|
+
- `message.channels` (messages in public channels)
|
|
98
|
+
- `message.groups` (messages in private channels)
|
|
99
|
+
5. Click **"Save Changes"**
|
|
100
|
+
|
|
101
|
+
### 1.6 Enable Interactivity
|
|
102
|
+
|
|
103
|
+
1. In the left sidebar, click **"Interactivity & Shortcuts"**
|
|
104
|
+
2. Toggle **"Interactivity"** to ON
|
|
105
|
+
3. Click **"Save Changes"**
|
|
106
|
+
|
|
107
|
+
> You do **not** need to enter a Request URL — Socket Mode handles this automatically.
|
|
108
|
+
|
|
109
|
+
### 1.7 Install the App to Your Workspace
|
|
110
|
+
|
|
111
|
+
1. In the left sidebar, click **"Install App"**
|
|
112
|
+
2. Click **"Install to Workspace"**
|
|
113
|
+
3. Review the permissions and click **"Allow"**
|
|
114
|
+
4. **Copy the Bot Token** (starts with `xoxb-`) — you will need this later
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Step 2: Install TwinDevBot
|
|
119
|
+
|
|
120
|
+
Open your terminal and run:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npm install -g twin-dev-bot
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Verify the installation:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
twindevbot help
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Step 3: Start the Server
|
|
135
|
+
|
|
136
|
+
Navigate to the directory where you want TwinDevBot to store its data (your Desktop is recommended). For example:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
cd ~/Desktop
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Then start the server:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
twindevbot start
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**If this is your first time**, a setup wizard will appear asking for:
|
|
149
|
+
|
|
150
|
+
1. **Slack App Token** — paste the `xapp-...` token from Step 1.2
|
|
151
|
+
2. **Slack Bot Token** — paste the `xoxb-...` token from Step 1.7
|
|
152
|
+
3. **Project base directory** — the parent folder where your projects live (default: your Desktop folder)
|
|
153
|
+
|
|
154
|
+
After completing the setup, the server will start and connect to Slack.
|
|
155
|
+
|
|
156
|
+
### Running in the Background (Recommended)
|
|
157
|
+
|
|
158
|
+
To keep TwinDevBot running even after you close the terminal:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
twindevbot start --daemon
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
This registers TwinDevBot as a background service that starts automatically on login (macOS launchd).
|
|
165
|
+
|
|
166
|
+
### Managing the Server
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
twindevbot status # Check if the background service is running
|
|
170
|
+
twindevbot stop # Stop and unregister the background service
|
|
171
|
+
twindevbot show # View saved Claude sessions
|
|
172
|
+
twindevbot clear # Delete saved data (sessions + workspaces)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
> `status` and `stop` are available only when daemon mode is supported (macOS).
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Step 4: Invite the Bot to a Slack Channel
|
|
180
|
+
|
|
181
|
+
Before using TwinDevBot in any channel, you must **invite the bot**:
|
|
182
|
+
|
|
183
|
+
1. Go to the Slack channel where you want to use TwinDevBot
|
|
184
|
+
2. Type `/invite @TwinDevBot` (use the name you gave your Slack app)
|
|
185
|
+
3. The bot should now appear as a channel member
|
|
186
|
+
|
|
187
|
+
> The bot **cannot receive messages** in channels where it is not a member.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Using TwinDevBot in Slack
|
|
192
|
+
|
|
193
|
+
### Setting Up a Channel
|
|
194
|
+
|
|
195
|
+
Before starting any work, tell TwinDevBot which project directory to use for this channel:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
/twindevbot init
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
This shows a list of folders inside your project base directory. Click a folder button to select it, or click **"Enter path manually"** to type a custom path.
|
|
202
|
+
|
|
203
|
+
You only need to do this **once per channel**.
|
|
204
|
+
|
|
205
|
+
### Starting a Task
|
|
206
|
+
|
|
207
|
+
Once a channel is set up, start a new work session:
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
/twindevbot task
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
This creates a message in the channel. **Click on the thread** of that message to begin chatting with Claude Code.
|
|
214
|
+
|
|
215
|
+
Type your instructions in the thread (e.g. "Create a login page with email and password fields"), and Claude Code will start working on your local codebase.
|
|
216
|
+
|
|
217
|
+
### Creating a New Project
|
|
218
|
+
|
|
219
|
+
**Create an empty project:**
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
/twindevbot new my-app --empty
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Create a project from a template:**
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
/twindevbot new my-app --template react
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
This creates the project in your base directory, sets it as the channel's working directory, and opens a new thread automatically.
|
|
232
|
+
|
|
233
|
+
**Available templates:**
|
|
234
|
+
|
|
235
|
+
| Category | Templates |
|
|
236
|
+
| -------- | ------------------------------------------------------------------------------------------------------------- |
|
|
237
|
+
| Frontend | `react`, `nextjs`, `vue`, `nuxt`, `sveltekit`, `angular`, `react-native-expo`, `react-native-bare`, `flutter` |
|
|
238
|
+
| Backend | `express`, `nestjs`, `fastify`, `spring-boot`, `django`, `fastapi`, `go`, `rails`, `laravel` |
|
|
239
|
+
|
|
240
|
+
### Autopilot Mode
|
|
241
|
+
|
|
242
|
+
In Autopilot mode, Claude Code automatically answers its own questions and keeps working without waiting for your input. Great for **small tasks** or **kicking off work right before bed** — let Claude handle it while you sleep.
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
/twindevbot task --autopilot
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Or with a new project:
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
/twindevbot new my-app --template react --autopilot
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
In Autopilot mode:
|
|
255
|
+
|
|
256
|
+
- Claude automatically selects the recommended option for each question
|
|
257
|
+
- All questions and auto-selected answers are logged in the thread for your review
|
|
258
|
+
- You can **interrupt** Autopilot by sending a message in the thread — you'll be asked to confirm before it stops
|
|
259
|
+
|
|
260
|
+
### Stopping a Running Task
|
|
261
|
+
|
|
262
|
+
To cancel the current Claude Code task in a channel:
|
|
263
|
+
|
|
264
|
+
```
|
|
265
|
+
/twindevbot stop
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Getting Help
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
/twindevbot
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Use `/twindevbot` with no subcommand to show the help message. Any unknown subcommand shows the same help.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## How a Conversation Works
|
|
279
|
+
|
|
280
|
+
Here's what happens step by step:
|
|
281
|
+
|
|
282
|
+
1. **You run** `/twindevbot task` → a parent message appears in the channel
|
|
283
|
+
2. **You type** your instructions in the **thread** of that message (e.g. "Add a dark mode toggle")
|
|
284
|
+
3. **Claude Code starts working** — you'll see emoji reactions showing progress:
|
|
285
|
+
- 👀 = Message received
|
|
286
|
+
- ⚙️ = Working (with tool usage updates like "Reading file", "Editing file", etc.)
|
|
287
|
+
- ✅ = Completed
|
|
288
|
+
- ❌ = Error occurred
|
|
289
|
+
4. **If Claude has a question**, buttons appear in the thread:
|
|
290
|
+
- Click a button to select an option
|
|
291
|
+
- Or click **"Custom Input"** to type your own answer
|
|
292
|
+
- For multi-select questions, toggle options and click **"Submit Selection"**
|
|
293
|
+
5. **When the task is done**, the elapsed time is displayed
|
|
294
|
+
6. **Send another message** in the same thread to continue working — Claude remembers the entire conversation
|
|
295
|
+
|
|
296
|
+
### Interrupting a Running Task
|
|
297
|
+
|
|
298
|
+
If Claude is still working and you send a new message in the thread, you'll see a confirmation prompt:
|
|
299
|
+
|
|
300
|
+
- In **normal mode**: you'll be asked whether to stop the current task and start a new one
|
|
301
|
+
- In **Autopilot mode**: you'll be asked to stop autopilot before running your new message
|
|
302
|
+
|
|
303
|
+
Click **Yes** to stop the current task and start your new one, or **No** to let it finish.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Inactivity Timeout
|
|
308
|
+
|
|
309
|
+
If Claude Code receives no events for **30 minutes** (configurable), the process is automatically terminated to save resources. You'll see a notification in the thread. Simply send a new message to restart.
|
|
310
|
+
|
|
311
|
+
To change the timeout, add this to your `.env` file:
|
|
312
|
+
|
|
313
|
+
```
|
|
314
|
+
INACTIVITY_TIMEOUT_MINUTES=60
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Configuration Reference
|
|
320
|
+
|
|
321
|
+
All settings are stored in the `.env` file in the directory where you started TwinDevBot.
|
|
322
|
+
|
|
323
|
+
| Variable | Required | Description | Default |
|
|
324
|
+
| ---------------------------- | -------- | -------------------------------------- | ------------ |
|
|
325
|
+
| `SLACK_BOT_TOKEN` | Yes | Slack Bot Token (`xoxb-...`) | — |
|
|
326
|
+
| `SLACK_APP_TOKEN` | Yes | Slack App Token (`xapp-...`) | — |
|
|
327
|
+
| `TWINDEVBOT_BASE_DIR` | No | Parent directory for projects | Home Desktop |
|
|
328
|
+
| `INACTIVITY_TIMEOUT_MINUTES` | No | Minutes before idle Claude is stopped | `30` |
|
|
329
|
+
| `LOG_LEVEL` | No | `debug` \| `info` \| `warn` \| `error` | `info` |
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## File Locations
|
|
334
|
+
|
|
335
|
+
TwinDevBot stores its data in the directory where you started the server:
|
|
336
|
+
|
|
337
|
+
| File | Purpose |
|
|
338
|
+
| ------------------------- | -------------------------------------- |
|
|
339
|
+
| `.env` | Configuration (Slack tokens, settings) |
|
|
340
|
+
| `data/sessions.json` | Saved Claude Code sessions |
|
|
341
|
+
| `data/workspaces.json` | Thread-to-directory mappings |
|
|
342
|
+
| `data/channels.json` | Channel-to-directory mappings |
|
|
343
|
+
| `data/twindevbot.pid` | Server process ID |
|
|
344
|
+
| `logs/twindevbot.err.log` | Error log |
|
|
345
|
+
| `logs/twindevbot.out.log` | Output log |
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Troubleshooting
|
|
350
|
+
|
|
351
|
+
### "Claude CLI is not installed or not found in PATH"
|
|
352
|
+
|
|
353
|
+
Make sure the `claude` command works in your terminal:
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
claude --version
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
If not, install it:
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
npm install -g @anthropic-ai/claude-code
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Bot doesn't respond to messages
|
|
366
|
+
|
|
367
|
+
1. Make sure the TwinDevBot server is running:
|
|
368
|
+
- If you're using daemon mode (macOS): `twindevbot status`
|
|
369
|
+
- Otherwise, confirm the `twindevbot start` process is still running
|
|
370
|
+
2. Make sure the bot is invited to the channel: `/invite @TwinDevBot`
|
|
371
|
+
3. Make sure you're typing in a **thread**, not directly in the channel
|
|
372
|
+
|
|
373
|
+
### "Session expired" message
|
|
374
|
+
|
|
375
|
+
Sessions are cleaned up after 24 hours of inactivity (cleanup runs hourly). Start a new session with `/twindevbot task`.
|
|
376
|
+
|
|
377
|
+
### Server won't start
|
|
378
|
+
|
|
379
|
+
Check the error log for details:
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
tail -f ./logs/twindevbot.err.log
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
(Run from the directory where you started TwinDevBot)
|
|
386
|
+
|
|
387
|
+
### Something seems wrong with the server
|
|
388
|
+
|
|
389
|
+
Check the error log:
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
tail -f ./logs/twindevbot.err.log
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
To clear all saved data and start fresh:
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
# If you're running in daemon mode (macOS)
|
|
399
|
+
twindevbot stop
|
|
400
|
+
twindevbot clear
|
|
401
|
+
twindevbot start --daemon
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
If you're running in the foreground, stop it with Ctrl+C first, then run:
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
twindevbot clear
|
|
408
|
+
twindevbot start
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## License
|
|
414
|
+
|
|
415
|
+
[AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.html) — You are free to use, modify, and distribute this software. If you distribute modified versions or run it as a network service, you must release your source code under the same license.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync, execFileSync } from "child_process";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const projectRoot = join(__dirname, "..");
|
|
8
|
+
const entry = join(projectRoot, "dist", "cli.js");
|
|
9
|
+
|
|
10
|
+
// start, help 명령만 빌드 (stop, status는 기존 dist 사용)
|
|
11
|
+
const cmd = process.argv[2];
|
|
12
|
+
if (!cmd || cmd === "start" || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
13
|
+
try {
|
|
14
|
+
execSync("npx tsc", { cwd: projectRoot, stdio: "inherit" });
|
|
15
|
+
} catch {
|
|
16
|
+
// noEmitOnError 미설정 → dist/ 파일은 갱신됨
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
execFileSync(process.execPath, [entry, ...process.argv.slice(2)], {
|
|
20
|
+
cwd: projectRoot,
|
|
21
|
+
stdio: "inherit",
|
|
22
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack action.value (~2,000 bytes) 및 private_metadata (~3,000 bytes) 크기 제한 대응.
|
|
3
|
+
* 큰 페이로드를 서버 사이드에 저장하고 짧은 키만 버튼 값에 포함.
|
|
4
|
+
* TTL 기반 자동 정리로 메모리 누수 방지.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* 페이로드 저장
|
|
8
|
+
*/
|
|
9
|
+
export declare function setPayload(key: string, data: unknown, ttlMs?: number): void;
|
|
10
|
+
/**
|
|
11
|
+
* 페이로드 조회 (기본: 조회 후 삭제하지 않음)
|
|
12
|
+
* @param remove true이면 조회 후 삭제 (일회성 데이터용)
|
|
13
|
+
*/
|
|
14
|
+
export declare function getPayload<T>(key: string, remove?: boolean): T | null;
|
|
15
|
+
/**
|
|
16
|
+
* 페이로드 삭제
|
|
17
|
+
*/
|
|
18
|
+
export declare function removePayload(key: string): void;
|
|
19
|
+
/**
|
|
20
|
+
* 테스트용: 스토어 전체 초기화
|
|
21
|
+
*/
|
|
22
|
+
export declare function clearAllPayloads(): void;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack action.value (~2,000 bytes) 및 private_metadata (~3,000 bytes) 크기 제한 대응.
|
|
3
|
+
* 큰 페이로드를 서버 사이드에 저장하고 짧은 키만 버튼 값에 포함.
|
|
4
|
+
* TTL 기반 자동 정리로 메모리 누수 방지.
|
|
5
|
+
*/
|
|
6
|
+
const store = new Map();
|
|
7
|
+
const DEFAULT_TTL_MS = 2 * 60 * 60 * 1000; // 2시간
|
|
8
|
+
function cleanup() {
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
for (const [key, entry] of store) {
|
|
11
|
+
if (now > entry.expiresAt)
|
|
12
|
+
store.delete(key);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 페이로드 저장
|
|
17
|
+
*/
|
|
18
|
+
export function setPayload(key, data, ttlMs = DEFAULT_TTL_MS) {
|
|
19
|
+
cleanup();
|
|
20
|
+
store.set(key, { data, expiresAt: Date.now() + ttlMs });
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 페이로드 조회 (기본: 조회 후 삭제하지 않음)
|
|
24
|
+
* @param remove true이면 조회 후 삭제 (일회성 데이터용)
|
|
25
|
+
*/
|
|
26
|
+
export function getPayload(key, remove = false) {
|
|
27
|
+
const entry = store.get(key);
|
|
28
|
+
if (!entry)
|
|
29
|
+
return null;
|
|
30
|
+
if (Date.now() > entry.expiresAt) {
|
|
31
|
+
store.delete(key);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (remove) {
|
|
35
|
+
store.delete(key);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// TTL 갱신: 접근 시 만료 시간 리셋 (사용자가 상호작용 중임을 표시)
|
|
39
|
+
entry.expiresAt = Date.now() + DEFAULT_TTL_MS;
|
|
40
|
+
}
|
|
41
|
+
return entry.data;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 페이로드 삭제
|
|
45
|
+
*/
|
|
46
|
+
export function removePayload(key) {
|
|
47
|
+
store.delete(key);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 테스트용: 스토어 전체 초기화
|
|
51
|
+
*/
|
|
52
|
+
export function clearAllPayloads() {
|
|
53
|
+
store.clear();
|
|
54
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 활성 러너 레지스트리
|
|
3
|
+
*
|
|
4
|
+
* Slack 스레드(threadTs)별로 실행 중인 ClaudeRunner 인스턴스를 추적하여
|
|
5
|
+
* 동일 스레드에서 여러 Claude 프로세스가 동시에 실행되는 것을 방지합니다.
|
|
6
|
+
*
|
|
7
|
+
* - 마지막 이벤트 수신 후 config.inactivityTimeoutMs(기본 30분) 동안
|
|
8
|
+
* 활동이 없으면 프로세스를 자동 종료하고 스레드 잠금을 해제합니다.
|
|
9
|
+
* - onTimeout 콜백을 통해 Slack 타임아웃 알림 등 외부 처리가 가능합니다.
|
|
10
|
+
*/
|
|
11
|
+
import type { ClaudeRunner } from "./claude-runner.js";
|
|
12
|
+
export interface RegisterRunnerOptions {
|
|
13
|
+
/** 타임아웃 시 호출할 콜백 (Slack 알림 등) */
|
|
14
|
+
onTimeout?: () => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 러너를 활성 상태로 등록
|
|
18
|
+
*/
|
|
19
|
+
export declare function registerRunner(threadTs: string, runner: ClaudeRunner, options?: RegisterRunnerOptions): void;
|
|
20
|
+
/**
|
|
21
|
+
* 이벤트 수신 시 활동 시간 갱신 및 타임아웃 리셋
|
|
22
|
+
* 저장된 runner와 동일한 인스턴스인 경우에만 갱신 (stale 갱신 방지)
|
|
23
|
+
*/
|
|
24
|
+
export declare function refreshActivity(threadTs: string, runner: ClaudeRunner): void;
|
|
25
|
+
/**
|
|
26
|
+
* 러너를 비활성 상태로 해제
|
|
27
|
+
* 저장된 runner와 동일한 인스턴스인 경우에만 해제 (stale 해제 방지)
|
|
28
|
+
*/
|
|
29
|
+
export declare function unregisterRunner(threadTs: string, runner: ClaudeRunner): void;
|
|
30
|
+
/**
|
|
31
|
+
* 해당 스레드에 활성 러너가 있는지 확인
|
|
32
|
+
*/
|
|
33
|
+
export declare function isRunnerActive(threadTs: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* 활성 러너를 강제 종료하고 등록 해제.
|
|
36
|
+
* autopilot 개입, /twindevbot stop 명령, 일반 모드 인터럽트 등
|
|
37
|
+
* 외부 요청에 의해 실행 중인 작업을 중단할 때 사용합니다.
|
|
38
|
+
*/
|
|
39
|
+
export declare function killActiveRunner(threadTs: string): void;
|
|
40
|
+
/**
|
|
41
|
+
* 모든 활성 러너를 강제 종료하고 등록 해제
|
|
42
|
+
* (graceful shutdown 시 고아 프로세스 방지)
|
|
43
|
+
*/
|
|
44
|
+
export declare function killAllRunners(): number;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 활성 러너 레지스트리
|
|
3
|
+
*
|
|
4
|
+
* Slack 스레드(threadTs)별로 실행 중인 ClaudeRunner 인스턴스를 추적하여
|
|
5
|
+
* 동일 스레드에서 여러 Claude 프로세스가 동시에 실행되는 것을 방지합니다.
|
|
6
|
+
*
|
|
7
|
+
* - 마지막 이벤트 수신 후 config.inactivityTimeoutMs(기본 30분) 동안
|
|
8
|
+
* 활동이 없으면 프로세스를 자동 종료하고 스레드 잠금을 해제합니다.
|
|
9
|
+
* - onTimeout 콜백을 통해 Slack 타임아웃 알림 등 외부 처리가 가능합니다.
|
|
10
|
+
*/
|
|
11
|
+
import { createLogger } from "./logger.js";
|
|
12
|
+
import { config } from "./config.js";
|
|
13
|
+
const log = createLogger("active-runners");
|
|
14
|
+
const activeRunners = new Map();
|
|
15
|
+
function startTimer(threadTs, entry) {
|
|
16
|
+
return setTimeout(() => {
|
|
17
|
+
log.warn("Runner inactivity timeout", {
|
|
18
|
+
threadTs,
|
|
19
|
+
inactiveMs: Date.now() - entry.lastActivityAt,
|
|
20
|
+
totalMs: Date.now() - entry.registeredAt,
|
|
21
|
+
});
|
|
22
|
+
// 콜백 실행 (Slack 알림 등)
|
|
23
|
+
try {
|
|
24
|
+
entry.onTimeout?.();
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
log.error("onTimeout callback error", err);
|
|
28
|
+
}
|
|
29
|
+
// 프로세스 종료 및 등록 해제
|
|
30
|
+
entry.runner.kill();
|
|
31
|
+
activeRunners.delete(threadTs);
|
|
32
|
+
}, config.inactivityTimeoutMs);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 러너를 활성 상태로 등록
|
|
36
|
+
*/
|
|
37
|
+
export function registerRunner(threadTs, runner, options) {
|
|
38
|
+
// 기존 엔트리가 있으면 프로세스 종료 및 타이머 정리
|
|
39
|
+
const existing = activeRunners.get(threadTs);
|
|
40
|
+
if (existing) {
|
|
41
|
+
clearTimeout(existing.timer);
|
|
42
|
+
existing.runner.kill();
|
|
43
|
+
log.info("Previous runner killed on re-register", { threadTs });
|
|
44
|
+
}
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
const entry = {
|
|
47
|
+
runner,
|
|
48
|
+
timer: null,
|
|
49
|
+
registeredAt: now,
|
|
50
|
+
lastActivityAt: now,
|
|
51
|
+
onTimeout: options?.onTimeout,
|
|
52
|
+
};
|
|
53
|
+
entry.timer = startTimer(threadTs, entry);
|
|
54
|
+
activeRunners.set(threadTs, entry);
|
|
55
|
+
log.debug("Runner registered", { threadTs });
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 이벤트 수신 시 활동 시간 갱신 및 타임아웃 리셋
|
|
59
|
+
* 저장된 runner와 동일한 인스턴스인 경우에만 갱신 (stale 갱신 방지)
|
|
60
|
+
*/
|
|
61
|
+
export function refreshActivity(threadTs, runner) {
|
|
62
|
+
const entry = activeRunners.get(threadTs);
|
|
63
|
+
if (!entry || entry.runner !== runner)
|
|
64
|
+
return;
|
|
65
|
+
entry.lastActivityAt = Date.now();
|
|
66
|
+
clearTimeout(entry.timer);
|
|
67
|
+
entry.timer = startTimer(threadTs, entry);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 러너를 비활성 상태로 해제
|
|
71
|
+
* 저장된 runner와 동일한 인스턴스인 경우에만 해제 (stale 해제 방지)
|
|
72
|
+
*/
|
|
73
|
+
export function unregisterRunner(threadTs, runner) {
|
|
74
|
+
const entry = activeRunners.get(threadTs);
|
|
75
|
+
if (!entry || entry.runner !== runner)
|
|
76
|
+
return;
|
|
77
|
+
clearTimeout(entry.timer);
|
|
78
|
+
activeRunners.delete(threadTs);
|
|
79
|
+
log.debug("Runner unregistered", { threadTs });
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 해당 스레드에 활성 러너가 있는지 확인
|
|
83
|
+
*/
|
|
84
|
+
export function isRunnerActive(threadTs) {
|
|
85
|
+
return activeRunners.has(threadTs);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 활성 러너를 강제 종료하고 등록 해제.
|
|
89
|
+
* autopilot 개입, /twindevbot stop 명령, 일반 모드 인터럽트 등
|
|
90
|
+
* 외부 요청에 의해 실행 중인 작업을 중단할 때 사용합니다.
|
|
91
|
+
*/
|
|
92
|
+
export function killActiveRunner(threadTs) {
|
|
93
|
+
const entry = activeRunners.get(threadTs);
|
|
94
|
+
if (!entry)
|
|
95
|
+
return;
|
|
96
|
+
clearTimeout(entry.timer);
|
|
97
|
+
entry.runner.kill();
|
|
98
|
+
activeRunners.delete(threadTs);
|
|
99
|
+
log.info("Runner killed by external request", { threadTs });
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 모든 활성 러너를 강제 종료하고 등록 해제
|
|
103
|
+
* (graceful shutdown 시 고아 프로세스 방지)
|
|
104
|
+
*/
|
|
105
|
+
export function killAllRunners() {
|
|
106
|
+
const count = activeRunners.size;
|
|
107
|
+
for (const [threadTs, entry] of activeRunners) {
|
|
108
|
+
clearTimeout(entry.timer);
|
|
109
|
+
entry.runner.kill();
|
|
110
|
+
log.info("Runner killed during shutdown", { threadTs });
|
|
111
|
+
}
|
|
112
|
+
activeRunners.clear();
|
|
113
|
+
return count;
|
|
114
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel Store
|
|
3
|
+
*
|
|
4
|
+
* 슬랙 채널과 작업 디렉토리의 매핑을 관리합니다.
|
|
5
|
+
* /twindevbot init으로 설정된 채널별 작업 디렉토리를 영속적으로 저장합니다.
|
|
6
|
+
*
|
|
7
|
+
* 키: channelId (Slack 채널 ID)
|
|
8
|
+
* 값: { directory, projectName }
|
|
9
|
+
*/
|
|
10
|
+
export interface ChannelDir {
|
|
11
|
+
directory: string;
|
|
12
|
+
projectName: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function setChannelDir(channelId: string, dir: ChannelDir): void;
|
|
15
|
+
export declare function getChannelDir(channelId: string): ChannelDir | undefined;
|
|
16
|
+
export declare function removeChannelDir(channelId: string): void;
|