multi-project-gateway 0.2.0 → 0.4.0

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/README.md CHANGED
@@ -5,17 +5,23 @@ A Discord bot that routes channel messages to per-project [Claude Code](https://
5
5
  ## How it works
6
6
 
7
7
  ```
8
- Discord channel --> Router --> Session Manager --> claude --print
9
- (per project) (channel -> project) (queue, resume, persist) (in project dir)
10
- |
11
- Discord reply <-- Chunker <---------------------- JSON response <------'
8
+ Discord channel --> Router --> Agent Dispatch --> Session Manager --> claude --print
9
+ (per project) (channel -> (@mention -> (queue, resume, (in project dir)
10
+ project) agent) persist) |
11
+ ^ |
12
+ |--- auto-handoff if response has @mention <-'
13
+ |
14
+ Discord reply <-- Chunker <-------------------------- JSON response <--------'
12
15
  ```
13
16
 
14
17
  1. User posts a message in a mapped Discord channel
15
18
  2. Router resolves the channel to a project config
16
- 3. Session manager spawns `claude --print` in the project directory (or resumes an existing session)
17
- 4. Response is chunked to fit Discord's 2000-char limit and sent back
18
- 5. Sessions persist to disk and resume across gateway restarts
19
+ 3. If the message is in a main channel, the bot creates a thread for the response; if already in a thread, replies there directly
20
+ 4. If agents are configured, agent dispatch routes via `@mention` or last active agent
21
+ 5. Session manager spawns `claude --print` in the project directory (or resumes an existing session)
22
+ 6. If the response contains an `@mention` of another agent, auto-handoff loops until done or turn limit reached
23
+ 7. Response is chunked to fit Discord's 2000-char limit and sent back in the thread
24
+ 8. Sessions persist to disk and resume across gateway restarts
19
25
 
20
26
  ## Security model
21
27
 
@@ -28,9 +34,57 @@ By default, each Claude session is restricted to its project directory using `--
28
34
  **Important considerations:**
29
35
  - Anyone who can post in a mapped Discord channel can instruct Claude to read and modify files in that project's directory
30
36
  - Only map channels that trusted users have access to
31
- - For stricter control, use `--allowed-tools` in `claudeArgs` to whitelist specific tools
37
+ - Tool restrictions are enforced via `--allowed-tools` / `--disallowed-tools` (see [Tool security](#tool-security))
32
38
  - For maximum access (e.g., in a sandboxed environment), you can set `claudeArgs` to use `--dangerously-skip-permissions`, but this gives Claude full OS-level access
33
39
 
40
+ ### Tool security
41
+
42
+ The gateway restricts which tools Claude can use via `--allowed-tools` and `--disallowed-tools` CLI flags. By default, only safe file-system and read-only tools are allowed.
43
+
44
+ **Default allowlist:**
45
+
46
+ | Tool | Description | Security implications |
47
+ |------|-------------|----------------------|
48
+ | `Read` | Read file contents | Read-only. Can read any file in the project directory. |
49
+ | `Edit` | Edit existing files (patch-based) | Can modify existing files. Cannot create new files. |
50
+ | `Write` | Create or overwrite files | Can create new files or overwrite existing ones. |
51
+ | `Glob` | Find files by pattern | Read-only directory listing. Low risk. |
52
+ | `Grep` | Search file contents | Read-only content search. Low risk. |
53
+ | `Bash(git:*)` | Run git commands only | Restricted to `git` subcommands. Can commit, push, branch. Cannot run arbitrary shell commands. |
54
+ | `TodoWrite` | Write to Claude's internal todo list | No file-system side effects. |
55
+
56
+ **Tools NOT in the default allowlist (higher risk):**
57
+
58
+ | Tool | Risk | Why it is excluded |
59
+ |------|------|--------------------|
60
+ | `Bash` (unrestricted) | **High** | Full shell access: can run any command, install packages, access network, modify system files. |
61
+ | `WebSearch` / `WebFetch` | **Medium** | Network access: can exfiltrate data or fetch untrusted content. |
62
+ | `NotebookEdit` | **Low** | Jupyter notebook editing. Excluded for simplicity; add if needed. |
63
+
64
+ **Configuration examples:**
65
+
66
+ ```json
67
+ {
68
+ "defaults": {
69
+ "allowedTools": ["Read", "Edit", "Write", "Glob", "Grep", "Bash(git:*)", "TodoWrite"]
70
+ },
71
+ "projects": {
72
+ "TRUSTED_PROJECT_CHANNEL": {
73
+ "directory": "/path/to/trusted-project",
74
+ "allowedTools": ["Read", "Edit", "Write", "Glob", "Grep", "Bash", "TodoWrite"]
75
+ },
76
+ "READ_ONLY_CHANNEL": {
77
+ "directory": "/path/to/sensitive-project",
78
+ "allowedTools": ["Read", "Glob", "Grep"]
79
+ }
80
+ }
81
+ }
82
+ ```
83
+
84
+ **Override precedence:** per-project `allowedTools`/`disallowedTools` override gateway defaults. If both `allowedTools` and `disallowedTools` are set at the same level, `allowedTools` takes precedence (a warning is logged). If `claudeArgs` (at either the gateway or project level) already contains `--allowed-tools` or `--disallowed-tools`, the config-based tool restrictions are skipped to avoid conflicts.
85
+
86
+ > **Disallow-only mode:** When a project sets only `disallowedTools` without setting `allowedTools`, the gateway-level `allowedTools` default still applies (via fallback). This means the project inherits the default allowlist _and_ adds its disallow rules on top — but since `allowedTools` takes precedence over `disallowedTools`, the disallow list is effectively ignored. To use disallow-only mode (block specific tools while allowing everything else), explicitly set `"allowedTools": []` at the project level to clear the inherited allowlist.
87
+
34
88
  ## Prerequisites
35
89
 
36
90
  - **Node.js** 20+
@@ -46,9 +100,10 @@ By default, each Claude session is restricted to its project directory using `--
46
100
  3. Go to **Bot** in the sidebar
47
101
  4. Click **Reset Token** and copy the token (you'll need it in step 3)
48
102
  5. Enable **Message Content Intent** under Privileged Gateway Intents
49
- 6. Go to **OAuth2 > URL Generator**, select the `bot` scope
50
- 7. Under Bot Permissions, select: **Send Messages**, **Read Message History**, **Add Reactions**
51
- 8. Copy the generated URL and open it in your browser to invite the bot to your server
103
+ 6. If you plan to use **role-based access control** (`allowedRoles` in config), also enable the **Server Members Intent** (GuildMembers) under Privileged Gateway Intents. This is required for the bot to read member roles.
104
+ 7. Go to **OAuth2 > URL Generator**, select the `bot` scope
105
+ 8. Under Bot Permissions, select: **Send Messages**, **Read Message History**, **Add Reactions**
106
+ 9. Copy the generated URL and open it in your browser to invite the bot to your server
52
107
 
53
108
  ### 2. Create Discord channels for your projects
54
109
 
@@ -61,11 +116,17 @@ npm install -g multi-project-gateway
61
116
  mpg init
62
117
  ```
63
118
 
119
+ This creates `.env` and `config.json` in the current directory. To store config centrally in `~/.mpg/` instead (recommended for worktrees and multi-config setups):
120
+
121
+ ```bash
122
+ mpg init --profile default
123
+ ```
124
+
64
125
  The init wizard will:
65
126
  - Check that `claude` CLI is available
66
127
  - Ask for your Discord bot token
67
128
  - Walk you through adding projects (name, directory path, channel ID)
68
- - Generate `config.json` and `.env`
129
+ - Generate `config.json` and `.env` (in CWD or `~/.mpg/profiles/<name>/` when using `--profile`)
69
130
 
70
131
  Or set up manually by cloning:
71
132
 
@@ -105,9 +166,11 @@ Create `config.json`:
105
166
  ### 4. Start the gateway
106
167
 
107
168
  ```bash
108
- mpg start # if installed globally
169
+ mpg start # if installed globally (uses default profile or CWD)
170
+ mpg start --profile dev # use a named profile from ~/.mpg/
171
+ mpg start --config /path/to/config.json # use an explicit config file
109
172
  # or
110
- npm run dev # development (no build step)
173
+ npm run dev # development (no build step)
111
174
  # or
112
175
  npm run build && npm start # production
113
176
  ```
@@ -132,9 +195,79 @@ Commands:
132
195
  start Start the gateway (default)
133
196
  init Interactive setup wizard
134
197
  status Show session status from disk
198
+ logs Show structured gateway logs
135
199
  help Show help
200
+
201
+ Options:
202
+ --profile <name> Use a named profile (default: "default")
203
+ --config <path> Use a specific config.json path
204
+ --migrate Copy CWD config files into ~/.mpg/profiles/default/
205
+ --level <level> (logs) Filter by minimum log level (debug|info|warn|error)
136
206
  ```
137
207
 
208
+ ## Config home (`~/.mpg/`)
209
+
210
+ By default, mpg resolves configuration from the current working directory. For multi-worktree setups or dev/prod separation, you can use a centralized config home at `~/.mpg/` (overridable via the `MPG_HOME` environment variable).
211
+
212
+ ### Directory layout
213
+
214
+ ```
215
+ ~/.mpg/
216
+ ├── .env # shared secrets (bot token)
217
+ ├── profiles/
218
+ │ ├── default/
219
+ │ │ ├── config.json # project/channel config
220
+ │ │ └── sessions.json # runtime session state
221
+ │ └── dev/
222
+ │ ├── config.json
223
+ │ └── sessions.json
224
+ ```
225
+
226
+ ### Resolution order
227
+
228
+ **`.env` / secrets:**
229
+ 1. Environment variables (already set) — highest priority
230
+ 2. `$MPG_HOME/.env`
231
+ 3. `$CWD/.env` — lowest priority, backward compat
232
+
233
+ **`config.json`:**
234
+ 1. `--config <path>` CLI flag
235
+ 2. `--profile <name>` resolves to `$MPG_HOME/profiles/<name>/config.json`
236
+ 3. `$MPG_HOME/profiles/default/config.json`
237
+ 4. `$CWD/config.json` — backward compat fallback
238
+
239
+ **`sessions.json`:** Always co-located with the resolved `config.json` (same directory).
240
+
241
+ ### Setting up profiles
242
+
243
+ ```bash
244
+ # Create a profile using the init wizard
245
+ mpg init --profile default
246
+
247
+ # Create a dev profile
248
+ mpg init --profile dev
249
+
250
+ # Start with a specific profile
251
+ mpg start --profile dev
252
+
253
+ # Or point to an explicit config file
254
+ mpg start --config /path/to/config.json
255
+ ```
256
+
257
+ ### Migrating from CWD-based setup
258
+
259
+ If you already have `.env`, `config.json`, and `.sessions.json` in your current directory:
260
+
261
+ ```bash
262
+ mpg init --migrate
263
+ ```
264
+
265
+ This copies your CWD files into `~/.mpg/profiles/default/` and prints what it did. The original files are left in place, so nothing breaks. No automatic migration is performed.
266
+
267
+ ### Backward compatibility
268
+
269
+ If `~/.mpg/` does not exist and CWD files do, everything works exactly as before — zero breaking change.
270
+
138
271
  ## Configuration
139
272
 
140
273
  ### `config.json`
@@ -144,16 +277,102 @@ Commands:
144
277
  | `defaults.idleTimeoutMs` | number | `1800000` (30 min) | Session idle timeout before cleanup |
145
278
  | `defaults.maxConcurrentSessions` | number | `4` | Max concurrent Claude processes |
146
279
  | `defaults.claudeArgs` | string[] | `["--permission-mode", "acceptEdits", "--output-format", "json"]` | Args passed to every `claude` invocation |
280
+ | `defaults.allowedTools` | string[] | `["Read", "Edit", "Write", "Glob", "Grep", "Bash(git:*)", "TodoWrite"]` | Tools Claude is allowed to use (see [Tool security](#tool-security)) |
281
+ | `defaults.disallowedTools` | string[] | `[]` | Tools Claude is forbidden from using (conflicts with `allowedTools`) |
282
+ | `defaults.maxTurnsPerAgent` | number | `5` | Max automatic handoffs in a single agent chain |
283
+ | `defaults.agentTimeoutMs` | number | `180000` (3 min) | Timeout per agent turn during auto-handoff |
284
+ | `defaults.sessionTtlMs` | number | `604800000` (7 days) | Max age for persisted sessions before pruning |
285
+ | `defaults.maxPersistedSessions` | number | `50` | Max number of persisted sessions kept on disk |
286
+ | `defaults.httpPort` | number \| false | `3100` | Port for the web dashboard and API (`false` to disable) |
287
+ | `defaults.logLevel` | string | `"info"` | Minimum log level (`debug`, `info`, `warn`, `error`) |
147
288
  | `projects.<channelId>.name` | string | channel ID | Display name for the project |
148
289
  | `projects.<channelId>.directory` | string | **required** | Absolute path to the project directory |
149
290
  | `projects.<channelId>.idleTimeoutMs` | number | inherits default | Per-project idle timeout override |
150
291
  | `projects.<channelId>.claudeArgs` | string[] | inherits default | Per-project Claude args override |
292
+ | `projects.<channelId>.allowedTools` | string[] | inherits default | Per-project allowed tools override |
293
+ | `projects.<channelId>.disallowedTools` | string[] | inherits default | Per-project disallowed tools override |
294
+ | `projects.<channelId>.agents` | object | — | Named agents for this project (see [Multi-agent setup](#multi-agent-setup)) |
295
+ | `projects.<channelId>.allowedRoles` | string[] | — | Discord role names required to use this project (empty = no restriction) |
296
+ | `projects.<channelId>.rateLimitPerUser` | number | — | Max messages per user per minute for this project |
297
+
298
+ ## Multi-agent setup
299
+
300
+ You can define multiple agents per project that collaborate via `@mentions`. Each agent gets its own Claude session with a dedicated system prompt, and agents can hand off work to each other automatically.
301
+
302
+ ### Defining agents
303
+
304
+ Add an `agents` map to any project in `config.json`. Each key is the agent name (used as `@name` in Discord), with `role` and `prompt` fields:
305
+
306
+ ```json
307
+ {
308
+ "projects": {
309
+ "CHANNEL_ID": {
310
+ "name": "my-app",
311
+ "directory": "/path/to/my-app",
312
+ "agents": {
313
+ "pm": {
314
+ "role": "Product Manager",
315
+ "prompt": "You are the PM for my-app. Analyze requirements, create issues, and review work. When you need code implemented, mention @engineer in your response. Never write code directly."
316
+ },
317
+ "engineer": {
318
+ "role": "Software Engineer",
319
+ "prompt": "You are a senior engineer for my-app. Implement features, write tests, fix bugs, and create PRs. When work is done or you need PM review, mention @pm in your response."
320
+ }
321
+ }
322
+ }
323
+ }
324
+ }
325
+ ```
326
+
327
+ ### How agent routing works
328
+
329
+ ```
330
+ User sends message
331
+ |
332
+ v
333
+ Contains @agentName? ── YES ──> Route to that agent
334
+ | (session key: threadId:agentName)
335
+ NO
336
+ |
337
+ v
338
+ In a thread with prior agent activity? ── YES ──> Route to last active agent
339
+ |
340
+ NO
341
+ |
342
+ v
343
+ Route to default session (no agent)
344
+ ```
345
+
346
+ - **`@mention` routing:** Write `@pm fix the login bug` to target a specific agent. The mention is stripped from the prompt.
347
+ - **Plain reply routing:** Follow-up messages in a thread (without an `@mention`) automatically route to whichever agent last responded in that thread.
348
+ - **Isolated sessions:** Each agent gets its own Claude session per thread (`threadId:agentName`), so `@pm` and `@engineer` maintain separate conversation histories.
349
+
350
+ ### Automatic agent handoffs
351
+
352
+ When an agent's response contains an `@mention` of another agent in the same project, the gateway automatically forwards that response as the next agent's input. This creates a collaborative loop:
353
+
354
+ 1. User writes `@pm add a search feature to the dashboard`
355
+ 2. PM agent analyzes the request, responds with requirements mentioning `@engineer`
356
+ 3. Gateway automatically sends PM's response to the engineer agent
357
+ 4. Engineer implements and responds mentioning `@pm` for review
358
+ 5. Loop continues until no `@mention` is found or the turn limit is reached
359
+
360
+ The turn counter resets whenever a human posts a new message. The `maxTurnsPerAgent` default (5) prevents runaway loops.
361
+
362
+ ### Listing agents
363
+
364
+ Use `!agents` in any mapped Discord channel to see the available agents for that project.
365
+
366
+ ### Thread history
367
+
368
+ When an agent is invoked in a thread, the gateway prepends the last 20 messages as context so the agent understands the conversation so far. This is especially useful when a different agent picks up a thread mid-conversation.
151
369
 
152
370
  ### Environment variables
153
371
 
154
372
  | Variable | Required | Description |
155
373
  |----------|----------|-------------|
156
374
  | `DISCORD_BOT_TOKEN` | Yes | Discord bot token |
375
+ | `MPG_HOME` | No | Override config home directory (default: `~/.mpg`) |
157
376
 
158
377
  ### Resuming sessions from terminal
159
378
 
@@ -166,6 +385,19 @@ claude --resume <session-id>
166
385
 
167
386
  **Important:** You must run `claude --resume` from the same directory the session was started in (i.e., the project's `directory` in `config.json`). Claude will not find the session if you run it from a different working directory.
168
387
 
388
+ ## Threading and per-thread sessions
389
+
390
+ When a user posts a message in a mapped channel, the bot automatically creates a Discord thread and replies there instead of cluttering the main channel. Follow-up messages within the thread continue the same conversation.
391
+
392
+ Each thread gets its **own Claude session**, isolated from the main channel and other threads. This means:
393
+
394
+ - Multiple users can work in the same project channel without their conversations interleaving
395
+ - Each thread maintains its own context and history
396
+ - The thread inherits the project config (directory, Claude args) from the parent channel
397
+ - Threads auto-archive after 60 minutes of inactivity
398
+
399
+ If thread creation fails (e.g., due to permissions), the bot falls back to replying in the main channel.
400
+
169
401
  ## Discord commands
170
402
 
171
403
  The gateway responds to commands in any mapped Discord channel:
@@ -174,21 +406,54 @@ The gateway responds to commands in any mapped Discord channel:
174
406
  |---------|-------------|
175
407
  | `!sessions` | List all active sessions with idle time and queue depth |
176
408
  | `!session <name>` | Inspect a specific project's session (ID, idle time, queue) |
409
+ | `!restart <name>` | Reset a session (fresh context, keeps worktree) |
177
410
  | `!kill <name>` | Force-close a project's session |
411
+ | `!ask <agent> <message>` | Dispatch a message to a specific agent (shorthand: `!<agent> <message>`) |
412
+ | `!agents` | List available agents for the current project |
178
413
  | `!help` | Show available commands |
179
414
 
415
+ ## Web dashboard
416
+
417
+ The gateway includes a built-in web dashboard for monitoring sessions and projects. It starts automatically on the port configured by `defaults.httpPort` (default: `3100`). Set `httpPort` to `false` to disable it.
418
+
419
+ Open `http://localhost:3100/` to view the dashboard, which shows:
420
+
421
+ - Gateway health and Discord connection status
422
+ - Active sessions with last activity time and queue depth
423
+ - Configured projects and their agents
424
+
425
+ ### API endpoints
426
+
427
+ | Endpoint | Description |
428
+ |----------|-------------|
429
+ | `GET /` | Web dashboard (auto-refreshes every 5 seconds) |
430
+ | `GET /health` | Health check — returns status, uptime, session/queue counts, and Discord connection state |
431
+ | `GET /api/sessions` | List all active sessions with details |
432
+ | `GET /api/projects` | List configured projects and their agents |
433
+ | `GET /api/status` | Combined status: version, health, sessions, and projects |
434
+
180
435
  ## Architecture
181
436
 
182
437
  | Module | Responsibility |
183
438
  |--------|---------------|
184
439
  | `src/cli.ts` | CLI entry point — `mpg start`, `mpg init`, `mpg status` |
185
- | `src/init.ts` | Interactive setup wizard |
440
+ | `src/resolve-home.ts` | Resolves `~/.mpg/` config home, profiles, and file resolution order |
441
+ | `src/init.ts` | Interactive setup wizard (supports `--profile`) |
186
442
  | `src/config.ts` | Validates and merges `config.json` with defaults |
187
- | `src/router.ts` | Maps channel IDs to project configs (supports threads via parent lookup) |
188
- | `src/session-manager.ts` | One session per project, queues concurrent messages, manages idle timeouts |
443
+ | `src/router.ts` | Maps channel IDs to project configs; threads resolve to their own session using the parent channel's project config |
444
+ | `src/session-manager.ts` | One session per channel/thread, queues concurrent messages, manages idle timeouts |
189
445
  | `src/session-store.ts` | Persists session IDs to `.sessions.json` for resume across restarts |
190
446
  | `src/claude-cli.ts` | Spawns `claude --print` subprocess, parses JSON output |
191
- | `src/discord.ts` | Discord.js client, message routing, response chunking |
447
+ | `src/agent-dispatch.ts` | Parses `@mentions`, resolves agent targets |
448
+ | `src/turn-counter.ts` | Tracks handoff turns per thread, enforces `maxTurnsPerAgent` |
449
+ | `src/worktree.ts` | Manages git worktrees for session isolation; reconciles orphans on startup |
450
+ | `src/embed-format.ts` | Builds Discord embeds for agent responses and handoff announcements |
451
+ | `src/persona-presets.ts` | Built-in persona library (PM, engineer, etc.) for agent shorthand config |
452
+ | `src/role-check.ts` | Checks Discord member roles against `allowedRoles` |
453
+ | `src/rate-limiter.ts` | Per-user rate limiting (sliding window) |
454
+ | `src/health-server.ts` | Web dashboard and REST API (`/health`, `/api/sessions`, `/api/projects`, `/api/status`) |
455
+ | `src/logger.ts` | Structured logger with level filtering and JSON output |
456
+ | `src/discord.ts` | Discord.js client, message routing, agent handoff loop, response chunking |
192
457
 
193
458
  ## Scripts
194
459
 
@@ -204,9 +469,9 @@ The gateway responds to commands in any mapped Discord channel:
204
469
 
205
470
  - **Text only** — attachments and embeds are not forwarded to Claude
206
471
  - **One message at a time per project** — concurrent messages to the same project are queued
207
- - **Threads share parent session** no per-thread isolation
472
+ - **Per-thread sessions** each thread gets its own Claude session scoped to the parent channel's project; threads auto-archive after 60 minutes of inactivity
208
473
  - **Local only** — the gateway runs on the same machine as the project directories
209
- - **No Discord access control** — any user in a mapped channel can send prompts; restrict channel access in Discord server settings
474
+ - **Optional Discord access control** — per-project `allowedRoles` restricts usage to specific Discord roles; `rateLimitPerUser` throttles per-user message rate. Without these, any user in a mapped channel can send prompts
210
475
 
211
476
  ## License
212
477