multi-project-gateway 0.3.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,18 +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
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
17
- 4. Session manager spawns `claude --print` in the project directory (or resumes an existing session)
18
- 5. Response is chunked to fit Discord's 2000-char limit and sent back in the thread
19
- 6. Sessions persist to disk and resume across gateway restarts
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
20
25
 
21
26
  ## Security model
22
27
 
@@ -29,9 +34,57 @@ By default, each Claude session is restricted to its project directory using `--
29
34
  **Important considerations:**
30
35
  - Anyone who can post in a mapped Discord channel can instruct Claude to read and modify files in that project's directory
31
36
  - Only map channels that trusted users have access to
32
- - 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))
33
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
34
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
+
35
88
  ## Prerequisites
36
89
 
37
90
  - **Node.js** 20+
@@ -47,9 +100,10 @@ By default, each Claude session is restricted to its project directory using `--
47
100
  3. Go to **Bot** in the sidebar
48
101
  4. Click **Reset Token** and copy the token (you'll need it in step 3)
49
102
  5. Enable **Message Content Intent** under Privileged Gateway Intents
50
- 6. Go to **OAuth2 > URL Generator**, select the `bot` scope
51
- 7. Under Bot Permissions, select: **Send Messages**, **Read Message History**, **Add Reactions**
52
- 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
53
107
 
54
108
  ### 2. Create Discord channels for your projects
55
109
 
@@ -62,11 +116,17 @@ npm install -g multi-project-gateway
62
116
  mpg init
63
117
  ```
64
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
+
65
125
  The init wizard will:
66
126
  - Check that `claude` CLI is available
67
127
  - Ask for your Discord bot token
68
128
  - Walk you through adding projects (name, directory path, channel ID)
69
- - Generate `config.json` and `.env`
129
+ - Generate `config.json` and `.env` (in CWD or `~/.mpg/profiles/<name>/` when using `--profile`)
70
130
 
71
131
  Or set up manually by cloning:
72
132
 
@@ -106,9 +166,11 @@ Create `config.json`:
106
166
  ### 4. Start the gateway
107
167
 
108
168
  ```bash
109
- 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
110
172
  # or
111
- npm run dev # development (no build step)
173
+ npm run dev # development (no build step)
112
174
  # or
113
175
  npm run build && npm start # production
114
176
  ```
@@ -133,9 +195,79 @@ Commands:
133
195
  start Start the gateway (default)
134
196
  init Interactive setup wizard
135
197
  status Show session status from disk
198
+ logs Show structured gateway logs
136
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)
206
+ ```
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
137
263
  ```
138
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
+
139
271
  ## Configuration
140
272
 
141
273
  ### `config.json`
@@ -145,16 +277,102 @@ Commands:
145
277
  | `defaults.idleTimeoutMs` | number | `1800000` (30 min) | Session idle timeout before cleanup |
146
278
  | `defaults.maxConcurrentSessions` | number | `4` | Max concurrent Claude processes |
147
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`) |
148
288
  | `projects.<channelId>.name` | string | channel ID | Display name for the project |
149
289
  | `projects.<channelId>.directory` | string | **required** | Absolute path to the project directory |
150
290
  | `projects.<channelId>.idleTimeoutMs` | number | inherits default | Per-project idle timeout override |
151
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.
152
369
 
153
370
  ### Environment variables
154
371
 
155
372
  | Variable | Required | Description |
156
373
  |----------|----------|-------------|
157
374
  | `DISCORD_BOT_TOKEN` | Yes | Discord bot token |
375
+ | `MPG_HOME` | No | Override config home directory (default: `~/.mpg`) |
158
376
 
159
377
  ### Resuming sessions from terminal
160
378
 
@@ -190,20 +408,52 @@ The gateway responds to commands in any mapped Discord channel:
190
408
  | `!session <name>` | Inspect a specific project's session (ID, idle time, queue) |
191
409
  | `!restart <name>` | Reset a session (fresh context, keeps worktree) |
192
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 |
193
413
  | `!help` | Show available commands |
194
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
+
195
435
  ## Architecture
196
436
 
197
437
  | Module | Responsibility |
198
438
  |--------|---------------|
199
439
  | `src/cli.ts` | CLI entry point — `mpg start`, `mpg init`, `mpg status` |
200
- | `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`) |
201
442
  | `src/config.ts` | Validates and merges `config.json` with defaults |
202
443
  | `src/router.ts` | Maps channel IDs to project configs; threads resolve to their own session using the parent channel's project config |
203
444
  | `src/session-manager.ts` | One session per channel/thread, queues concurrent messages, manages idle timeouts |
204
445
  | `src/session-store.ts` | Persists session IDs to `.sessions.json` for resume across restarts |
205
446
  | `src/claude-cli.ts` | Spawns `claude --print` subprocess, parses JSON output |
206
- | `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 |
207
457
 
208
458
  ## Scripts
209
459
 
@@ -221,7 +471,7 @@ The gateway responds to commands in any mapped Discord channel:
221
471
  - **One message at a time per project** — concurrent messages to the same project are queued
222
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
223
473
  - **Local only** — the gateway runs on the same machine as the project directories
224
- - **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
225
475
 
226
476
  ## License
227
477