multi-project-gateway 0.2.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/LICENSE +21 -0
- package/README.md +213 -0
- package/dist/cli.js +705 -0
- package/dist/cli.js.map +1 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 yama-kei
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# multi-project-gateway
|
|
2
|
+
|
|
3
|
+
A Discord bot that routes channel messages to per-project [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI sessions. Each Discord channel maps to a local project directory, and the gateway manages Claude Code sessions, concurrency, and persistence automatically.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
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 <------'
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
1. User posts a message in a mapped Discord channel
|
|
15
|
+
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
|
+
|
|
20
|
+
## Security model
|
|
21
|
+
|
|
22
|
+
By default, each Claude session is restricted to its project directory using `--permission-mode acceptEdits`. This means:
|
|
23
|
+
|
|
24
|
+
- Claude can **read and edit files** within the project directory
|
|
25
|
+
- Claude **cannot access files** outside the project directory
|
|
26
|
+
- Claude **cannot run arbitrary shell commands** without approval (which is auto-denied in `--print` mode)
|
|
27
|
+
|
|
28
|
+
**Important considerations:**
|
|
29
|
+
- Anyone who can post in a mapped Discord channel can instruct Claude to read and modify files in that project's directory
|
|
30
|
+
- Only map channels that trusted users have access to
|
|
31
|
+
- For stricter control, use `--allowed-tools` in `claudeArgs` to whitelist specific tools
|
|
32
|
+
- 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
|
+
|
|
34
|
+
## Prerequisites
|
|
35
|
+
|
|
36
|
+
- **Node.js** 20+
|
|
37
|
+
- **Claude Code CLI** installed and authenticated (`claude` on PATH)
|
|
38
|
+
- **Discord bot** token
|
|
39
|
+
|
|
40
|
+
## Setup guide
|
|
41
|
+
|
|
42
|
+
### 1. Create a Discord bot
|
|
43
|
+
|
|
44
|
+
1. Go to the [Discord Developer Portal](https://discord.com/developers/applications)
|
|
45
|
+
2. Click **New Application**, give it a name
|
|
46
|
+
3. Go to **Bot** in the sidebar
|
|
47
|
+
4. Click **Reset Token** and copy the token (you'll need it in step 3)
|
|
48
|
+
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
|
|
52
|
+
|
|
53
|
+
### 2. Create Discord channels for your projects
|
|
54
|
+
|
|
55
|
+
Create a text channel for each project you want to manage (e.g., `#my-app`, `#my-api`). You'll need the channel IDs — enable Developer Mode in Discord settings (App Settings > Advanced > Developer Mode), then right-click a channel and select **Copy Channel ID**.
|
|
56
|
+
|
|
57
|
+
### 3. Install and configure the gateway
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm install -g multi-project-gateway
|
|
61
|
+
mpg init
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The init wizard will:
|
|
65
|
+
- Check that `claude` CLI is available
|
|
66
|
+
- Ask for your Discord bot token
|
|
67
|
+
- Walk you through adding projects (name, directory path, channel ID)
|
|
68
|
+
- Generate `config.json` and `.env`
|
|
69
|
+
|
|
70
|
+
Or set up manually by cloning:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git clone https://github.com/yama-kei/multi-project-gateway.git
|
|
74
|
+
cd multi-project-gateway
|
|
75
|
+
npm install
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Create `.env`:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
DISCORD_BOT_TOKEN=your-bot-token-here
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Create `config.json`:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"defaults": {
|
|
89
|
+
"idleTimeoutMs": 1800000,
|
|
90
|
+
"maxConcurrentSessions": 4,
|
|
91
|
+
"claudeArgs": [
|
|
92
|
+
"--permission-mode", "acceptEdits",
|
|
93
|
+
"--output-format", "json"
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
"projects": {
|
|
97
|
+
"DISCORD_CHANNEL_ID": {
|
|
98
|
+
"name": "MyProject",
|
|
99
|
+
"directory": "/absolute/path/to/project"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 4. Start the gateway
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
mpg start # if installed globally
|
|
109
|
+
# or
|
|
110
|
+
npm run dev # development (no build step)
|
|
111
|
+
# or
|
|
112
|
+
npm run build && npm start # production
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
You should see:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
Loaded N project(s) from config
|
|
119
|
+
Gateway connected as YourBot#1234
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 5. Use it
|
|
123
|
+
|
|
124
|
+
Post a message in any mapped Discord channel. The bot reacts with an eye emoji, forwards your message to Claude Code running in the project directory, and sends back the response.
|
|
125
|
+
|
|
126
|
+
## CLI
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
mpg <command>
|
|
130
|
+
|
|
131
|
+
Commands:
|
|
132
|
+
start Start the gateway (default)
|
|
133
|
+
init Interactive setup wizard
|
|
134
|
+
status Show session status from disk
|
|
135
|
+
help Show help
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Configuration
|
|
139
|
+
|
|
140
|
+
### `config.json`
|
|
141
|
+
|
|
142
|
+
| Field | Type | Default | Description |
|
|
143
|
+
|-------|------|---------|-------------|
|
|
144
|
+
| `defaults.idleTimeoutMs` | number | `1800000` (30 min) | Session idle timeout before cleanup |
|
|
145
|
+
| `defaults.maxConcurrentSessions` | number | `4` | Max concurrent Claude processes |
|
|
146
|
+
| `defaults.claudeArgs` | string[] | `["--permission-mode", "acceptEdits", "--output-format", "json"]` | Args passed to every `claude` invocation |
|
|
147
|
+
| `projects.<channelId>.name` | string | channel ID | Display name for the project |
|
|
148
|
+
| `projects.<channelId>.directory` | string | **required** | Absolute path to the project directory |
|
|
149
|
+
| `projects.<channelId>.idleTimeoutMs` | number | inherits default | Per-project idle timeout override |
|
|
150
|
+
| `projects.<channelId>.claudeArgs` | string[] | inherits default | Per-project Claude args override |
|
|
151
|
+
|
|
152
|
+
### Environment variables
|
|
153
|
+
|
|
154
|
+
| Variable | Required | Description |
|
|
155
|
+
|----------|----------|-------------|
|
|
156
|
+
| `DISCORD_BOT_TOKEN` | Yes | Discord bot token |
|
|
157
|
+
|
|
158
|
+
### Resuming sessions from terminal
|
|
159
|
+
|
|
160
|
+
Each Claude session started by the gateway can be resumed interactively. Use `!session <name>` in Discord to get the session ID, then:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
cd /path/to/project # must match the project directory in config.json
|
|
164
|
+
claude --resume <session-id>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**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
|
+
|
|
169
|
+
## Discord commands
|
|
170
|
+
|
|
171
|
+
The gateway responds to commands in any mapped Discord channel:
|
|
172
|
+
|
|
173
|
+
| Command | Description |
|
|
174
|
+
|---------|-------------|
|
|
175
|
+
| `!sessions` | List all active sessions with idle time and queue depth |
|
|
176
|
+
| `!session <name>` | Inspect a specific project's session (ID, idle time, queue) |
|
|
177
|
+
| `!kill <name>` | Force-close a project's session |
|
|
178
|
+
| `!help` | Show available commands |
|
|
179
|
+
|
|
180
|
+
## Architecture
|
|
181
|
+
|
|
182
|
+
| Module | Responsibility |
|
|
183
|
+
|--------|---------------|
|
|
184
|
+
| `src/cli.ts` | CLI entry point — `mpg start`, `mpg init`, `mpg status` |
|
|
185
|
+
| `src/init.ts` | Interactive setup wizard |
|
|
186
|
+
| `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 |
|
|
189
|
+
| `src/session-store.ts` | Persists session IDs to `.sessions.json` for resume across restarts |
|
|
190
|
+
| `src/claude-cli.ts` | Spawns `claude --print` subprocess, parses JSON output |
|
|
191
|
+
| `src/discord.ts` | Discord.js client, message routing, response chunking |
|
|
192
|
+
|
|
193
|
+
## Scripts
|
|
194
|
+
|
|
195
|
+
| Command | Description |
|
|
196
|
+
|---------|-------------|
|
|
197
|
+
| `npm run dev` | Run with tsx (no build step) |
|
|
198
|
+
| `npm run build` | Bundle with tsup to `dist/` |
|
|
199
|
+
| `npm start` | Run bundled CLI |
|
|
200
|
+
| `npm test` | Run tests once |
|
|
201
|
+
| `npm run test:watch` | Run tests in watch mode |
|
|
202
|
+
|
|
203
|
+
## Limitations
|
|
204
|
+
|
|
205
|
+
- **Text only** — attachments and embeds are not forwarded to Claude
|
|
206
|
+
- **One message at a time per project** — concurrent messages to the same project are queued
|
|
207
|
+
- **Threads share parent session** — no per-thread isolation
|
|
208
|
+
- **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
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { resolve as resolve2 } from "path";
|
|
5
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
6
|
+
import { config as loadEnv } from "dotenv";
|
|
7
|
+
|
|
8
|
+
// src/config.ts
|
|
9
|
+
function loadConfig(raw) {
|
|
10
|
+
if (!raw || typeof raw !== "object") {
|
|
11
|
+
throw new Error("Config must be an object");
|
|
12
|
+
}
|
|
13
|
+
const obj = raw;
|
|
14
|
+
if (!obj.projects || typeof obj.projects !== "object") {
|
|
15
|
+
throw new Error('Config must have a "projects" object');
|
|
16
|
+
}
|
|
17
|
+
const projects = obj.projects;
|
|
18
|
+
const validated = {};
|
|
19
|
+
for (const [channelId, project] of Object.entries(projects)) {
|
|
20
|
+
if (!project || typeof project !== "object") {
|
|
21
|
+
throw new Error(`Project for channel ${channelId} must be an object`);
|
|
22
|
+
}
|
|
23
|
+
const p = project;
|
|
24
|
+
if (typeof p.directory !== "string" || !p.directory) {
|
|
25
|
+
throw new Error(`Project for channel ${channelId} must have a "directory" string`);
|
|
26
|
+
}
|
|
27
|
+
validated[channelId] = {
|
|
28
|
+
name: typeof p.name === "string" ? p.name : channelId,
|
|
29
|
+
directory: p.directory,
|
|
30
|
+
...p.idleTimeoutMs !== void 0 && { idleTimeoutMs: Number(p.idleTimeoutMs) },
|
|
31
|
+
...Array.isArray(p.claudeArgs) && { claudeArgs: p.claudeArgs }
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const defaults = obj.defaults ?? {};
|
|
35
|
+
return {
|
|
36
|
+
defaults: {
|
|
37
|
+
idleTimeoutMs: typeof defaults.idleTimeoutMs === "number" ? defaults.idleTimeoutMs : 18e5,
|
|
38
|
+
maxConcurrentSessions: typeof defaults.maxConcurrentSessions === "number" ? defaults.maxConcurrentSessions : 4,
|
|
39
|
+
claudeArgs: Array.isArray(defaults.claudeArgs) ? defaults.claudeArgs : ["--permission-mode", "acceptEdits", "--output-format", "json"]
|
|
40
|
+
},
|
|
41
|
+
projects: validated
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/router.ts
|
|
46
|
+
function createRouter(config) {
|
|
47
|
+
return {
|
|
48
|
+
resolve(channelId, parentChannelId) {
|
|
49
|
+
const project = config.projects[channelId];
|
|
50
|
+
if (project) {
|
|
51
|
+
return { channelId, name: project.name, directory: project.directory };
|
|
52
|
+
}
|
|
53
|
+
if (parentChannelId) {
|
|
54
|
+
const parentProject = config.projects[parentChannelId];
|
|
55
|
+
if (parentProject) {
|
|
56
|
+
return { channelId: parentChannelId, name: parentProject.name, directory: parentProject.directory };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/claude-cli.ts
|
|
65
|
+
import { spawn } from "child_process";
|
|
66
|
+
function parseClaudeJsonOutput(raw) {
|
|
67
|
+
const data = JSON.parse(raw);
|
|
68
|
+
return {
|
|
69
|
+
text: data.result ?? "",
|
|
70
|
+
sessionId: data.session_id ?? "",
|
|
71
|
+
isError: Boolean(data.is_error)
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function buildClaudeArgs(baseArgs, prompt, sessionId) {
|
|
75
|
+
const args2 = ["--print", ...baseArgs];
|
|
76
|
+
if (sessionId) {
|
|
77
|
+
args2.push("--resume", sessionId);
|
|
78
|
+
}
|
|
79
|
+
args2.push(prompt);
|
|
80
|
+
return args2;
|
|
81
|
+
}
|
|
82
|
+
function runClaude(cwd, baseArgs, prompt, sessionId) {
|
|
83
|
+
return new Promise((resolve3, reject) => {
|
|
84
|
+
const args2 = buildClaudeArgs(baseArgs, prompt, sessionId);
|
|
85
|
+
const proc = spawn("claude", args2, {
|
|
86
|
+
cwd,
|
|
87
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
88
|
+
});
|
|
89
|
+
let stdout = "";
|
|
90
|
+
let stderr = "";
|
|
91
|
+
proc.stdout.on("data", (chunk) => {
|
|
92
|
+
stdout += chunk.toString();
|
|
93
|
+
});
|
|
94
|
+
proc.stderr.on("data", (chunk) => {
|
|
95
|
+
stderr += chunk.toString();
|
|
96
|
+
});
|
|
97
|
+
proc.on("close", (code) => {
|
|
98
|
+
if (code !== 0) {
|
|
99
|
+
reject(new Error(`claude exited with code ${code}: ${stderr}`));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const result = parseClaudeJsonOutput(stdout.trim());
|
|
104
|
+
resolve3(result);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
reject(new Error(`Failed to parse claude output: ${stdout.slice(0, 200)}`));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
proc.on("error", (err) => {
|
|
110
|
+
reject(new Error(`Failed to spawn claude: ${err.message}`));
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/session-manager.ts
|
|
116
|
+
function createSessionManager(defaults, store) {
|
|
117
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
118
|
+
let activeProcesses = 0;
|
|
119
|
+
const waiters = [];
|
|
120
|
+
function persistSessions() {
|
|
121
|
+
if (!store) return;
|
|
122
|
+
const persisted = store.load();
|
|
123
|
+
for (const [key, s] of sessions) {
|
|
124
|
+
if (s.sessionId) {
|
|
125
|
+
persisted.set(key, {
|
|
126
|
+
sessionId: s.sessionId,
|
|
127
|
+
projectKey: s.projectKey,
|
|
128
|
+
cwd: s.cwd,
|
|
129
|
+
lastActivity: s.lastActivity
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
store.save(persisted);
|
|
134
|
+
}
|
|
135
|
+
async function acquireSlot() {
|
|
136
|
+
if (activeProcesses < defaults.maxConcurrentSessions) {
|
|
137
|
+
activeProcesses++;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
return new Promise((resolve3) => {
|
|
141
|
+
waiters.push(() => {
|
|
142
|
+
activeProcesses++;
|
|
143
|
+
resolve3();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function releaseSlot() {
|
|
148
|
+
activeProcesses--;
|
|
149
|
+
const next = waiters.shift();
|
|
150
|
+
if (next) next();
|
|
151
|
+
}
|
|
152
|
+
function resetIdleTimer(session) {
|
|
153
|
+
if (session.idleTimer) clearTimeout(session.idleTimer);
|
|
154
|
+
session.idleTimer = setTimeout(() => {
|
|
155
|
+
sessions.delete(session.projectKey);
|
|
156
|
+
}, defaults.idleTimeoutMs);
|
|
157
|
+
}
|
|
158
|
+
async function processQueue(session) {
|
|
159
|
+
if (session.processing || session.queue.length === 0) return;
|
|
160
|
+
session.processing = true;
|
|
161
|
+
while (session.queue.length > 0) {
|
|
162
|
+
const item = session.queue.shift();
|
|
163
|
+
await acquireSlot();
|
|
164
|
+
try {
|
|
165
|
+
const result = await runClaude(
|
|
166
|
+
session.cwd,
|
|
167
|
+
defaults.claudeArgs,
|
|
168
|
+
item.prompt,
|
|
169
|
+
session.sessionId
|
|
170
|
+
);
|
|
171
|
+
session.sessionId = result.sessionId || session.sessionId;
|
|
172
|
+
session.lastActivity = Date.now();
|
|
173
|
+
resetIdleTimer(session);
|
|
174
|
+
persistSessions();
|
|
175
|
+
item.resolve(result);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
if (session.sessionId) {
|
|
178
|
+
session.sessionId = void 0;
|
|
179
|
+
try {
|
|
180
|
+
const result = await runClaude(session.cwd, defaults.claudeArgs, item.prompt, void 0);
|
|
181
|
+
session.sessionId = result.sessionId || void 0;
|
|
182
|
+
session.lastActivity = Date.now();
|
|
183
|
+
resetIdleTimer(session);
|
|
184
|
+
persistSessions();
|
|
185
|
+
item.resolve(result);
|
|
186
|
+
} catch (retryErr) {
|
|
187
|
+
item.reject(retryErr instanceof Error ? retryErr : new Error(String(retryErr)));
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
item.reject(err instanceof Error ? err : new Error(String(err)));
|
|
191
|
+
}
|
|
192
|
+
} finally {
|
|
193
|
+
releaseSlot();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
session.processing = false;
|
|
197
|
+
}
|
|
198
|
+
function getOrCreateSession(projectKey, cwd) {
|
|
199
|
+
let session = sessions.get(projectKey);
|
|
200
|
+
if (!session) {
|
|
201
|
+
let restoredSessionId;
|
|
202
|
+
if (store) {
|
|
203
|
+
const persisted = store.load();
|
|
204
|
+
const entry = persisted.get(projectKey);
|
|
205
|
+
if (entry?.sessionId) {
|
|
206
|
+
restoredSessionId = entry.sessionId;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
session = {
|
|
210
|
+
sessionId: restoredSessionId,
|
|
211
|
+
projectKey,
|
|
212
|
+
cwd,
|
|
213
|
+
lastActivity: Date.now(),
|
|
214
|
+
processing: false,
|
|
215
|
+
queue: [],
|
|
216
|
+
idleTimer: null
|
|
217
|
+
};
|
|
218
|
+
sessions.set(projectKey, session);
|
|
219
|
+
resetIdleTimer(session);
|
|
220
|
+
}
|
|
221
|
+
return session;
|
|
222
|
+
}
|
|
223
|
+
if (store) {
|
|
224
|
+
const persisted = store.load();
|
|
225
|
+
for (const [key, entry] of persisted) {
|
|
226
|
+
sessions.set(key, {
|
|
227
|
+
sessionId: entry.sessionId,
|
|
228
|
+
projectKey: entry.projectKey,
|
|
229
|
+
cwd: entry.cwd,
|
|
230
|
+
lastActivity: entry.lastActivity,
|
|
231
|
+
processing: false,
|
|
232
|
+
queue: [],
|
|
233
|
+
idleTimer: null
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
for (const session of sessions.values()) {
|
|
237
|
+
resetIdleTimer(session);
|
|
238
|
+
}
|
|
239
|
+
if (persisted.size > 0) {
|
|
240
|
+
console.log(`Restored ${persisted.size} session(s) from disk`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
send(projectKey, cwd, prompt) {
|
|
245
|
+
const session = getOrCreateSession(projectKey, cwd);
|
|
246
|
+
return new Promise((resolve3, reject) => {
|
|
247
|
+
session.queue.push({ prompt, resolve: resolve3, reject });
|
|
248
|
+
processQueue(session);
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
getSession(projectKey) {
|
|
252
|
+
const session = sessions.get(projectKey);
|
|
253
|
+
if (!session) return void 0;
|
|
254
|
+
return {
|
|
255
|
+
sessionId: session.sessionId ?? "",
|
|
256
|
+
projectKey: session.projectKey,
|
|
257
|
+
lastActivity: session.lastActivity,
|
|
258
|
+
queueLength: session.queue.length
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
listSessions() {
|
|
262
|
+
return Array.from(sessions.values()).map((s) => ({
|
|
263
|
+
sessionId: s.sessionId ?? "",
|
|
264
|
+
projectKey: s.projectKey,
|
|
265
|
+
lastActivity: s.lastActivity,
|
|
266
|
+
queueLength: s.queue.length
|
|
267
|
+
}));
|
|
268
|
+
},
|
|
269
|
+
clearSession(projectKey) {
|
|
270
|
+
const session = sessions.get(projectKey);
|
|
271
|
+
if (!session) return false;
|
|
272
|
+
if (session.idleTimer) clearTimeout(session.idleTimer);
|
|
273
|
+
sessions.delete(projectKey);
|
|
274
|
+
persistSessions();
|
|
275
|
+
return true;
|
|
276
|
+
},
|
|
277
|
+
shutdown() {
|
|
278
|
+
persistSessions();
|
|
279
|
+
for (const session of sessions.values()) {
|
|
280
|
+
if (session.idleTimer) clearTimeout(session.idleTimer);
|
|
281
|
+
}
|
|
282
|
+
sessions.clear();
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/session-store.ts
|
|
288
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
289
|
+
import { dirname } from "path";
|
|
290
|
+
function createFileSessionStore(filePath) {
|
|
291
|
+
return {
|
|
292
|
+
load() {
|
|
293
|
+
try {
|
|
294
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
295
|
+
const entries = JSON.parse(raw);
|
|
296
|
+
const map = /* @__PURE__ */ new Map();
|
|
297
|
+
for (const entry of entries) {
|
|
298
|
+
if (entry.sessionId && entry.projectKey) {
|
|
299
|
+
map.set(entry.projectKey, entry);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return map;
|
|
303
|
+
} catch {
|
|
304
|
+
return /* @__PURE__ */ new Map();
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
save(sessions) {
|
|
308
|
+
const entries = Array.from(sessions.values());
|
|
309
|
+
try {
|
|
310
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
311
|
+
writeFileSync(filePath, JSON.stringify(entries, null, 2) + "\n");
|
|
312
|
+
} catch (err) {
|
|
313
|
+
console.error("Failed to persist sessions:", err);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/discord.ts
|
|
320
|
+
import { Client, GatewayIntentBits, Events } from "discord.js";
|
|
321
|
+
function chunkMessage(text, limit) {
|
|
322
|
+
if (text.length <= limit) return [text];
|
|
323
|
+
const chunks = [];
|
|
324
|
+
const lines = text.split("\n");
|
|
325
|
+
let current = "";
|
|
326
|
+
for (const line of lines) {
|
|
327
|
+
if (line.length > limit) {
|
|
328
|
+
if (current) {
|
|
329
|
+
chunks.push(current);
|
|
330
|
+
current = "";
|
|
331
|
+
}
|
|
332
|
+
for (let i = 0; i < line.length; i += limit) {
|
|
333
|
+
chunks.push(line.slice(i, i + limit));
|
|
334
|
+
}
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const candidate = current ? `${current}
|
|
338
|
+
${line}` : line;
|
|
339
|
+
if (candidate.length > limit) {
|
|
340
|
+
chunks.push(current);
|
|
341
|
+
current = line;
|
|
342
|
+
} else {
|
|
343
|
+
current = candidate;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (current || chunks.length === 0) {
|
|
347
|
+
chunks.push(current);
|
|
348
|
+
}
|
|
349
|
+
return chunks;
|
|
350
|
+
}
|
|
351
|
+
function resolveProjectName(config, channelId) {
|
|
352
|
+
return config.projects[channelId]?.name ?? channelId;
|
|
353
|
+
}
|
|
354
|
+
function findProjectByName(config, name) {
|
|
355
|
+
const lower = name.toLowerCase();
|
|
356
|
+
for (const [channelId, project] of Object.entries(config.projects)) {
|
|
357
|
+
if (project.name.toLowerCase() === lower) {
|
|
358
|
+
return { channelId, name: project.name };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
function formatTimeSince(timestamp) {
|
|
364
|
+
const seconds = Math.floor((Date.now() - timestamp) / 1e3);
|
|
365
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
366
|
+
const minutes = Math.floor(seconds / 60);
|
|
367
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
368
|
+
const hours = Math.floor(minutes / 60);
|
|
369
|
+
return `${hours}h ${minutes % 60}m ago`;
|
|
370
|
+
}
|
|
371
|
+
function handleCommand(command2, config, sessionManager) {
|
|
372
|
+
const parts = command2.trim().split(/\s+/);
|
|
373
|
+
const cmd = parts[0]?.toLowerCase();
|
|
374
|
+
if (cmd === "!sessions") {
|
|
375
|
+
const allSessions = sessionManager.listSessions();
|
|
376
|
+
if (allSessions.length === 0) {
|
|
377
|
+
return "No active sessions.";
|
|
378
|
+
}
|
|
379
|
+
const lines = allSessions.map((s) => {
|
|
380
|
+
const name = resolveProjectName(config, s.projectKey);
|
|
381
|
+
const idle = formatTimeSince(s.lastActivity);
|
|
382
|
+
const queue = s.queueLength > 0 ? ` | queue: ${s.queueLength}` : "";
|
|
383
|
+
const sid = s.sessionId ? ` | \`${s.sessionId.slice(0, 8)}\u2026\`` : "";
|
|
384
|
+
return `- **${name}** \u2014 last active ${idle}${queue}${sid}`;
|
|
385
|
+
});
|
|
386
|
+
return `**Active sessions (${allSessions.length})**
|
|
387
|
+
${lines.join("\n")}`;
|
|
388
|
+
}
|
|
389
|
+
if (cmd === "!session") {
|
|
390
|
+
const name = parts.slice(1).join(" ");
|
|
391
|
+
if (!name) return "Usage: `!session <project name>`";
|
|
392
|
+
const project = findProjectByName(config, name);
|
|
393
|
+
if (!project) return `No project found matching "${name}".`;
|
|
394
|
+
const info = sessionManager.getSession(project.channelId);
|
|
395
|
+
if (!info) return `**${project.name}** \u2014 no active session.`;
|
|
396
|
+
const idle = formatTimeSince(info.lastActivity);
|
|
397
|
+
const sid = info.sessionId || "none";
|
|
398
|
+
return [
|
|
399
|
+
`**${project.name}**`,
|
|
400
|
+
`Session ID: \`${sid}\``,
|
|
401
|
+
`Last active: ${idle}`,
|
|
402
|
+
`Queue depth: ${info.queueLength}`
|
|
403
|
+
].join("\n");
|
|
404
|
+
}
|
|
405
|
+
if (cmd === "!kill") {
|
|
406
|
+
const name = parts.slice(1).join(" ");
|
|
407
|
+
if (!name) return "Usage: `!kill <project name>`";
|
|
408
|
+
const project = findProjectByName(config, name);
|
|
409
|
+
if (!project) return `No project found matching "${name}".`;
|
|
410
|
+
const cleared = sessionManager.clearSession(project.channelId);
|
|
411
|
+
if (cleared) return `Session for **${project.name}** cleared.`;
|
|
412
|
+
return `**${project.name}** \u2014 no active session to clear.`;
|
|
413
|
+
}
|
|
414
|
+
if (cmd === "!help") {
|
|
415
|
+
return [
|
|
416
|
+
"**Gateway commands**",
|
|
417
|
+
"`!sessions` \u2014 list all active sessions",
|
|
418
|
+
"`!session <name>` \u2014 inspect a specific project session",
|
|
419
|
+
"`!kill <name>` \u2014 force-close a project session",
|
|
420
|
+
"`!help` \u2014 show this message"
|
|
421
|
+
].join("\n");
|
|
422
|
+
}
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
function createDiscordBot(router, sessionManager, config) {
|
|
426
|
+
const client = new Client({
|
|
427
|
+
intents: [
|
|
428
|
+
GatewayIntentBits.Guilds,
|
|
429
|
+
GatewayIntentBits.GuildMessages,
|
|
430
|
+
GatewayIntentBits.MessageContent
|
|
431
|
+
]
|
|
432
|
+
});
|
|
433
|
+
client.on(Events.MessageCreate, async (message) => {
|
|
434
|
+
if (message.author.bot) return;
|
|
435
|
+
if (!("send" in message.channel)) return;
|
|
436
|
+
if (message.content.startsWith("!")) {
|
|
437
|
+
const parentId2 = message.channel.isThread() ? message.channel.parentId ?? void 0 : void 0;
|
|
438
|
+
const resolved2 = router.resolve(message.channelId, parentId2);
|
|
439
|
+
if (resolved2) {
|
|
440
|
+
const response = handleCommand(message.content, config, sessionManager);
|
|
441
|
+
if (response) {
|
|
442
|
+
await message.channel.send(response);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const parentId = message.channel.isThread() ? message.channel.parentId ?? void 0 : void 0;
|
|
448
|
+
const resolved = router.resolve(message.channelId, parentId);
|
|
449
|
+
if (!resolved) return;
|
|
450
|
+
try {
|
|
451
|
+
await message.react("\u{1F440}");
|
|
452
|
+
} catch {
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
const result = await sessionManager.send(
|
|
456
|
+
resolved.channelId,
|
|
457
|
+
resolved.directory,
|
|
458
|
+
message.content
|
|
459
|
+
);
|
|
460
|
+
const chunks = chunkMessage(result.text, 2e3);
|
|
461
|
+
for (const chunk of chunks) {
|
|
462
|
+
await message.channel.send(chunk);
|
|
463
|
+
}
|
|
464
|
+
} catch (err) {
|
|
465
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
466
|
+
await message.channel.send(
|
|
467
|
+
`**Error** (${resolved.name}): ${errorMsg.slice(0, 1800)}`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
return {
|
|
472
|
+
async start(token) {
|
|
473
|
+
await client.login(token);
|
|
474
|
+
console.log(`Gateway connected as ${client.user?.tag}`);
|
|
475
|
+
},
|
|
476
|
+
stop() {
|
|
477
|
+
client.destroy();
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/init.ts
|
|
483
|
+
import { createInterface } from "readline";
|
|
484
|
+
import { writeFileSync as writeFileSync2, existsSync } from "fs";
|
|
485
|
+
import { resolve } from "path";
|
|
486
|
+
import { execSync } from "child_process";
|
|
487
|
+
function createPrompt() {
|
|
488
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
489
|
+
return (question) => new Promise((resolve3) => rl.question(question, (answer) => resolve3(answer.trim())));
|
|
490
|
+
}
|
|
491
|
+
async function runInit() {
|
|
492
|
+
const ask = createPrompt();
|
|
493
|
+
const configDir = process.cwd();
|
|
494
|
+
console.log("\nmpg init \u2014 set up multi-project gateway\n");
|
|
495
|
+
try {
|
|
496
|
+
execSync("claude --version", { stdio: "pipe" });
|
|
497
|
+
console.log("Claude CLI found.");
|
|
498
|
+
} catch {
|
|
499
|
+
console.warn("Warning: `claude` not found on PATH. Make sure it is installed before starting the gateway.");
|
|
500
|
+
}
|
|
501
|
+
let token = process.env.DISCORD_BOT_TOKEN ?? "";
|
|
502
|
+
const inputToken = await ask(`Discord bot token${token ? " (press Enter to keep existing)" : ""}: `);
|
|
503
|
+
if (inputToken) token = inputToken;
|
|
504
|
+
if (!token) {
|
|
505
|
+
console.error("A Discord bot token is required. Create one at https://discord.com/developers/applications");
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
const envPath = resolve(configDir, ".env");
|
|
509
|
+
writeFileSync2(envPath, `DISCORD_BOT_TOKEN=${token}
|
|
510
|
+
`);
|
|
511
|
+
console.log(`Wrote ${envPath}`);
|
|
512
|
+
const projects = [];
|
|
513
|
+
const configPath = resolve(configDir, "config.json");
|
|
514
|
+
if (existsSync(configPath)) {
|
|
515
|
+
try {
|
|
516
|
+
const existing = JSON.parse(
|
|
517
|
+
(await import("fs")).readFileSync(configPath, "utf-8")
|
|
518
|
+
);
|
|
519
|
+
if (existing.projects) {
|
|
520
|
+
for (const [channelId, project] of Object.entries(existing.projects)) {
|
|
521
|
+
const p = project;
|
|
522
|
+
projects.push({ name: p.name ?? channelId, directory: p.directory, channelId });
|
|
523
|
+
}
|
|
524
|
+
if (projects.length > 0) {
|
|
525
|
+
console.log(`
|
|
526
|
+
Existing projects (${projects.length}):`);
|
|
527
|
+
for (const p of projects) {
|
|
528
|
+
console.log(` ${p.name} \u2192 ${p.directory} (${p.channelId})`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
} catch {
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
console.log("\nAdd projects (empty name to finish):\n");
|
|
536
|
+
while (true) {
|
|
537
|
+
const name = await ask("Project name: ");
|
|
538
|
+
if (!name) break;
|
|
539
|
+
const directory = await ask("Project directory (absolute path): ");
|
|
540
|
+
if (!directory) {
|
|
541
|
+
console.log("Directory is required, skipping.");
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
if (!existsSync(directory)) {
|
|
545
|
+
console.warn(`Warning: ${directory} does not exist.`);
|
|
546
|
+
}
|
|
547
|
+
const channelId = await ask("Discord channel ID: ");
|
|
548
|
+
if (!channelId) {
|
|
549
|
+
console.log("Channel ID is required, skipping.");
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
projects.push({ name, directory, channelId });
|
|
553
|
+
console.log(`Added ${name}
|
|
554
|
+
`);
|
|
555
|
+
}
|
|
556
|
+
if (projects.length === 0) {
|
|
557
|
+
console.log("No projects configured. You can edit config.json later.");
|
|
558
|
+
}
|
|
559
|
+
const config = {
|
|
560
|
+
defaults: {
|
|
561
|
+
idleTimeoutMs: 18e5,
|
|
562
|
+
maxConcurrentSessions: 4,
|
|
563
|
+
claudeArgs: ["--permission-mode", "acceptEdits", "--output-format", "json"]
|
|
564
|
+
},
|
|
565
|
+
projects: Object.fromEntries(
|
|
566
|
+
projects.map((p) => [p.channelId, { name: p.name, directory: p.directory }])
|
|
567
|
+
)
|
|
568
|
+
};
|
|
569
|
+
writeFileSync2(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
570
|
+
console.log(`Wrote ${configPath}`);
|
|
571
|
+
console.log("\nSetup complete! Run `mpg start` to launch the gateway.");
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/cli.ts
|
|
575
|
+
var args = process.argv.slice(2);
|
|
576
|
+
var command = args[0] ?? "start";
|
|
577
|
+
async function main() {
|
|
578
|
+
switch (command) {
|
|
579
|
+
case "start":
|
|
580
|
+
return start();
|
|
581
|
+
case "init":
|
|
582
|
+
return runInit();
|
|
583
|
+
case "status":
|
|
584
|
+
return status();
|
|
585
|
+
case "help":
|
|
586
|
+
case "--help":
|
|
587
|
+
case "-h":
|
|
588
|
+
return help();
|
|
589
|
+
case "--version":
|
|
590
|
+
case "-v":
|
|
591
|
+
return version();
|
|
592
|
+
default:
|
|
593
|
+
console.error(`Unknown command: ${command}`);
|
|
594
|
+
help();
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function help() {
|
|
599
|
+
console.log(`
|
|
600
|
+
mpg \u2014 multi-project gateway for Claude Code
|
|
601
|
+
|
|
602
|
+
Usage: mpg <command>
|
|
603
|
+
|
|
604
|
+
Commands:
|
|
605
|
+
start Start the gateway (default)
|
|
606
|
+
init Interactive setup wizard
|
|
607
|
+
status Show session status
|
|
608
|
+
help Show this message
|
|
609
|
+
|
|
610
|
+
Options:
|
|
611
|
+
-v, --version Show version
|
|
612
|
+
-h, --help Show this message
|
|
613
|
+
`.trim());
|
|
614
|
+
}
|
|
615
|
+
function version() {
|
|
616
|
+
const pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf-8"));
|
|
617
|
+
console.log(`mpg v${pkg.version}`);
|
|
618
|
+
}
|
|
619
|
+
function start() {
|
|
620
|
+
const configDir = process.cwd();
|
|
621
|
+
const envPath = resolve2(configDir, ".env");
|
|
622
|
+
if (existsSync2(envPath)) {
|
|
623
|
+
loadEnv({ path: envPath });
|
|
624
|
+
}
|
|
625
|
+
const token = process.env.DISCORD_BOT_TOKEN;
|
|
626
|
+
if (!token) {
|
|
627
|
+
console.error("DISCORD_BOT_TOKEN is not set. Run `mpg init` or set it in .env");
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
630
|
+
const configPath = resolve2(configDir, "config.json");
|
|
631
|
+
if (!existsSync2(configPath)) {
|
|
632
|
+
console.error("config.json not found. Run `mpg init` to create one.");
|
|
633
|
+
process.exit(1);
|
|
634
|
+
}
|
|
635
|
+
const rawConfig = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
636
|
+
const config = loadConfig(rawConfig);
|
|
637
|
+
const projectCount = Object.keys(config.projects).length;
|
|
638
|
+
if (projectCount === 0) {
|
|
639
|
+
console.error("No projects configured in config.json");
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
for (const [channelId, project] of Object.entries(config.projects)) {
|
|
643
|
+
if (!existsSync2(project.directory)) {
|
|
644
|
+
console.warn(`Warning: directory not found for ${project.name}: ${project.directory}`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
console.log(`Loaded ${projectCount} project(s) from config`);
|
|
648
|
+
const router = createRouter(config);
|
|
649
|
+
const sessionStore = createFileSessionStore(resolve2(configDir, ".sessions.json"));
|
|
650
|
+
const sessionManager = createSessionManager(config.defaults, sessionStore);
|
|
651
|
+
const bot = createDiscordBot(router, sessionManager, config);
|
|
652
|
+
function shutdown() {
|
|
653
|
+
console.log("Shutting down...");
|
|
654
|
+
sessionManager.shutdown();
|
|
655
|
+
bot.stop();
|
|
656
|
+
process.exit(0);
|
|
657
|
+
}
|
|
658
|
+
process.on("SIGINT", shutdown);
|
|
659
|
+
process.on("SIGTERM", shutdown);
|
|
660
|
+
bot.start(token).catch((err) => {
|
|
661
|
+
console.error("Failed to start bot:", err);
|
|
662
|
+
process.exit(1);
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
function status() {
|
|
666
|
+
const configDir = process.cwd();
|
|
667
|
+
const sessionsPath = resolve2(configDir, ".sessions.json");
|
|
668
|
+
if (!existsSync2(sessionsPath)) {
|
|
669
|
+
console.log("No sessions file found. Is the gateway running?");
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const configPath = resolve2(configDir, "config.json");
|
|
673
|
+
let projectNames = {};
|
|
674
|
+
if (existsSync2(configPath)) {
|
|
675
|
+
try {
|
|
676
|
+
const raw = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
677
|
+
const config = loadConfig(raw);
|
|
678
|
+
for (const [channelId, project] of Object.entries(config.projects)) {
|
|
679
|
+
projectNames[channelId] = project.name;
|
|
680
|
+
}
|
|
681
|
+
} catch {
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
const sessions = JSON.parse(readFileSync2(sessionsPath, "utf-8"));
|
|
685
|
+
if (sessions.length === 0) {
|
|
686
|
+
console.log("No active sessions.");
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
console.log(`Sessions (${sessions.length}):
|
|
690
|
+
`);
|
|
691
|
+
for (const s of sessions) {
|
|
692
|
+
const name = projectNames[s.projectKey] ?? s.projectKey;
|
|
693
|
+
const ago = Math.floor((Date.now() - s.lastActivity) / 6e4);
|
|
694
|
+
console.log(` ${name}`);
|
|
695
|
+
console.log(` Session: ${s.sessionId}`);
|
|
696
|
+
console.log(` Dir: ${s.cwd}`);
|
|
697
|
+
console.log(` Idle: ${ago}m`);
|
|
698
|
+
console.log();
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
main().catch((err) => {
|
|
702
|
+
console.error(err);
|
|
703
|
+
process.exit(1);
|
|
704
|
+
});
|
|
705
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/config.ts","../src/router.ts","../src/claude-cli.ts","../src/session-manager.ts","../src/session-store.ts","../src/discord.ts","../src/init.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { config as loadEnv } from 'dotenv';\nimport { loadConfig } from './config.js';\nimport { createRouter } from './router.js';\nimport { createSessionManager } from './session-manager.js';\nimport { createFileSessionStore } from './session-store.js';\nimport { createDiscordBot } from './discord.js';\nimport { runInit } from './init.js';\n\nconst args = process.argv.slice(2);\nconst command = args[0] ?? 'start';\n\nasync function main() {\n switch (command) {\n case 'start':\n return start();\n case 'init':\n return runInit();\n case 'status':\n return status();\n case 'help':\n case '--help':\n case '-h':\n return help();\n case '--version':\n case '-v':\n return version();\n default:\n console.error(`Unknown command: ${command}`);\n help();\n process.exit(1);\n }\n}\n\nfunction help() {\n console.log(`\nmpg — multi-project gateway for Claude Code\n\nUsage: mpg <command>\n\nCommands:\n start Start the gateway (default)\n init Interactive setup wizard\n status Show session status\n help Show this message\n\nOptions:\n -v, --version Show version\n -h, --help Show this message\n`.trim());\n}\n\nfunction version() {\n const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));\n console.log(`mpg v${pkg.version}`);\n}\n\nfunction start() {\n const configDir = process.cwd();\n const envPath = resolve(configDir, '.env');\n if (existsSync(envPath)) {\n loadEnv({ path: envPath });\n }\n\n const token = process.env.DISCORD_BOT_TOKEN;\n if (!token) {\n console.error('DISCORD_BOT_TOKEN is not set. Run `mpg init` or set it in .env');\n process.exit(1);\n }\n\n const configPath = resolve(configDir, 'config.json');\n if (!existsSync(configPath)) {\n console.error('config.json not found. Run `mpg init` to create one.');\n process.exit(1);\n }\n\n const rawConfig = JSON.parse(readFileSync(configPath, 'utf-8'));\n const config = loadConfig(rawConfig);\n\n const projectCount = Object.keys(config.projects).length;\n if (projectCount === 0) {\n console.error('No projects configured in config.json');\n process.exit(1);\n }\n\n // Validate project directories exist\n for (const [channelId, project] of Object.entries(config.projects)) {\n if (!existsSync(project.directory)) {\n console.warn(`Warning: directory not found for ${project.name}: ${project.directory}`);\n }\n }\n\n console.log(`Loaded ${projectCount} project(s) from config`);\n\n const router = createRouter(config);\n const sessionStore = createFileSessionStore(resolve(configDir, '.sessions.json'));\n const sessionManager = createSessionManager(config.defaults, sessionStore);\n const bot = createDiscordBot(router, sessionManager, config);\n\n function shutdown() {\n console.log('Shutting down...');\n sessionManager.shutdown();\n bot.stop();\n process.exit(0);\n }\n\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n\n bot.start(token).catch((err) => {\n console.error('Failed to start bot:', err);\n process.exit(1);\n });\n}\n\nfunction status() {\n const configDir = process.cwd();\n const sessionsPath = resolve(configDir, '.sessions.json');\n\n if (!existsSync(sessionsPath)) {\n console.log('No sessions file found. Is the gateway running?');\n return;\n }\n\n const configPath = resolve(configDir, 'config.json');\n let projectNames: Record<string, string> = {};\n if (existsSync(configPath)) {\n try {\n const raw = JSON.parse(readFileSync(configPath, 'utf-8'));\n const config = loadConfig(raw);\n for (const [channelId, project] of Object.entries(config.projects)) {\n projectNames[channelId] = project.name;\n }\n } catch {\n // ignore config errors for status\n }\n }\n\n const sessions = JSON.parse(readFileSync(sessionsPath, 'utf-8')) as Array<{\n sessionId: string;\n projectKey: string;\n cwd: string;\n lastActivity: number;\n }>;\n\n if (sessions.length === 0) {\n console.log('No active sessions.');\n return;\n }\n\n console.log(`Sessions (${sessions.length}):\\n`);\n for (const s of sessions) {\n const name = projectNames[s.projectKey] ?? s.projectKey;\n const ago = Math.floor((Date.now() - s.lastActivity) / 60000);\n console.log(` ${name}`);\n console.log(` Session: ${s.sessionId}`);\n console.log(` Dir: ${s.cwd}`);\n console.log(` Idle: ${ago}m`);\n console.log();\n }\n}\n\nmain().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n","export interface ProjectConfig {\n name: string;\n directory: string;\n idleTimeoutMs?: number;\n claudeArgs?: string[];\n}\n\nexport interface GatewayDefaults {\n idleTimeoutMs: number;\n maxConcurrentSessions: number;\n claudeArgs: string[];\n}\n\nexport interface GatewayConfig {\n defaults: GatewayDefaults;\n projects: Record<string, ProjectConfig>;\n}\n\nexport function loadConfig(raw: unknown): GatewayConfig {\n if (!raw || typeof raw !== 'object') {\n throw new Error('Config must be an object');\n }\n\n const obj = raw as Record<string, unknown>;\n\n if (!obj.projects || typeof obj.projects !== 'object') {\n throw new Error('Config must have a \"projects\" object');\n }\n\n const projects = obj.projects as Record<string, unknown>;\n const validated: Record<string, ProjectConfig> = {};\n\n for (const [channelId, project] of Object.entries(projects)) {\n if (!project || typeof project !== 'object') {\n throw new Error(`Project for channel ${channelId} must be an object`);\n }\n const p = project as Record<string, unknown>;\n if (typeof p.directory !== 'string' || !p.directory) {\n throw new Error(`Project for channel ${channelId} must have a \"directory\" string`);\n }\n validated[channelId] = {\n name: typeof p.name === 'string' ? p.name : channelId,\n directory: p.directory,\n ...(p.idleTimeoutMs !== undefined && { idleTimeoutMs: Number(p.idleTimeoutMs) }),\n ...(Array.isArray(p.claudeArgs) && { claudeArgs: p.claudeArgs as string[] }),\n };\n }\n\n const defaults = (obj.defaults ?? {}) as Record<string, unknown>;\n\n return {\n defaults: {\n idleTimeoutMs: typeof defaults.idleTimeoutMs === 'number' ? defaults.idleTimeoutMs : 1800000,\n maxConcurrentSessions: typeof defaults.maxConcurrentSessions === 'number' ? defaults.maxConcurrentSessions : 4,\n claudeArgs: Array.isArray(defaults.claudeArgs) ? (defaults.claudeArgs as string[]) : ['--permission-mode', 'acceptEdits', '--output-format', 'json'],\n },\n projects: validated,\n };\n}\n","import type { GatewayConfig, ProjectConfig } from './config.js';\n\nexport interface ResolvedProject {\n channelId: string;\n name: string;\n directory: string;\n}\n\nexport interface Router {\n resolve(channelId: string, parentChannelId?: string): ResolvedProject | null;\n}\n\nexport function createRouter(config: GatewayConfig): Router {\n return {\n resolve(channelId: string, parentChannelId?: string): ResolvedProject | null {\n const project = config.projects[channelId];\n if (project) {\n return { channelId, name: project.name, directory: project.directory };\n }\n\n if (parentChannelId) {\n const parentProject = config.projects[parentChannelId];\n if (parentProject) {\n return { channelId: parentChannelId, name: parentProject.name, directory: parentProject.directory };\n }\n }\n\n return null;\n },\n };\n}\n","import { spawn } from 'node:child_process';\n\nexport interface ClaudeResult {\n text: string;\n sessionId: string;\n isError: boolean;\n}\n\nexport function parseClaudeJsonOutput(raw: string): ClaudeResult {\n const data = JSON.parse(raw);\n return {\n text: data.result ?? '',\n sessionId: data.session_id ?? '',\n isError: Boolean(data.is_error),\n };\n}\n\nexport function buildClaudeArgs(\n baseArgs: string[],\n prompt: string,\n sessionId: string | undefined,\n): string[] {\n const args = ['--print', ...baseArgs];\n if (sessionId) {\n args.push('--resume', sessionId);\n }\n args.push(prompt);\n return args;\n}\n\nexport function runClaude(\n cwd: string,\n baseArgs: string[],\n prompt: string,\n sessionId: string | undefined,\n): Promise<ClaudeResult> {\n return new Promise((resolve, reject) => {\n const args = buildClaudeArgs(baseArgs, prompt, sessionId);\n const proc = spawn('claude', args, {\n cwd,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n let stdout = '';\n let stderr = '';\n\n proc.stdout.on('data', (chunk: Buffer) => {\n stdout += chunk.toString();\n });\n\n proc.stderr.on('data', (chunk: Buffer) => {\n stderr += chunk.toString();\n });\n\n proc.on('close', (code) => {\n if (code !== 0) {\n reject(new Error(`claude exited with code ${code}: ${stderr}`));\n return;\n }\n try {\n const result = parseClaudeJsonOutput(stdout.trim());\n resolve(result);\n } catch (err) {\n reject(new Error(`Failed to parse claude output: ${stdout.slice(0, 200)}`));\n }\n });\n\n proc.on('error', (err) => {\n reject(new Error(`Failed to spawn claude: ${err.message}`));\n });\n });\n}\n","import { runClaude, type ClaudeResult } from './claude-cli.js';\nimport type { SessionStore, PersistedSession } from './session-store.js';\n\nexport interface SessionInfo {\n sessionId: string;\n projectKey: string;\n lastActivity: number;\n queueLength: number;\n}\n\nexport interface SessionManager {\n send(projectKey: string, cwd: string, prompt: string): Promise<ClaudeResult>;\n getSession(projectKey: string): SessionInfo | undefined;\n listSessions(): SessionInfo[];\n clearSession(projectKey: string): boolean;\n shutdown(): void;\n}\n\ninterface InternalSession {\n sessionId: string | undefined;\n projectKey: string;\n cwd: string;\n lastActivity: number;\n processing: boolean;\n queue: Array<{\n prompt: string;\n resolve: (result: ClaudeResult) => void;\n reject: (error: Error) => void;\n }>;\n idleTimer: ReturnType<typeof setTimeout> | null;\n}\n\nexport function createSessionManager(defaults: {\n idleTimeoutMs: number;\n maxConcurrentSessions: number;\n claudeArgs: string[];\n}, store?: SessionStore): SessionManager {\n const sessions = new Map<string, InternalSession>();\n\n let activeProcesses = 0;\n const waiters: Array<() => void> = [];\n\n function persistSessions(): void {\n if (!store) return;\n // Merge in-memory sessions with existing persisted data.\n // In-memory sessions take precedence; persisted-only entries are preserved.\n const persisted = store.load();\n for (const [key, s] of sessions) {\n if (s.sessionId) {\n persisted.set(key, {\n sessionId: s.sessionId,\n projectKey: s.projectKey,\n cwd: s.cwd,\n lastActivity: s.lastActivity,\n });\n }\n }\n store.save(persisted);\n }\n\n async function acquireSlot(): Promise<void> {\n if (activeProcesses < defaults.maxConcurrentSessions) {\n activeProcesses++;\n return;\n }\n return new Promise<void>((resolve) => {\n waiters.push(() => {\n activeProcesses++;\n resolve();\n });\n });\n }\n\n function releaseSlot(): void {\n activeProcesses--;\n const next = waiters.shift();\n if (next) next();\n }\n\n function resetIdleTimer(session: InternalSession) {\n if (session.idleTimer) clearTimeout(session.idleTimer);\n session.idleTimer = setTimeout(() => {\n // Remove from memory only; session ID stays on disk for later resume\n sessions.delete(session.projectKey);\n }, defaults.idleTimeoutMs);\n }\n\n async function processQueue(session: InternalSession): Promise<void> {\n if (session.processing || session.queue.length === 0) return;\n session.processing = true;\n\n while (session.queue.length > 0) {\n const item = session.queue.shift()!;\n await acquireSlot();\n try {\n const result = await runClaude(\n session.cwd,\n defaults.claudeArgs,\n item.prompt,\n session.sessionId,\n );\n session.sessionId = result.sessionId || session.sessionId;\n session.lastActivity = Date.now();\n resetIdleTimer(session);\n persistSessions();\n item.resolve(result);\n } catch (err) {\n if (session.sessionId) {\n session.sessionId = undefined;\n try {\n const result = await runClaude(session.cwd, defaults.claudeArgs, item.prompt, undefined);\n session.sessionId = result.sessionId || undefined;\n session.lastActivity = Date.now();\n resetIdleTimer(session);\n persistSessions();\n item.resolve(result);\n } catch (retryErr) {\n item.reject(retryErr instanceof Error ? retryErr : new Error(String(retryErr)));\n }\n } else {\n item.reject(err instanceof Error ? err : new Error(String(err)));\n }\n } finally {\n releaseSlot();\n }\n }\n\n session.processing = false;\n }\n\n function getOrCreateSession(projectKey: string, cwd: string): InternalSession {\n let session = sessions.get(projectKey);\n if (!session) {\n // Check store for a previously persisted session ID\n let restoredSessionId: string | undefined;\n if (store) {\n const persisted = store.load();\n const entry = persisted.get(projectKey);\n if (entry?.sessionId) {\n restoredSessionId = entry.sessionId;\n }\n }\n\n session = {\n sessionId: restoredSessionId,\n projectKey,\n cwd,\n lastActivity: Date.now(),\n processing: false,\n queue: [],\n idleTimer: null,\n };\n sessions.set(projectKey, session);\n resetIdleTimer(session);\n }\n return session;\n }\n\n // Restore persisted sessions into memory at startup\n if (store) {\n const persisted = store.load();\n for (const [key, entry] of persisted) {\n sessions.set(key, {\n sessionId: entry.sessionId,\n projectKey: entry.projectKey,\n cwd: entry.cwd,\n lastActivity: entry.lastActivity,\n processing: false,\n queue: [],\n idleTimer: null,\n });\n }\n for (const session of sessions.values()) {\n resetIdleTimer(session);\n }\n if (persisted.size > 0) {\n console.log(`Restored ${persisted.size} session(s) from disk`);\n }\n }\n\n return {\n send(projectKey: string, cwd: string, prompt: string): Promise<ClaudeResult> {\n const session = getOrCreateSession(projectKey, cwd);\n return new Promise<ClaudeResult>((resolve, reject) => {\n session.queue.push({ prompt, resolve, reject });\n processQueue(session);\n });\n },\n\n getSession(projectKey: string): SessionInfo | undefined {\n const session = sessions.get(projectKey);\n if (!session) return undefined;\n return {\n sessionId: session.sessionId ?? '',\n projectKey: session.projectKey,\n lastActivity: session.lastActivity,\n queueLength: session.queue.length,\n };\n },\n\n listSessions(): SessionInfo[] {\n return Array.from(sessions.values()).map((s) => ({\n sessionId: s.sessionId ?? '',\n projectKey: s.projectKey,\n lastActivity: s.lastActivity,\n queueLength: s.queue.length,\n }));\n },\n\n clearSession(projectKey: string): boolean {\n const session = sessions.get(projectKey);\n if (!session) return false;\n if (session.idleTimer) clearTimeout(session.idleTimer);\n sessions.delete(projectKey);\n persistSessions();\n return true;\n },\n\n shutdown() {\n persistSessions();\n for (const session of sessions.values()) {\n if (session.idleTimer) clearTimeout(session.idleTimer);\n }\n sessions.clear();\n },\n };\n}\n","import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';\nimport { dirname } from 'node:path';\n\nexport interface PersistedSession {\n sessionId: string;\n projectKey: string;\n cwd: string;\n lastActivity: number;\n}\n\nexport interface SessionStore {\n load(): Map<string, PersistedSession>;\n save(sessions: Map<string, PersistedSession>): void;\n}\n\nexport function createFileSessionStore(filePath: string): SessionStore {\n return {\n load(): Map<string, PersistedSession> {\n try {\n const raw = readFileSync(filePath, 'utf-8');\n const entries: PersistedSession[] = JSON.parse(raw);\n const map = new Map<string, PersistedSession>();\n for (const entry of entries) {\n if (entry.sessionId && entry.projectKey) {\n map.set(entry.projectKey, entry);\n }\n }\n return map;\n } catch {\n return new Map();\n }\n },\n\n save(sessions: Map<string, PersistedSession>): void {\n const entries = Array.from(sessions.values());\n try {\n mkdirSync(dirname(filePath), { recursive: true });\n writeFileSync(filePath, JSON.stringify(entries, null, 2) + '\\n');\n } catch (err) {\n console.error('Failed to persist sessions:', err);\n }\n },\n };\n}\n","import { Client, GatewayIntentBits, Events, type Message } from 'discord.js';\nimport type { Router } from './router.js';\nimport type { SessionManager } from './session-manager.js';\nimport type { GatewayConfig } from './config.js';\n\nexport function chunkMessage(text: string, limit: number): string[] {\n if (text.length <= limit) return [text];\n\n const chunks: string[] = [];\n const lines = text.split('\\n');\n let current = '';\n\n for (const line of lines) {\n if (line.length > limit) {\n if (current) {\n chunks.push(current);\n current = '';\n }\n for (let i = 0; i < line.length; i += limit) {\n chunks.push(line.slice(i, i + limit));\n }\n continue;\n }\n\n const candidate = current ? `${current}\\n${line}` : line;\n if (candidate.length > limit) {\n chunks.push(current);\n current = line;\n } else {\n current = candidate;\n }\n }\n\n if (current || chunks.length === 0) {\n chunks.push(current);\n }\n\n return chunks;\n}\n\nexport interface DiscordBot {\n start(token: string): Promise<void>;\n stop(): void;\n}\n\nfunction resolveProjectName(config: GatewayConfig, channelId: string): string {\n return config.projects[channelId]?.name ?? channelId;\n}\n\nfunction findProjectByName(config: GatewayConfig, name: string): { channelId: string; name: string } | null {\n const lower = name.toLowerCase();\n for (const [channelId, project] of Object.entries(config.projects)) {\n if (project.name.toLowerCase() === lower) {\n return { channelId, name: project.name };\n }\n }\n return null;\n}\n\nfunction formatTimeSince(timestamp: number): string {\n const seconds = Math.floor((Date.now() - timestamp) / 1000);\n if (seconds < 60) return `${seconds}s ago`;\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes}m ago`;\n const hours = Math.floor(minutes / 60);\n return `${hours}h ${minutes % 60}m ago`;\n}\n\nexport function handleCommand(\n command: string,\n config: GatewayConfig,\n sessionManager: SessionManager,\n): string | null {\n const parts = command.trim().split(/\\s+/);\n const cmd = parts[0]?.toLowerCase();\n\n if (cmd === '!sessions') {\n const allSessions = sessionManager.listSessions();\n if (allSessions.length === 0) {\n return 'No active sessions.';\n }\n const lines = allSessions.map((s) => {\n const name = resolveProjectName(config, s.projectKey);\n const idle = formatTimeSince(s.lastActivity);\n const queue = s.queueLength > 0 ? ` | queue: ${s.queueLength}` : '';\n const sid = s.sessionId ? ` | \\`${s.sessionId.slice(0, 8)}…\\`` : '';\n return `- **${name}** — last active ${idle}${queue}${sid}`;\n });\n return `**Active sessions (${allSessions.length})**\\n${lines.join('\\n')}`;\n }\n\n if (cmd === '!session') {\n const name = parts.slice(1).join(' ');\n if (!name) return 'Usage: `!session <project name>`';\n const project = findProjectByName(config, name);\n if (!project) return `No project found matching \"${name}\".`;\n const info = sessionManager.getSession(project.channelId);\n if (!info) return `**${project.name}** — no active session.`;\n const idle = formatTimeSince(info.lastActivity);\n const sid = info.sessionId || 'none';\n return [\n `**${project.name}**`,\n `Session ID: \\`${sid}\\``,\n `Last active: ${idle}`,\n `Queue depth: ${info.queueLength}`,\n ].join('\\n');\n }\n\n if (cmd === '!kill') {\n const name = parts.slice(1).join(' ');\n if (!name) return 'Usage: `!kill <project name>`';\n const project = findProjectByName(config, name);\n if (!project) return `No project found matching \"${name}\".`;\n const cleared = sessionManager.clearSession(project.channelId);\n if (cleared) return `Session for **${project.name}** cleared.`;\n return `**${project.name}** — no active session to clear.`;\n }\n\n if (cmd === '!help') {\n return [\n '**Gateway commands**',\n '`!sessions` — list all active sessions',\n '`!session <name>` — inspect a specific project session',\n '`!kill <name>` — force-close a project session',\n '`!help` — show this message',\n ].join('\\n');\n }\n\n return null;\n}\n\nexport function createDiscordBot(router: Router, sessionManager: SessionManager, config: GatewayConfig): DiscordBot {\n const client = new Client({\n intents: [\n GatewayIntentBits.Guilds,\n GatewayIntentBits.GuildMessages,\n GatewayIntentBits.MessageContent,\n ],\n });\n\n client.on(Events.MessageCreate, async (message: Message) => {\n if (message.author.bot) return;\n\n if (!('send' in message.channel)) return;\n\n // Handle gateway commands from any mapped channel\n if (message.content.startsWith('!')) {\n const parentId = message.channel.isThread() ? message.channel.parentId ?? undefined : undefined;\n const resolved = router.resolve(message.channelId, parentId);\n if (resolved) {\n const response = handleCommand(message.content, config, sessionManager);\n if (response) {\n await message.channel.send(response);\n return;\n }\n }\n }\n\n const parentId = message.channel.isThread() ? message.channel.parentId ?? undefined : undefined;\n const resolved = router.resolve(message.channelId, parentId);\n if (!resolved) return;\n\n try {\n await message.react('👀');\n } catch {\n // Reaction may fail if permissions are missing — non-critical\n }\n\n try {\n const result = await sessionManager.send(\n resolved.channelId,\n resolved.directory,\n message.content,\n );\n\n const chunks = chunkMessage(result.text, 2000);\n for (const chunk of chunks) {\n await message.channel.send(chunk);\n }\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n await message.channel.send(\n `**Error** (${resolved.name}): ${errorMsg.slice(0, 1800)}`,\n );\n }\n });\n\n return {\n async start(token: string) {\n await client.login(token);\n console.log(`Gateway connected as ${client.user?.tag}`);\n },\n stop() {\n client.destroy();\n },\n };\n}\n","import { createInterface } from 'node:readline';\nimport { writeFileSync, existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { execSync } from 'node:child_process';\n\nfunction createPrompt(): (question: string) => Promise<string> {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n return (question: string) =>\n new Promise((resolve) => rl.question(question, (answer) => resolve(answer.trim())));\n}\n\nexport async function runInit() {\n const ask = createPrompt();\n const configDir = process.cwd();\n\n console.log('\\nmpg init — set up multi-project gateway\\n');\n\n // Check for claude CLI\n try {\n execSync('claude --version', { stdio: 'pipe' });\n console.log('Claude CLI found.');\n } catch {\n console.warn('Warning: `claude` not found on PATH. Make sure it is installed before starting the gateway.');\n }\n\n // Discord bot token\n let token = process.env.DISCORD_BOT_TOKEN ?? '';\n const inputToken = await ask(`Discord bot token${token ? ' (press Enter to keep existing)' : ''}: `);\n if (inputToken) token = inputToken;\n if (!token) {\n console.error('A Discord bot token is required. Create one at https://discord.com/developers/applications');\n process.exit(1);\n }\n\n // Write .env\n const envPath = resolve(configDir, '.env');\n writeFileSync(envPath, `DISCORD_BOT_TOKEN=${token}\\n`);\n console.log(`Wrote ${envPath}`);\n\n // Collect projects\n interface ProjectEntry {\n name: string;\n directory: string;\n channelId: string;\n }\n\n const projects: ProjectEntry[] = [];\n\n // Load existing config if present\n const configPath = resolve(configDir, 'config.json');\n if (existsSync(configPath)) {\n try {\n const existing = JSON.parse(\n (await import('node:fs')).readFileSync(configPath, 'utf-8'),\n );\n if (existing.projects) {\n for (const [channelId, project] of Object.entries(existing.projects)) {\n const p = project as { name?: string; directory: string };\n projects.push({ name: p.name ?? channelId, directory: p.directory, channelId });\n }\n if (projects.length > 0) {\n console.log(`\\nExisting projects (${projects.length}):`);\n for (const p of projects) {\n console.log(` ${p.name} → ${p.directory} (${p.channelId})`);\n }\n }\n }\n } catch {\n // ignore parse errors\n }\n }\n\n console.log('\\nAdd projects (empty name to finish):\\n');\n\n while (true) {\n const name = await ask('Project name: ');\n if (!name) break;\n\n const directory = await ask('Project directory (absolute path): ');\n if (!directory) {\n console.log('Directory is required, skipping.');\n continue;\n }\n if (!existsSync(directory)) {\n console.warn(`Warning: ${directory} does not exist.`);\n }\n\n const channelId = await ask('Discord channel ID: ');\n if (!channelId) {\n console.log('Channel ID is required, skipping.');\n continue;\n }\n\n projects.push({ name, directory, channelId });\n console.log(`Added ${name}\\n`);\n }\n\n if (projects.length === 0) {\n console.log('No projects configured. You can edit config.json later.');\n }\n\n // Build config\n const config = {\n defaults: {\n idleTimeoutMs: 1800000,\n maxConcurrentSessions: 4,\n claudeArgs: ['--permission-mode', 'acceptEdits', '--output-format', 'json'],\n },\n projects: Object.fromEntries(\n projects.map((p) => [p.channelId, { name: p.name, directory: p.directory }]),\n ),\n };\n\n writeFileSync(configPath, JSON.stringify(config, null, 2) + '\\n');\n console.log(`Wrote ${configPath}`);\n\n console.log('\\nSetup complete! Run `mpg start` to launch the gateway.');\n}\n"],"mappings":";;;AAAA,SAAS,WAAAA,gBAAe;AACxB,SAAS,cAAAC,aAAY,gBAAAC,qBAAoB;AACzC,SAAS,UAAU,eAAe;;;ACgB3B,SAAS,WAAW,KAA6B;AACtD,MAAI,CAAC,OAAO,OAAO,QAAQ,UAAU;AACnC,UAAM,IAAI,MAAM,0BAA0B;AAAA,EAC5C;AAEA,QAAM,MAAM;AAEZ,MAAI,CAAC,IAAI,YAAY,OAAO,IAAI,aAAa,UAAU;AACrD,UAAM,IAAI,MAAM,sCAAsC;AAAA,EACxD;AAEA,QAAM,WAAW,IAAI;AACrB,QAAM,YAA2C,CAAC;AAElD,aAAW,CAAC,WAAW,OAAO,KAAK,OAAO,QAAQ,QAAQ,GAAG;AAC3D,QAAI,CAAC,WAAW,OAAO,YAAY,UAAU;AAC3C,YAAM,IAAI,MAAM,uBAAuB,SAAS,oBAAoB;AAAA,IACtE;AACA,UAAM,IAAI;AACV,QAAI,OAAO,EAAE,cAAc,YAAY,CAAC,EAAE,WAAW;AACnD,YAAM,IAAI,MAAM,uBAAuB,SAAS,iCAAiC;AAAA,IACnF;AACA,cAAU,SAAS,IAAI;AAAA,MACrB,MAAM,OAAO,EAAE,SAAS,WAAW,EAAE,OAAO;AAAA,MAC5C,WAAW,EAAE;AAAA,MACb,GAAI,EAAE,kBAAkB,UAAa,EAAE,eAAe,OAAO,EAAE,aAAa,EAAE;AAAA,MAC9E,GAAI,MAAM,QAAQ,EAAE,UAAU,KAAK,EAAE,YAAY,EAAE,WAAuB;AAAA,IAC5E;AAAA,EACF;AAEA,QAAM,WAAY,IAAI,YAAY,CAAC;AAEnC,SAAO;AAAA,IACL,UAAU;AAAA,MACR,eAAe,OAAO,SAAS,kBAAkB,WAAW,SAAS,gBAAgB;AAAA,MACrF,uBAAuB,OAAO,SAAS,0BAA0B,WAAW,SAAS,wBAAwB;AAAA,MAC7G,YAAY,MAAM,QAAQ,SAAS,UAAU,IAAK,SAAS,aAA0B,CAAC,qBAAqB,eAAe,mBAAmB,MAAM;AAAA,IACrJ;AAAA,IACA,UAAU;AAAA,EACZ;AACF;;;AC9CO,SAAS,aAAa,QAA+B;AAC1D,SAAO;AAAA,IACL,QAAQ,WAAmB,iBAAkD;AAC3E,YAAM,UAAU,OAAO,SAAS,SAAS;AACzC,UAAI,SAAS;AACX,eAAO,EAAE,WAAW,MAAM,QAAQ,MAAM,WAAW,QAAQ,UAAU;AAAA,MACvE;AAEA,UAAI,iBAAiB;AACnB,cAAM,gBAAgB,OAAO,SAAS,eAAe;AACrD,YAAI,eAAe;AACjB,iBAAO,EAAE,WAAW,iBAAiB,MAAM,cAAc,MAAM,WAAW,cAAc,UAAU;AAAA,QACpG;AAAA,MACF;AAEA,aAAO;AAAA,IACT;AAAA,EACF;AACF;;;AC9BA,SAAS,aAAa;AAQf,SAAS,sBAAsB,KAA2B;AAC/D,QAAM,OAAO,KAAK,MAAM,GAAG;AAC3B,SAAO;AAAA,IACL,MAAM,KAAK,UAAU;AAAA,IACrB,WAAW,KAAK,cAAc;AAAA,IAC9B,SAAS,QAAQ,KAAK,QAAQ;AAAA,EAChC;AACF;AAEO,SAAS,gBACd,UACA,QACA,WACU;AACV,QAAMC,QAAO,CAAC,WAAW,GAAG,QAAQ;AACpC,MAAI,WAAW;AACb,IAAAA,MAAK,KAAK,YAAY,SAAS;AAAA,EACjC;AACA,EAAAA,MAAK,KAAK,MAAM;AAChB,SAAOA;AACT;AAEO,SAAS,UACd,KACA,UACA,QACA,WACuB;AACvB,SAAO,IAAI,QAAQ,CAACC,UAAS,WAAW;AACtC,UAAMD,QAAO,gBAAgB,UAAU,QAAQ,SAAS;AACxD,UAAM,OAAO,MAAM,UAAUA,OAAM;AAAA,MACjC;AAAA,MACA,OAAO,CAAC,UAAU,QAAQ,MAAM;AAAA,IAClC,CAAC;AAED,QAAI,SAAS;AACb,QAAI,SAAS;AAEb,SAAK,OAAO,GAAG,QAAQ,CAAC,UAAkB;AACxC,gBAAU,MAAM,SAAS;AAAA,IAC3B,CAAC;AAED,SAAK,OAAO,GAAG,QAAQ,CAAC,UAAkB;AACxC,gBAAU,MAAM,SAAS;AAAA,IAC3B,CAAC;AAED,SAAK,GAAG,SAAS,CAAC,SAAS;AACzB,UAAI,SAAS,GAAG;AACd,eAAO,IAAI,MAAM,2BAA2B,IAAI,KAAK,MAAM,EAAE,CAAC;AAC9D;AAAA,MACF;AACA,UAAI;AACF,cAAM,SAAS,sBAAsB,OAAO,KAAK,CAAC;AAClD,QAAAC,SAAQ,MAAM;AAAA,MAChB,SAAS,KAAK;AACZ,eAAO,IAAI,MAAM,kCAAkC,OAAO,MAAM,GAAG,GAAG,CAAC,EAAE,CAAC;AAAA,MAC5E;AAAA,IACF,CAAC;AAED,SAAK,GAAG,SAAS,CAAC,QAAQ;AACxB,aAAO,IAAI,MAAM,2BAA2B,IAAI,OAAO,EAAE,CAAC;AAAA,IAC5D,CAAC;AAAA,EACH,CAAC;AACH;;;ACvCO,SAAS,qBAAqB,UAIlC,OAAsC;AACvC,QAAM,WAAW,oBAAI,IAA6B;AAElD,MAAI,kBAAkB;AACtB,QAAM,UAA6B,CAAC;AAEpC,WAAS,kBAAwB;AAC/B,QAAI,CAAC,MAAO;AAGZ,UAAM,YAAY,MAAM,KAAK;AAC7B,eAAW,CAAC,KAAK,CAAC,KAAK,UAAU;AAC/B,UAAI,EAAE,WAAW;AACf,kBAAU,IAAI,KAAK;AAAA,UACjB,WAAW,EAAE;AAAA,UACb,YAAY,EAAE;AAAA,UACd,KAAK,EAAE;AAAA,UACP,cAAc,EAAE;AAAA,QAClB,CAAC;AAAA,MACH;AAAA,IACF;AACA,UAAM,KAAK,SAAS;AAAA,EACtB;AAEA,iBAAe,cAA6B;AAC1C,QAAI,kBAAkB,SAAS,uBAAuB;AACpD;AACA;AAAA,IACF;AACA,WAAO,IAAI,QAAc,CAACC,aAAY;AACpC,cAAQ,KAAK,MAAM;AACjB;AACA,QAAAA,SAAQ;AAAA,MACV,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAEA,WAAS,cAAoB;AAC3B;AACA,UAAM,OAAO,QAAQ,MAAM;AAC3B,QAAI,KAAM,MAAK;AAAA,EACjB;AAEA,WAAS,eAAe,SAA0B;AAChD,QAAI,QAAQ,UAAW,cAAa,QAAQ,SAAS;AACrD,YAAQ,YAAY,WAAW,MAAM;AAEnC,eAAS,OAAO,QAAQ,UAAU;AAAA,IACpC,GAAG,SAAS,aAAa;AAAA,EAC3B;AAEA,iBAAe,aAAa,SAAyC;AACnE,QAAI,QAAQ,cAAc,QAAQ,MAAM,WAAW,EAAG;AACtD,YAAQ,aAAa;AAErB,WAAO,QAAQ,MAAM,SAAS,GAAG;AAC/B,YAAM,OAAO,QAAQ,MAAM,MAAM;AACjC,YAAM,YAAY;AAClB,UAAI;AACF,cAAM,SAAS,MAAM;AAAA,UACnB,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,KAAK;AAAA,UACL,QAAQ;AAAA,QACV;AACA,gBAAQ,YAAY,OAAO,aAAa,QAAQ;AAChD,gBAAQ,eAAe,KAAK,IAAI;AAChC,uBAAe,OAAO;AACtB,wBAAgB;AAChB,aAAK,QAAQ,MAAM;AAAA,MACrB,SAAS,KAAK;AACZ,YAAI,QAAQ,WAAW;AACrB,kBAAQ,YAAY;AACpB,cAAI;AACF,kBAAM,SAAS,MAAM,UAAU,QAAQ,KAAK,SAAS,YAAY,KAAK,QAAQ,MAAS;AACvF,oBAAQ,YAAY,OAAO,aAAa;AACxC,oBAAQ,eAAe,KAAK,IAAI;AAChC,2BAAe,OAAO;AACtB,4BAAgB;AAChB,iBAAK,QAAQ,MAAM;AAAA,UACrB,SAAS,UAAU;AACjB,iBAAK,OAAO,oBAAoB,QAAQ,WAAW,IAAI,MAAM,OAAO,QAAQ,CAAC,CAAC;AAAA,UAChF;AAAA,QACF,OAAO;AACL,eAAK,OAAO,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,QACjE;AAAA,MACF,UAAE;AACA,oBAAY;AAAA,MACd;AAAA,IACF;AAEA,YAAQ,aAAa;AAAA,EACvB;AAEA,WAAS,mBAAmB,YAAoB,KAA8B;AAC5E,QAAI,UAAU,SAAS,IAAI,UAAU;AACrC,QAAI,CAAC,SAAS;AAEZ,UAAI;AACJ,UAAI,OAAO;AACT,cAAM,YAAY,MAAM,KAAK;AAC7B,cAAM,QAAQ,UAAU,IAAI,UAAU;AACtC,YAAI,OAAO,WAAW;AACpB,8BAAoB,MAAM;AAAA,QAC5B;AAAA,MACF;AAEA,gBAAU;AAAA,QACR,WAAW;AAAA,QACX;AAAA,QACA;AAAA,QACA,cAAc,KAAK,IAAI;AAAA,QACvB,YAAY;AAAA,QACZ,OAAO,CAAC;AAAA,QACR,WAAW;AAAA,MACb;AACA,eAAS,IAAI,YAAY,OAAO;AAChC,qBAAe,OAAO;AAAA,IACxB;AACA,WAAO;AAAA,EACT;AAGA,MAAI,OAAO;AACT,UAAM,YAAY,MAAM,KAAK;AAC7B,eAAW,CAAC,KAAK,KAAK,KAAK,WAAW;AACpC,eAAS,IAAI,KAAK;AAAA,QAChB,WAAW,MAAM;AAAA,QACjB,YAAY,MAAM;AAAA,QAClB,KAAK,MAAM;AAAA,QACX,cAAc,MAAM;AAAA,QACpB,YAAY;AAAA,QACZ,OAAO,CAAC;AAAA,QACR,WAAW;AAAA,MACb,CAAC;AAAA,IACH;AACA,eAAW,WAAW,SAAS,OAAO,GAAG;AACvC,qBAAe,OAAO;AAAA,IACxB;AACA,QAAI,UAAU,OAAO,GAAG;AACtB,cAAQ,IAAI,YAAY,UAAU,IAAI,uBAAuB;AAAA,IAC/D;AAAA,EACF;AAEA,SAAO;AAAA,IACL,KAAK,YAAoB,KAAa,QAAuC;AAC3E,YAAM,UAAU,mBAAmB,YAAY,GAAG;AAClD,aAAO,IAAI,QAAsB,CAACA,UAAS,WAAW;AACpD,gBAAQ,MAAM,KAAK,EAAE,QAAQ,SAAAA,UAAS,OAAO,CAAC;AAC9C,qBAAa,OAAO;AAAA,MACtB,CAAC;AAAA,IACH;AAAA,IAEA,WAAW,YAA6C;AACtD,YAAM,UAAU,SAAS,IAAI,UAAU;AACvC,UAAI,CAAC,QAAS,QAAO;AACrB,aAAO;AAAA,QACL,WAAW,QAAQ,aAAa;AAAA,QAChC,YAAY,QAAQ;AAAA,QACpB,cAAc,QAAQ;AAAA,QACtB,aAAa,QAAQ,MAAM;AAAA,MAC7B;AAAA,IACF;AAAA,IAEA,eAA8B;AAC5B,aAAO,MAAM,KAAK,SAAS,OAAO,CAAC,EAAE,IAAI,CAAC,OAAO;AAAA,QAC/C,WAAW,EAAE,aAAa;AAAA,QAC1B,YAAY,EAAE;AAAA,QACd,cAAc,EAAE;AAAA,QAChB,aAAa,EAAE,MAAM;AAAA,MACvB,EAAE;AAAA,IACJ;AAAA,IAEA,aAAa,YAA6B;AACxC,YAAM,UAAU,SAAS,IAAI,UAAU;AACvC,UAAI,CAAC,QAAS,QAAO;AACrB,UAAI,QAAQ,UAAW,cAAa,QAAQ,SAAS;AACrD,eAAS,OAAO,UAAU;AAC1B,sBAAgB;AAChB,aAAO;AAAA,IACT;AAAA,IAEA,WAAW;AACT,sBAAgB;AAChB,iBAAW,WAAW,SAAS,OAAO,GAAG;AACvC,YAAI,QAAQ,UAAW,cAAa,QAAQ,SAAS;AAAA,MACvD;AACA,eAAS,MAAM;AAAA,IACjB;AAAA,EACF;AACF;;;AClOA,SAAS,cAAc,eAAe,iBAAiB;AACvD,SAAS,eAAe;AAcjB,SAAS,uBAAuB,UAAgC;AACrE,SAAO;AAAA,IACL,OAAsC;AACpC,UAAI;AACF,cAAM,MAAM,aAAa,UAAU,OAAO;AAC1C,cAAM,UAA8B,KAAK,MAAM,GAAG;AAClD,cAAM,MAAM,oBAAI,IAA8B;AAC9C,mBAAW,SAAS,SAAS;AAC3B,cAAI,MAAM,aAAa,MAAM,YAAY;AACvC,gBAAI,IAAI,MAAM,YAAY,KAAK;AAAA,UACjC;AAAA,QACF;AACA,eAAO;AAAA,MACT,QAAQ;AACN,eAAO,oBAAI,IAAI;AAAA,MACjB;AAAA,IACF;AAAA,IAEA,KAAK,UAA+C;AAClD,YAAM,UAAU,MAAM,KAAK,SAAS,OAAO,CAAC;AAC5C,UAAI;AACF,kBAAU,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AAChD,sBAAc,UAAU,KAAK,UAAU,SAAS,MAAM,CAAC,IAAI,IAAI;AAAA,MACjE,SAAS,KAAK;AACZ,gBAAQ,MAAM,+BAA+B,GAAG;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AACF;;;AC3CA,SAAS,QAAQ,mBAAmB,cAA4B;AAKzD,SAAS,aAAa,MAAc,OAAyB;AAClE,MAAI,KAAK,UAAU,MAAO,QAAO,CAAC,IAAI;AAEtC,QAAM,SAAmB,CAAC;AAC1B,QAAM,QAAQ,KAAK,MAAM,IAAI;AAC7B,MAAI,UAAU;AAEd,aAAW,QAAQ,OAAO;AACxB,QAAI,KAAK,SAAS,OAAO;AACvB,UAAI,SAAS;AACX,eAAO,KAAK,OAAO;AACnB,kBAAU;AAAA,MACZ;AACA,eAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,OAAO;AAC3C,eAAO,KAAK,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC;AAAA,MACtC;AACA;AAAA,IACF;AAEA,UAAM,YAAY,UAAU,GAAG,OAAO;AAAA,EAAK,IAAI,KAAK;AACpD,QAAI,UAAU,SAAS,OAAO;AAC5B,aAAO,KAAK,OAAO;AACnB,gBAAU;AAAA,IACZ,OAAO;AACL,gBAAU;AAAA,IACZ;AAAA,EACF;AAEA,MAAI,WAAW,OAAO,WAAW,GAAG;AAClC,WAAO,KAAK,OAAO;AAAA,EACrB;AAEA,SAAO;AACT;AAOA,SAAS,mBAAmB,QAAuB,WAA2B;AAC5E,SAAO,OAAO,SAAS,SAAS,GAAG,QAAQ;AAC7C;AAEA,SAAS,kBAAkB,QAAuB,MAA0D;AAC1G,QAAM,QAAQ,KAAK,YAAY;AAC/B,aAAW,CAAC,WAAW,OAAO,KAAK,OAAO,QAAQ,OAAO,QAAQ,GAAG;AAClE,QAAI,QAAQ,KAAK,YAAY,MAAM,OAAO;AACxC,aAAO,EAAE,WAAW,MAAM,QAAQ,KAAK;AAAA,IACzC;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,gBAAgB,WAA2B;AAClD,QAAM,UAAU,KAAK,OAAO,KAAK,IAAI,IAAI,aAAa,GAAI;AAC1D,MAAI,UAAU,GAAI,QAAO,GAAG,OAAO;AACnC,QAAM,UAAU,KAAK,MAAM,UAAU,EAAE;AACvC,MAAI,UAAU,GAAI,QAAO,GAAG,OAAO;AACnC,QAAM,QAAQ,KAAK,MAAM,UAAU,EAAE;AACrC,SAAO,GAAG,KAAK,KAAK,UAAU,EAAE;AAClC;AAEO,SAAS,cACdC,UACA,QACA,gBACe;AACf,QAAM,QAAQA,SAAQ,KAAK,EAAE,MAAM,KAAK;AACxC,QAAM,MAAM,MAAM,CAAC,GAAG,YAAY;AAElC,MAAI,QAAQ,aAAa;AACvB,UAAM,cAAc,eAAe,aAAa;AAChD,QAAI,YAAY,WAAW,GAAG;AAC5B,aAAO;AAAA,IACT;AACA,UAAM,QAAQ,YAAY,IAAI,CAAC,MAAM;AACnC,YAAM,OAAO,mBAAmB,QAAQ,EAAE,UAAU;AACpD,YAAM,OAAO,gBAAgB,EAAE,YAAY;AAC3C,YAAM,QAAQ,EAAE,cAAc,IAAI,aAAa,EAAE,WAAW,KAAK;AACjE,YAAM,MAAM,EAAE,YAAY,QAAQ,EAAE,UAAU,MAAM,GAAG,CAAC,CAAC,aAAQ;AACjE,aAAO,OAAO,IAAI,yBAAoB,IAAI,GAAG,KAAK,GAAG,GAAG;AAAA,IAC1D,CAAC;AACD,WAAO,sBAAsB,YAAY,MAAM;AAAA,EAAQ,MAAM,KAAK,IAAI,CAAC;AAAA,EACzE;AAEA,MAAI,QAAQ,YAAY;AACtB,UAAM,OAAO,MAAM,MAAM,CAAC,EAAE,KAAK,GAAG;AACpC,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,UAAU,kBAAkB,QAAQ,IAAI;AAC9C,QAAI,CAAC,QAAS,QAAO,8BAA8B,IAAI;AACvD,UAAM,OAAO,eAAe,WAAW,QAAQ,SAAS;AACxD,QAAI,CAAC,KAAM,QAAO,KAAK,QAAQ,IAAI;AACnC,UAAM,OAAO,gBAAgB,KAAK,YAAY;AAC9C,UAAM,MAAM,KAAK,aAAa;AAC9B,WAAO;AAAA,MACL,KAAK,QAAQ,IAAI;AAAA,MACjB,iBAAiB,GAAG;AAAA,MACpB,gBAAgB,IAAI;AAAA,MACpB,gBAAgB,KAAK,WAAW;AAAA,IAClC,EAAE,KAAK,IAAI;AAAA,EACb;AAEA,MAAI,QAAQ,SAAS;AACnB,UAAM,OAAO,MAAM,MAAM,CAAC,EAAE,KAAK,GAAG;AACpC,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,UAAU,kBAAkB,QAAQ,IAAI;AAC9C,QAAI,CAAC,QAAS,QAAO,8BAA8B,IAAI;AACvD,UAAM,UAAU,eAAe,aAAa,QAAQ,SAAS;AAC7D,QAAI,QAAS,QAAO,iBAAiB,QAAQ,IAAI;AACjD,WAAO,KAAK,QAAQ,IAAI;AAAA,EAC1B;AAEA,MAAI,QAAQ,SAAS;AACnB,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,EAAE,KAAK,IAAI;AAAA,EACb;AAEA,SAAO;AACT;AAEO,SAAS,iBAAiB,QAAgB,gBAAgC,QAAmC;AAClH,QAAM,SAAS,IAAI,OAAO;AAAA,IACxB,SAAS;AAAA,MACP,kBAAkB;AAAA,MAClB,kBAAkB;AAAA,MAClB,kBAAkB;AAAA,IACpB;AAAA,EACF,CAAC;AAED,SAAO,GAAG,OAAO,eAAe,OAAO,YAAqB;AAC1D,QAAI,QAAQ,OAAO,IAAK;AAExB,QAAI,EAAE,UAAU,QAAQ,SAAU;AAGlC,QAAI,QAAQ,QAAQ,WAAW,GAAG,GAAG;AACnC,YAAMC,YAAW,QAAQ,QAAQ,SAAS,IAAI,QAAQ,QAAQ,YAAY,SAAY;AACtF,YAAMC,YAAW,OAAO,QAAQ,QAAQ,WAAWD,SAAQ;AAC3D,UAAIC,WAAU;AACZ,cAAM,WAAW,cAAc,QAAQ,SAAS,QAAQ,cAAc;AACtE,YAAI,UAAU;AACZ,gBAAM,QAAQ,QAAQ,KAAK,QAAQ;AACnC;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,UAAM,WAAW,QAAQ,QAAQ,SAAS,IAAI,QAAQ,QAAQ,YAAY,SAAY;AACtF,UAAM,WAAW,OAAO,QAAQ,QAAQ,WAAW,QAAQ;AAC3D,QAAI,CAAC,SAAU;AAEf,QAAI;AACF,YAAM,QAAQ,MAAM,WAAI;AAAA,IAC1B,QAAQ;AAAA,IAER;AAEA,QAAI;AACF,YAAM,SAAS,MAAM,eAAe;AAAA,QAClC,SAAS;AAAA,QACT,SAAS;AAAA,QACT,QAAQ;AAAA,MACV;AAEA,YAAM,SAAS,aAAa,OAAO,MAAM,GAAI;AAC7C,iBAAW,SAAS,QAAQ;AAC1B,cAAM,QAAQ,QAAQ,KAAK,KAAK;AAAA,MAClC;AAAA,IACF,SAAS,KAAK;AACZ,YAAM,WAAW,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAChE,YAAM,QAAQ,QAAQ;AAAA,QACpB,cAAc,SAAS,IAAI,MAAM,SAAS,MAAM,GAAG,IAAI,CAAC;AAAA,MAC1D;AAAA,IACF;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL,MAAM,MAAM,OAAe;AACzB,YAAM,OAAO,MAAM,KAAK;AACxB,cAAQ,IAAI,wBAAwB,OAAO,MAAM,GAAG,EAAE;AAAA,IACxD;AAAA,IACA,OAAO;AACL,aAAO,QAAQ;AAAA,IACjB;AAAA,EACF;AACF;;;ACpMA,SAAS,uBAAuB;AAChC,SAAS,iBAAAC,gBAAe,kBAAkB;AAC1C,SAAS,eAAe;AACxB,SAAS,gBAAgB;AAEzB,SAAS,eAAsD;AAC7D,QAAM,KAAK,gBAAgB,EAAE,OAAO,QAAQ,OAAO,QAAQ,QAAQ,OAAO,CAAC;AAC3E,SAAO,CAAC,aACN,IAAI,QAAQ,CAACC,aAAY,GAAG,SAAS,UAAU,CAAC,WAAWA,SAAQ,OAAO,KAAK,CAAC,CAAC,CAAC;AACtF;AAEA,eAAsB,UAAU;AAC9B,QAAM,MAAM,aAAa;AACzB,QAAM,YAAY,QAAQ,IAAI;AAE9B,UAAQ,IAAI,kDAA6C;AAGzD,MAAI;AACF,aAAS,oBAAoB,EAAE,OAAO,OAAO,CAAC;AAC9C,YAAQ,IAAI,mBAAmB;AAAA,EACjC,QAAQ;AACN,YAAQ,KAAK,6FAA6F;AAAA,EAC5G;AAGA,MAAI,QAAQ,QAAQ,IAAI,qBAAqB;AAC7C,QAAM,aAAa,MAAM,IAAI,oBAAoB,QAAQ,oCAAoC,EAAE,IAAI;AACnG,MAAI,WAAY,SAAQ;AACxB,MAAI,CAAC,OAAO;AACV,YAAQ,MAAM,4FAA4F;AAC1G,YAAQ,KAAK,CAAC;AAAA,EAChB;AAGA,QAAM,UAAU,QAAQ,WAAW,MAAM;AACzC,EAAAD,eAAc,SAAS,qBAAqB,KAAK;AAAA,CAAI;AACrD,UAAQ,IAAI,SAAS,OAAO,EAAE;AAS9B,QAAM,WAA2B,CAAC;AAGlC,QAAM,aAAa,QAAQ,WAAW,aAAa;AACnD,MAAI,WAAW,UAAU,GAAG;AAC1B,QAAI;AACF,YAAM,WAAW,KAAK;AAAA,SACnB,MAAM,OAAO,IAAS,GAAG,aAAa,YAAY,OAAO;AAAA,MAC5D;AACA,UAAI,SAAS,UAAU;AACrB,mBAAW,CAAC,WAAW,OAAO,KAAK,OAAO,QAAQ,SAAS,QAAQ,GAAG;AACpE,gBAAM,IAAI;AACV,mBAAS,KAAK,EAAE,MAAM,EAAE,QAAQ,WAAW,WAAW,EAAE,WAAW,UAAU,CAAC;AAAA,QAChF;AACA,YAAI,SAAS,SAAS,GAAG;AACvB,kBAAQ,IAAI;AAAA,qBAAwB,SAAS,MAAM,IAAI;AACvD,qBAAW,KAAK,UAAU;AACxB,oBAAQ,IAAI,KAAK,EAAE,IAAI,WAAM,EAAE,SAAS,KAAK,EAAE,SAAS,GAAG;AAAA,UAC7D;AAAA,QACF;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,UAAQ,IAAI,0CAA0C;AAEtD,SAAO,MAAM;AACX,UAAM,OAAO,MAAM,IAAI,gBAAgB;AACvC,QAAI,CAAC,KAAM;AAEX,UAAM,YAAY,MAAM,IAAI,qCAAqC;AACjE,QAAI,CAAC,WAAW;AACd,cAAQ,IAAI,kCAAkC;AAC9C;AAAA,IACF;AACA,QAAI,CAAC,WAAW,SAAS,GAAG;AAC1B,cAAQ,KAAK,YAAY,SAAS,kBAAkB;AAAA,IACtD;AAEA,UAAM,YAAY,MAAM,IAAI,sBAAsB;AAClD,QAAI,CAAC,WAAW;AACd,cAAQ,IAAI,mCAAmC;AAC/C;AAAA,IACF;AAEA,aAAS,KAAK,EAAE,MAAM,WAAW,UAAU,CAAC;AAC5C,YAAQ,IAAI,SAAS,IAAI;AAAA,CAAI;AAAA,EAC/B;AAEA,MAAI,SAAS,WAAW,GAAG;AACzB,YAAQ,IAAI,yDAAyD;AAAA,EACvE;AAGA,QAAM,SAAS;AAAA,IACb,UAAU;AAAA,MACR,eAAe;AAAA,MACf,uBAAuB;AAAA,MACvB,YAAY,CAAC,qBAAqB,eAAe,mBAAmB,MAAM;AAAA,IAC5E;AAAA,IACA,UAAU,OAAO;AAAA,MACf,SAAS,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,WAAW,EAAE,UAAU,CAAC,CAAC;AAAA,IAC7E;AAAA,EACF;AAEA,EAAAA,eAAc,YAAY,KAAK,UAAU,QAAQ,MAAM,CAAC,IAAI,IAAI;AAChE,UAAQ,IAAI,SAAS,UAAU,EAAE;AAEjC,UAAQ,IAAI,0DAA0D;AACxE;;;AP3GA,IAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AACjC,IAAM,UAAU,KAAK,CAAC,KAAK;AAE3B,eAAe,OAAO;AACpB,UAAQ,SAAS;AAAA,IACf,KAAK;AACH,aAAO,MAAM;AAAA,IACf,KAAK;AACH,aAAO,QAAQ;AAAA,IACjB,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO,KAAK;AAAA,IACd,KAAK;AAAA,IACL,KAAK;AACH,aAAO,QAAQ;AAAA,IACjB;AACE,cAAQ,MAAM,oBAAoB,OAAO,EAAE;AAC3C,WAAK;AACL,cAAQ,KAAK,CAAC;AAAA,EAClB;AACF;AAEA,SAAS,OAAO;AACd,UAAQ,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcZ,KAAK,CAAC;AACR;AAEA,SAAS,UAAU;AACjB,QAAM,MAAM,KAAK,MAAME,cAAa,IAAI,IAAI,mBAAmB,YAAY,GAAG,GAAG,OAAO,CAAC;AACzF,UAAQ,IAAI,QAAQ,IAAI,OAAO,EAAE;AACnC;AAEA,SAAS,QAAQ;AACf,QAAM,YAAY,QAAQ,IAAI;AAC9B,QAAM,UAAUC,SAAQ,WAAW,MAAM;AACzC,MAAIC,YAAW,OAAO,GAAG;AACvB,YAAQ,EAAE,MAAM,QAAQ,CAAC;AAAA,EAC3B;AAEA,QAAM,QAAQ,QAAQ,IAAI;AAC1B,MAAI,CAAC,OAAO;AACV,YAAQ,MAAM,gEAAgE;AAC9E,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,aAAaD,SAAQ,WAAW,aAAa;AACnD,MAAI,CAACC,YAAW,UAAU,GAAG;AAC3B,YAAQ,MAAM,sDAAsD;AACpE,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,YAAY,KAAK,MAAMF,cAAa,YAAY,OAAO,CAAC;AAC9D,QAAM,SAAS,WAAW,SAAS;AAEnC,QAAM,eAAe,OAAO,KAAK,OAAO,QAAQ,EAAE;AAClD,MAAI,iBAAiB,GAAG;AACtB,YAAQ,MAAM,uCAAuC;AACrD,YAAQ,KAAK,CAAC;AAAA,EAChB;AAGA,aAAW,CAAC,WAAW,OAAO,KAAK,OAAO,QAAQ,OAAO,QAAQ,GAAG;AAClE,QAAI,CAACE,YAAW,QAAQ,SAAS,GAAG;AAClC,cAAQ,KAAK,oCAAoC,QAAQ,IAAI,KAAK,QAAQ,SAAS,EAAE;AAAA,IACvF;AAAA,EACF;AAEA,UAAQ,IAAI,UAAU,YAAY,yBAAyB;AAE3D,QAAM,SAAS,aAAa,MAAM;AAClC,QAAM,eAAe,uBAAuBD,SAAQ,WAAW,gBAAgB,CAAC;AAChF,QAAM,iBAAiB,qBAAqB,OAAO,UAAU,YAAY;AACzE,QAAM,MAAM,iBAAiB,QAAQ,gBAAgB,MAAM;AAE3D,WAAS,WAAW;AAClB,YAAQ,IAAI,kBAAkB;AAC9B,mBAAe,SAAS;AACxB,QAAI,KAAK;AACT,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,UAAQ,GAAG,UAAU,QAAQ;AAC7B,UAAQ,GAAG,WAAW,QAAQ;AAE9B,MAAI,MAAM,KAAK,EAAE,MAAM,CAAC,QAAQ;AAC9B,YAAQ,MAAM,wBAAwB,GAAG;AACzC,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;AAEA,SAAS,SAAS;AAChB,QAAM,YAAY,QAAQ,IAAI;AAC9B,QAAM,eAAeA,SAAQ,WAAW,gBAAgB;AAExD,MAAI,CAACC,YAAW,YAAY,GAAG;AAC7B,YAAQ,IAAI,iDAAiD;AAC7D;AAAA,EACF;AAEA,QAAM,aAAaD,SAAQ,WAAW,aAAa;AACnD,MAAI,eAAuC,CAAC;AAC5C,MAAIC,YAAW,UAAU,GAAG;AAC1B,QAAI;AACF,YAAM,MAAM,KAAK,MAAMF,cAAa,YAAY,OAAO,CAAC;AACxD,YAAM,SAAS,WAAW,GAAG;AAC7B,iBAAW,CAAC,WAAW,OAAO,KAAK,OAAO,QAAQ,OAAO,QAAQ,GAAG;AAClE,qBAAa,SAAS,IAAI,QAAQ;AAAA,MACpC;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,WAAW,KAAK,MAAMA,cAAa,cAAc,OAAO,CAAC;AAO/D,MAAI,SAAS,WAAW,GAAG;AACzB,YAAQ,IAAI,qBAAqB;AACjC;AAAA,EACF;AAEA,UAAQ,IAAI,aAAa,SAAS,MAAM;AAAA,CAAM;AAC9C,aAAW,KAAK,UAAU;AACxB,UAAM,OAAO,aAAa,EAAE,UAAU,KAAK,EAAE;AAC7C,UAAM,MAAM,KAAK,OAAO,KAAK,IAAI,IAAI,EAAE,gBAAgB,GAAK;AAC5D,YAAQ,IAAI,KAAK,IAAI,EAAE;AACvB,YAAQ,IAAI,gBAAgB,EAAE,SAAS,EAAE;AACzC,YAAQ,IAAI,gBAAgB,EAAE,GAAG,EAAE;AACnC,YAAQ,IAAI,gBAAgB,GAAG,GAAG;AAClC,YAAQ,IAAI;AAAA,EACd;AACF;AAEA,KAAK,EAAE,MAAM,CAAC,QAAQ;AACpB,UAAQ,MAAM,GAAG;AACjB,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":["resolve","existsSync","readFileSync","args","resolve","resolve","command","parentId","resolved","writeFileSync","resolve","readFileSync","resolve","existsSync"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "multi-project-gateway",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Route Discord messages to per-project Claude Code CLI sessions",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mpg": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup",
|
|
14
|
+
"start": "node dist/cli.js start",
|
|
15
|
+
"dev": "tsx src/cli.ts start",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"test:watch": "vitest"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"discord.js": "^14.14.0",
|
|
21
|
+
"dotenv": "^16.4.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^20.11.0",
|
|
25
|
+
"tsup": "^8.5.1",
|
|
26
|
+
"tsx": "^4.7.0",
|
|
27
|
+
"typescript": "^5.3.0",
|
|
28
|
+
"vitest": "^2.0.0"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"claude",
|
|
35
|
+
"claude-code",
|
|
36
|
+
"discord",
|
|
37
|
+
"gateway",
|
|
38
|
+
"multi-project"
|
|
39
|
+
],
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/yama-kei/multi-project-gateway.git"
|
|
43
|
+
},
|
|
44
|
+
"license": "MIT"
|
|
45
|
+
}
|