@mudramo/mudcode 0.6.6
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 +526 -0
- package/bin/discode +193 -0
- package/package.json +15 -0
- package/postinstall.mjs +21 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 cmc
|
|
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,526 @@
|
|
|
1
|
+
# Discode
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="./discode.png" alt="Discode" width="220" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
[English](README.md) | [한국어](docs/README.ko.md)
|
|
8
|
+
|
|
9
|
+
Bridge AI agent CLIs to Discord for remote monitoring and control.
|
|
10
|
+
|
|
11
|
+
> Derived from [DoBuDevel/discord-agent-bridge](https://github.com/DoBuDevel/discord-agent-bridge). This project preserves original authorship and builds on top of the upstream work.
|
|
12
|
+
|
|
13
|
+
[](https://www.typescriptlang.org/)
|
|
14
|
+
[](https://bun.sh/)
|
|
15
|
+
[](https://opensource.org/licenses/MIT)
|
|
16
|
+
[](./tests)
|
|
17
|
+
|
|
18
|
+
## Overview
|
|
19
|
+
|
|
20
|
+
Discoding - run AI coding CLIs locally and relay them to Discord.
|
|
21
|
+
|
|
22
|
+
I built this after experimenting with OpenClaw.
|
|
23
|
+
Even with full system permissions, I realized I preferred conversational control over full autonomy.
|
|
24
|
+
|
|
25
|
+
Instead of building another dashboard, I wired my AI CLI to Discord.
|
|
26
|
+
|
|
27
|
+
Discode runs your AI agent in tmux and simply relays output to Discord - no wrappers, no hidden execution layers, no cloud dependency.
|
|
28
|
+
- Local-first
|
|
29
|
+
- Relay-only architecture
|
|
30
|
+
- Persistent tmux sessions
|
|
31
|
+
- Single daemon managing multiple projects
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- **Multi-Agent Support**: Works with Claude Code, Gemini CLI, OpenCode, and OpenAI Codex CLI
|
|
38
|
+
- **Auto-Discovery**: Automatically detects installed AI agents on your system
|
|
39
|
+
- **Real-Time Streaming**: Sends agent outputs to Discord/Slack through event hooks
|
|
40
|
+
- **Project Isolation**: Each project gets a dedicated Discord channel
|
|
41
|
+
- **Single Daemon**: One Discord bot connection manages all projects
|
|
42
|
+
- **Session Management**: Persistent tmux sessions survive disconnections
|
|
43
|
+
- **Rich CLI**: Intuitive commands for setup, control, and monitoring
|
|
44
|
+
- **Type-Safe**: Written in TypeScript with dependency injection pattern
|
|
45
|
+
- **Well-Tested**: 322 unit tests with Vitest
|
|
46
|
+
|
|
47
|
+
## Supported Platforms
|
|
48
|
+
|
|
49
|
+
| Platform | Supported | Notes |
|
|
50
|
+
|----------|-----------|-------|
|
|
51
|
+
| **macOS** | Yes | Developed and tested |
|
|
52
|
+
| **Linux** | Expected | Should work (tmux-based), not yet tested |
|
|
53
|
+
| **Windows (WSL)** | Expected | Should work with tmux installed in WSL, not yet tested |
|
|
54
|
+
| **Windows (native)** | No | tmux is not available natively |
|
|
55
|
+
|
|
56
|
+
## Prerequisites
|
|
57
|
+
|
|
58
|
+
- **Bun**: Version 1.3 or higher
|
|
59
|
+
- **tmux**: Version 3.0 or higher
|
|
60
|
+
- Basic tmux proficiency (session/window/pane navigation, attach/detach) is recommended
|
|
61
|
+
- **Discord Bot**: Create a bot following the [Discord Bot Setup Guide](docs/DISCORD_SETUP.md)
|
|
62
|
+
- Required permissions: Send Messages, Manage Channels, Read Message History, Embed Links, Add Reactions
|
|
63
|
+
- Required intents: Guilds, GuildMessages, MessageContent, GuildMessageReactions
|
|
64
|
+
- **Slack (optional)**: Use Slack instead of Discord by following the [Slack Setup Guide](docs/SLACK_SETUP.md)
|
|
65
|
+
- **AI Agent**: At least one of:
|
|
66
|
+
- [Claude Code](https://code.claude.com/docs/en/overview)
|
|
67
|
+
- [Gemini CLI](https://github.com/google-gemini/gemini-cli)
|
|
68
|
+
- [OpenCode](https://github.com/OpenCodeAI/opencode)
|
|
69
|
+
- [OpenAI Codex CLI](https://github.com/openai/codex)
|
|
70
|
+
|
|
71
|
+
## Installation
|
|
72
|
+
|
|
73
|
+
### Global install (npm or Bun)
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npm install -g @siisee11/discode
|
|
77
|
+
bun add -g @siisee11/discode
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Binary install (no Bun/Node runtime required)
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
curl -fsSL https://discode.chat/install | bash
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Fallback:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
curl -fsSL https://raw.githubusercontent.com/siisee11/discode/main/install | bash
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### From source
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
git clone https://github.com/siisee11/discode.git
|
|
96
|
+
cd discode
|
|
97
|
+
bun install
|
|
98
|
+
bun run build
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
For local runtime switching and development workflows, see [`DEVELOPMENT.md`](./DEVELOPMENT.md).
|
|
102
|
+
|
|
103
|
+
## Uninstall
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
discode uninstall
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Full cleanup (remove config/state/logs and installed bridge plugins too):
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
discode uninstall --purge --yes
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Quick Start
|
|
116
|
+
|
|
117
|
+
### 1. Setup Discord Bot
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# One-time onboarding
|
|
121
|
+
discode onboard
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The `onboard` command prompts for your bot token, auto-detects the Discord server ID, lets you choose a default AI CLI, and asks whether to enable OpenCode `allow` permission mode. You can verify or change settings later:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
discode config --show # View current configuration
|
|
128
|
+
discode config --server SERVER_ID # Change server ID manually
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
> **Note**: `onboard` is required for initial configuration — it auto-detects the server ID by connecting to Discord. The `config` command only updates individual values without auto-detection.
|
|
132
|
+
|
|
133
|
+
### 2. Start Working
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
cd ~/projects/my-app
|
|
137
|
+
|
|
138
|
+
# Just run new — that's it!
|
|
139
|
+
discode new
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`new` handles everything automatically: detects installed agents, starts the daemon, creates a Discord channel, launches the agent in tmux, and attaches you to the session.
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
discode new claude # Specify an agent explicitly
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Your AI agent is now running in tmux, with output delivered to Discord/Slack in real time through hooks.
|
|
149
|
+
|
|
150
|
+
## CLI Reference
|
|
151
|
+
|
|
152
|
+
### Global Commands
|
|
153
|
+
|
|
154
|
+
#### `onboard`
|
|
155
|
+
|
|
156
|
+
One-time onboarding: prompts for bot token, connects to Discord to auto-detect your server, lets you choose your default AI CLI, and configures OpenCode permission mode.
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
discode onboard
|
|
160
|
+
# Optional for non-interactive shells
|
|
161
|
+
discode onboard --token YOUR_BOT_TOKEN
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The onboarding flow will:
|
|
165
|
+
1. Save your bot token to `~/.discode/config.json`
|
|
166
|
+
2. Connect to Discord and detect which server(s) your bot is in
|
|
167
|
+
3. If the bot is in multiple servers, prompt you to select one
|
|
168
|
+
4. Let you choose a default AI CLI for `discode new`
|
|
169
|
+
5. Ask whether to set OpenCode permission mode to `allow`
|
|
170
|
+
6. Warn that non-`allow` mode may cause inconvenient approval prompts in Discord
|
|
171
|
+
|
|
172
|
+
#### `daemon <action>`
|
|
173
|
+
|
|
174
|
+
Control the global daemon process.
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
discode daemon start # Start daemon
|
|
178
|
+
discode daemon stop # Stop daemon
|
|
179
|
+
discode daemon status # Check daemon status
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
#### `list`
|
|
183
|
+
|
|
184
|
+
List all registered projects.
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
discode list
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
#### `agents`
|
|
191
|
+
|
|
192
|
+
List available AI agents detected on your system.
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
discode agents
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
#### `tui`
|
|
199
|
+
|
|
200
|
+
Open interactive terminal UI. Use `/new` inside the TUI to create a new agent session.
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
discode tui
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### `config [options]`
|
|
207
|
+
|
|
208
|
+
View or update global configuration.
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
discode config --show # Show current configuration
|
|
212
|
+
discode config --token NEW_TOKEN # Update bot token
|
|
213
|
+
discode config --server SERVER_ID # Set Discord server ID manually
|
|
214
|
+
discode config --port 18470 # Set hook server port
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Project Commands
|
|
218
|
+
|
|
219
|
+
Run these commands from your project directory.
|
|
220
|
+
|
|
221
|
+
#### `start [options]`
|
|
222
|
+
|
|
223
|
+
Start the bridge server for registered projects.
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
discode start # Start all projects
|
|
227
|
+
discode start -p my-app # Start a specific project
|
|
228
|
+
discode start -p my-app --attach # Start and attach to tmux
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
#### `stop [project]`
|
|
232
|
+
|
|
233
|
+
Stop a project: kills tmux session, deletes Discord channel, and removes project state. Defaults to current directory name if project is not specified.
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
discode stop # Stop current directory's project
|
|
237
|
+
discode stop my-app # Stop a specific project
|
|
238
|
+
discode stop --keep-channel # Keep Discord channel (only kill tmux)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### `status`
|
|
242
|
+
|
|
243
|
+
Show project status.
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
discode status
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### `attach [project]`
|
|
250
|
+
|
|
251
|
+
Attach to a project's tmux session. Defaults to current directory name if project is not specified.
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
discode attach # Attach to current directory's project
|
|
255
|
+
discode attach my-app # Attach to a specific project
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Press `Ctrl-b d` to detach from tmux without stopping the agent.
|
|
259
|
+
|
|
260
|
+
#### `new [agent] [options]`
|
|
261
|
+
|
|
262
|
+
Quick start: start daemon, set up project if needed, and attach to tmux. Auto-detects installed agents and creates the Discord channel automatically.
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
discode new # Auto-detect agent, setup & attach
|
|
266
|
+
discode new claude # Use a specific agent
|
|
267
|
+
discode new --no-attach # Start without attaching to tmux
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Supported Agents
|
|
271
|
+
|
|
272
|
+
| Agent | Binary | Auto-Detect | Notes |
|
|
273
|
+
|-------|--------|-------------|-------|
|
|
274
|
+
| **Claude Code** | `claude` | Yes | Official Anthropic CLI |
|
|
275
|
+
| **Gemini CLI** | `gemini` | Yes | Google Gemini CLI |
|
|
276
|
+
| **OpenCode** | `opencode` | Yes | Open-source alternative |
|
|
277
|
+
| **OpenAI Codex CLI** | `codex` | Yes | Uses tmux capture fallback (no native hook) |
|
|
278
|
+
|
|
279
|
+
### Agent Detection
|
|
280
|
+
|
|
281
|
+
The CLI automatically detects installed agents using `command -v <binary>`. Run `discode agents` to see available agents on your system.
|
|
282
|
+
|
|
283
|
+
## Configuration
|
|
284
|
+
|
|
285
|
+
### Global Config
|
|
286
|
+
|
|
287
|
+
Stored in `~/.discode/config.json`:
|
|
288
|
+
|
|
289
|
+
```json
|
|
290
|
+
{
|
|
291
|
+
"token": "YOUR_BOT_TOKEN",
|
|
292
|
+
"serverId": "YOUR_SERVER_ID",
|
|
293
|
+
"hookServerPort": 18470
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
| Key | Required | Description | Default |
|
|
298
|
+
|-----|----------|-------------|---------|
|
|
299
|
+
| `token` | **Yes** | Discord bot token. Set via `discode onboard` or `config --token` | - |
|
|
300
|
+
| `serverId` | **Yes** | Discord server (guild) ID. Auto-detected by `onboard`, or set via `config --server` | - |
|
|
301
|
+
| `hookServerPort` | No | Port for the hook server | `18470` |
|
|
302
|
+
| `defaultAgentCli` | No | Default AI CLI used by `discode new` when agent is omitted | First installed CLI |
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
discode config --show # View current config
|
|
306
|
+
discode config --token NEW_TOKEN # Update bot token
|
|
307
|
+
discode config --server SERVER_ID # Set server ID manually
|
|
308
|
+
discode config --port 18470 # Set hook server port
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Project State
|
|
312
|
+
|
|
313
|
+
Project state is stored in `~/.discode/state.json` and managed automatically.
|
|
314
|
+
|
|
315
|
+
### Environment Variables
|
|
316
|
+
|
|
317
|
+
Config values can be overridden with environment variables:
|
|
318
|
+
|
|
319
|
+
| Variable | Required | Description | Default |
|
|
320
|
+
|----------|----------|-------------|---------|
|
|
321
|
+
| `DISCORD_BOT_TOKEN` | **Yes** (if not in config.json) | Discord bot token | - |
|
|
322
|
+
| `DISCORD_GUILD_ID` | **Yes** (if not in config.json) | Discord server ID | - |
|
|
323
|
+
| `DISCORD_CHANNEL_ID` | No | Override default channel | Auto-created per project |
|
|
324
|
+
| `TMUX_SESSION_PREFIX` | No | Prefix for tmux session names | `` |
|
|
325
|
+
| `TMUX_SHARED_SESSION_NAME` | No | Shared tmux session name (without prefix) | `bridge` |
|
|
326
|
+
| `DISCODE_DEFAULT_AGENT_CLI` | No | Default AI CLI used by `discode new` when agent is omitted | First installed CLI |
|
|
327
|
+
| `HOOK_SERVER_PORT` | No | Port for the hook server | `18470` |
|
|
328
|
+
| `AGENT_DISCORD_CAPTURE_POLL_MS` | No | Poll interval (ms) for non-hook agents like Codex | `3000` |
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
DISCORD_BOT_TOKEN=token discode daemon start
|
|
332
|
+
DISCORD_GUILD_ID=server_id discode new
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Development
|
|
336
|
+
|
|
337
|
+
Architecture overview: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
|
|
338
|
+
Module boundaries: [docs/MODULE_BOUNDARIES.md](docs/MODULE_BOUNDARIES.md)
|
|
339
|
+
|
|
340
|
+
### Building
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
bun install
|
|
344
|
+
bun run build # Compile TypeScript
|
|
345
|
+
bun run dev # Dev mode
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Release Packaging (prebuilt binaries)
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
npm run build:release # Build platform binaries + npm meta package
|
|
352
|
+
npm run build:release:binaries:single # Build only current OS/arch binary
|
|
353
|
+
npm run pack:release # Create npm tarballs in dist/release
|
|
354
|
+
npm run publish:release:dry-run # Build + validate npm publish flow without uploading
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Rust daemon sidecar (optional, for `DISCODE_DAEMON_RUNTIME=rust`) can be bundled into each platform package:
|
|
358
|
+
|
|
359
|
+
- `DISCODE_RS_BIN` - one binary path used for all targets
|
|
360
|
+
- `DISCODE_RS_BIN_<SUFFIX>` - per-target binary path (example: `DISCODE_RS_BIN_LINUX_X64`)
|
|
361
|
+
- `DISCODE_RS_PREBUILT_DIR` - directory containing prebuilt `discode-rs-*` binaries
|
|
362
|
+
- If no path is provided for the host target, `build-binaries` auto-attempts `cargo build --release` from `discode-rs/`
|
|
363
|
+
- `DISCODE_RS_SKIP_LOCAL_BUILD=1` - disable that auto-build fallback
|
|
364
|
+
- `DISCODE_NPM_SCOPE=@your-npm-id` - override publish scope for release packages (meta + platform binaries)
|
|
365
|
+
|
|
366
|
+
One-shot publish under your own npm scope:
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
DISCODE_NPM_SCOPE=@your-npm-id npm run publish:release
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Publish with Bun instead of npm:
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
DISCODE_NPM_SCOPE=@your-npm-id npm run publish:release:bun
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
Publish only the current host target (recommended for local machines):
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
DISCODE_NPM_SCOPE=@your-npm-id npm run publish:release:bun:single
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Full multi-platform release (Linux/macOS/Windows matrix) is provided via GitHub Actions:
|
|
385
|
+
|
|
386
|
+
- Workflow: `.github/workflows/release-publish.yml`
|
|
387
|
+
- Required secret: `NPM_TOKEN`
|
|
388
|
+
|
|
389
|
+
### Testing
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
bun test # Run all tests
|
|
393
|
+
bun run test:watch # Watch mode
|
|
394
|
+
bun run test:coverage # Coverage report
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Test suite includes 322 tests covering:
|
|
398
|
+
- Agent adapters
|
|
399
|
+
- State management
|
|
400
|
+
- Discord client
|
|
401
|
+
- Hook-based event delivery
|
|
402
|
+
- CLI commands
|
|
403
|
+
- Storage and execution mocks
|
|
404
|
+
|
|
405
|
+
### Project Structure
|
|
406
|
+
|
|
407
|
+
```
|
|
408
|
+
discode/
|
|
409
|
+
├── bin/ # CLI entry point (discode)
|
|
410
|
+
├── src/
|
|
411
|
+
│ ├── agents/ # Agent adapters (Claude, Gemini, OpenCode, Codex)
|
|
412
|
+
│ ├── capture/ # shared message parsing utilities
|
|
413
|
+
│ ├── config/ # Configuration management
|
|
414
|
+
│ ├── discord/ # Discord client and message handlers
|
|
415
|
+
│ ├── infra/ # Infrastructure (storage, shell, environment)
|
|
416
|
+
│ ├── state/ # Project state management
|
|
417
|
+
│ ├── tmux/ # tmux session management
|
|
418
|
+
│ └── types/ # TypeScript interfaces
|
|
419
|
+
├── tests/ # Vitest test suite
|
|
420
|
+
├── package.json
|
|
421
|
+
└── tsconfig.json
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Dependency Injection
|
|
425
|
+
|
|
426
|
+
The codebase uses constructor injection with interfaces for testability:
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
// Interfaces
|
|
430
|
+
interface IStorage { readFile, writeFile, exists, unlink, mkdirp, chmod }
|
|
431
|
+
interface ICommandExecutor { exec, execVoid }
|
|
432
|
+
interface IEnvironment { get, homedir, platform }
|
|
433
|
+
|
|
434
|
+
// Usage
|
|
435
|
+
class DaemonManager {
|
|
436
|
+
constructor(
|
|
437
|
+
private storage: IStorage = new FileStorage(),
|
|
438
|
+
private executor: ICommandExecutor = new ShellCommandExecutor()
|
|
439
|
+
) {}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Testing
|
|
443
|
+
const mockStorage = new MockStorage();
|
|
444
|
+
const daemon = new DaemonManager(mockStorage);
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Code Quality
|
|
448
|
+
|
|
449
|
+
- TypeScript strict mode enabled
|
|
450
|
+
- ESM modules with `.js` extensions in imports
|
|
451
|
+
- Vitest with 322 passing tests
|
|
452
|
+
- No unused locals/parameters (enforced by `tsconfig.json`)
|
|
453
|
+
|
|
454
|
+
## Troubleshooting
|
|
455
|
+
|
|
456
|
+
### Bot not connecting
|
|
457
|
+
|
|
458
|
+
1. Verify token: `discode config --show`
|
|
459
|
+
2. Check bot permissions in Discord Developer Portal
|
|
460
|
+
3. Ensure MessageContent intent is enabled
|
|
461
|
+
4. Restart daemon: `discode daemon stop && discode daemon start`
|
|
462
|
+
|
|
463
|
+
### Agent not detected
|
|
464
|
+
|
|
465
|
+
1. Run `discode agents` to see available agents
|
|
466
|
+
2. Verify agent binary is in PATH: `which claude`
|
|
467
|
+
3. Install missing agent and retry
|
|
468
|
+
|
|
469
|
+
### tmux session issues
|
|
470
|
+
|
|
471
|
+
1. Check session exists: `tmux ls`
|
|
472
|
+
2. Kill stale session: `tmux kill-session -t <session-name>`
|
|
473
|
+
3. Restart project: `discode stop && discode start`
|
|
474
|
+
|
|
475
|
+
### No messages in Discord
|
|
476
|
+
|
|
477
|
+
1. Check daemon status: `discode daemon status`
|
|
478
|
+
2. Check daemon logs
|
|
479
|
+
3. Check Discord channel permissions (bot needs Send Messages)
|
|
480
|
+
|
|
481
|
+
### Tip: Keep running with lid closed (macOS)
|
|
482
|
+
|
|
483
|
+
If you want Discode to keep working when the laptop lid is closed on battery power, run:
|
|
484
|
+
|
|
485
|
+
```bash
|
|
486
|
+
sudo pmset -b disablesleep 1
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
To revert to normal sleep behavior:
|
|
490
|
+
|
|
491
|
+
```bash
|
|
492
|
+
sudo pmset -b disablesleep 0
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
## Contributing
|
|
496
|
+
|
|
497
|
+
Contributions are welcome! Please:
|
|
498
|
+
|
|
499
|
+
1. Fork the repository
|
|
500
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
501
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
502
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
503
|
+
5. Open a Pull Request
|
|
504
|
+
|
|
505
|
+
### Guidelines
|
|
506
|
+
|
|
507
|
+
- Add tests for new features
|
|
508
|
+
- Maintain TypeScript strict mode compliance
|
|
509
|
+
- Follow existing code style
|
|
510
|
+
- Update documentation as needed
|
|
511
|
+
|
|
512
|
+
## License
|
|
513
|
+
|
|
514
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
515
|
+
|
|
516
|
+
## Acknowledgments
|
|
517
|
+
|
|
518
|
+
- Built with [Discord.js](https://discord.js.org/)
|
|
519
|
+
- Powered by [Claude Code](https://code.claude.com/docs/en/overview), [Gemini CLI](https://github.com/google-gemini/gemini-cli), and [OpenCode](https://github.com/OpenCodeAI/opencode)
|
|
520
|
+
- Inspired by [OpenClaw](https://github.com/nicepkg/openclaw)'s messenger-based command system. The motivation was to remotely control and monitor long-running AI agent tasks from anywhere via Discord.
|
|
521
|
+
|
|
522
|
+
## Support
|
|
523
|
+
|
|
524
|
+
- Issues: [GitHub Issues](https://github.com/siisee11/discode/issues)
|
|
525
|
+
- Discord Bot Setup: [Setup Guide](docs/DISCORD_SETUP.md)
|
|
526
|
+
- Slack Setup: [Setup Guide](docs/SLACK_SETUP.md)
|
package/bin/discode
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import childProcess from 'child_process';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { createRequire } from 'module';
|
|
9
|
+
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
|
|
14
|
+
function normalizeScope(raw) {
|
|
15
|
+
const trimmed = (raw || '').trim();
|
|
16
|
+
if (!trimmed) return '';
|
|
17
|
+
return trimmed.startsWith('@') ? trimmed : `@${trimmed}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readSelfPackageName() {
|
|
21
|
+
const candidates = [
|
|
22
|
+
path.join(__dirname, '..', 'package.json'),
|
|
23
|
+
path.join(__dirname, '..', '..', 'package.json'),
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
for (const candidate of candidates) {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
|
|
29
|
+
if (typeof parsed?.name === 'string' && parsed.name.length > 0) {
|
|
30
|
+
return parsed.name;
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
// continue
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolvePackageScope() {
|
|
41
|
+
const envScope = normalizeScope(process.env.DISCODE_NPM_SCOPE);
|
|
42
|
+
if (envScope) return envScope;
|
|
43
|
+
|
|
44
|
+
const packageName = readSelfPackageName();
|
|
45
|
+
const match = packageName.match(/^(@[^/]+)\//);
|
|
46
|
+
if (match) return match[1];
|
|
47
|
+
|
|
48
|
+
return '@siisee11';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const packageScope = resolvePackageScope();
|
|
52
|
+
|
|
53
|
+
function spawnAndExit(command, args) {
|
|
54
|
+
const result = childProcess.spawnSync(command, args, {
|
|
55
|
+
stdio: 'inherit',
|
|
56
|
+
env: process.env,
|
|
57
|
+
});
|
|
58
|
+
if (result.error) {
|
|
59
|
+
console.error(result.error.message);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
process.exit(typeof result.status === 'number' ? result.status : 0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function spawnAndExitWithEnv(command, args, envPatch) {
|
|
66
|
+
const result = childProcess.spawnSync(command, args, {
|
|
67
|
+
stdio: 'inherit',
|
|
68
|
+
env: {
|
|
69
|
+
...process.env,
|
|
70
|
+
...envPatch,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
if (result.error) {
|
|
74
|
+
console.error(result.error.message);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
process.exit(typeof result.status === 'number' ? result.status : 0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isMuslLinux() {
|
|
81
|
+
if (os.platform() !== 'linux') return false;
|
|
82
|
+
if (fs.existsSync('/etc/alpine-release')) return true;
|
|
83
|
+
try {
|
|
84
|
+
const out = childProcess.execSync('ldd --version 2>&1', { encoding: 'utf-8' });
|
|
85
|
+
return out.toLowerCase().includes('musl');
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function needsBaselineX64() {
|
|
92
|
+
if (os.arch() !== 'x64') return false;
|
|
93
|
+
if (os.platform() === 'linux') {
|
|
94
|
+
try {
|
|
95
|
+
const cpuinfo = fs.readFileSync('/proc/cpuinfo', 'utf-8').toLowerCase();
|
|
96
|
+
return !cpuinfo.includes('avx2');
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (os.platform() === 'darwin') {
|
|
102
|
+
try {
|
|
103
|
+
const out = childProcess.execSync('sysctl -n hw.optional.avx2_0', { encoding: 'utf-8' }).trim();
|
|
104
|
+
return out !== '1';
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function packageCandidates() {
|
|
113
|
+
const platformMap = {
|
|
114
|
+
darwin: 'darwin',
|
|
115
|
+
linux: 'linux',
|
|
116
|
+
win32: 'windows',
|
|
117
|
+
};
|
|
118
|
+
const archMap = {
|
|
119
|
+
x64: 'x64',
|
|
120
|
+
arm64: 'arm64',
|
|
121
|
+
};
|
|
122
|
+
const platform = platformMap[os.platform()] || os.platform();
|
|
123
|
+
const arch = archMap[os.arch()] || os.arch();
|
|
124
|
+
const scopedBase = `${packageScope}/discode-${platform}-${arch}`;
|
|
125
|
+
|
|
126
|
+
const candidates = [];
|
|
127
|
+
if (platform === 'linux' && isMuslLinux()) {
|
|
128
|
+
if (arch === 'x64' && needsBaselineX64()) candidates.push(`${scopedBase}-baseline-musl`);
|
|
129
|
+
candidates.push(`${scopedBase}-musl`);
|
|
130
|
+
}
|
|
131
|
+
if (arch === 'x64' && needsBaselineX64()) candidates.push(`${scopedBase}-baseline`);
|
|
132
|
+
candidates.push(scopedBase);
|
|
133
|
+
return candidates;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function findPlatformBinary() {
|
|
137
|
+
const binaryName = os.platform() === 'win32' ? 'discode.exe' : 'discode';
|
|
138
|
+
for (const pkg of packageCandidates()) {
|
|
139
|
+
try {
|
|
140
|
+
const packageJsonPath = require.resolve(`${pkg}/package.json`);
|
|
141
|
+
const packageDir = path.dirname(packageJsonPath);
|
|
142
|
+
const binaryPath = path.join(packageDir, 'bin', binaryName);
|
|
143
|
+
if (fs.existsSync(binaryPath)) {
|
|
144
|
+
return { binaryPath, packageDir };
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
// try next package candidate
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function resolveRustSidecarFromDir(dir) {
|
|
154
|
+
const sidecarName = os.platform() === 'win32' ? 'discode-rs.exe' : 'discode-rs';
|
|
155
|
+
const sidecarPath = path.join(dir, sidecarName);
|
|
156
|
+
return fs.existsSync(sidecarPath) ? sidecarPath : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function findLocalDevEntrypoint() {
|
|
160
|
+
const scriptDir = __dirname;
|
|
161
|
+
const candidates = [
|
|
162
|
+
path.join(scriptDir, '..', 'dist', 'bin', 'discode.js'),
|
|
163
|
+
path.join(scriptDir, '..', '..', 'dist', 'bin', 'discode.js'),
|
|
164
|
+
];
|
|
165
|
+
return candidates.find((file) => fs.existsSync(file)) || null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const explicitBinary = process.env.DISCODE_BIN_PATH;
|
|
169
|
+
if (explicitBinary) {
|
|
170
|
+
const rustSidecar = resolveRustSidecarFromDir(path.dirname(explicitBinary));
|
|
171
|
+
if (rustSidecar && !process.env.DISCODE_RS_BIN) {
|
|
172
|
+
spawnAndExitWithEnv(explicitBinary, process.argv.slice(2), { DISCODE_RS_BIN: rustSidecar });
|
|
173
|
+
}
|
|
174
|
+
spawnAndExit(explicitBinary, process.argv.slice(2));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const platformBinary = findPlatformBinary();
|
|
178
|
+
if (platformBinary) {
|
|
179
|
+
const rustSidecar = resolveRustSidecarFromDir(path.join(platformBinary.packageDir, 'bin'));
|
|
180
|
+
if (rustSidecar && !process.env.DISCODE_RS_BIN) {
|
|
181
|
+
spawnAndExitWithEnv(platformBinary.binaryPath, process.argv.slice(2), { DISCODE_RS_BIN: rustSidecar });
|
|
182
|
+
}
|
|
183
|
+
spawnAndExit(platformBinary.binaryPath, process.argv.slice(2));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const devEntrypoint = findLocalDevEntrypoint();
|
|
187
|
+
if (devEntrypoint) {
|
|
188
|
+
spawnAndExit(process.execPath, [devEntrypoint, ...process.argv.slice(2)]);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
console.error('No runnable discode binary found for this platform.');
|
|
192
|
+
console.error(`Try reinstalling with: npm i -g ${packageScope}/discode`);
|
|
193
|
+
process.exit(1);
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mudramo/mudcode",
|
|
3
|
+
"version": "0.6.6",
|
|
4
|
+
"description": "Bridge AI agent outputs to Discord via Claude Code hooks and tmux",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"discode": "bin/discode"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"postinstall": "node ./postinstall.mjs"
|
|
11
|
+
},
|
|
12
|
+
"optionalDependencies": {
|
|
13
|
+
"@mudramo/discode-linux-x64": "0.6.6"
|
|
14
|
+
}
|
|
15
|
+
}
|
package/postinstall.mjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const platformMap = {
|
|
6
|
+
darwin: 'darwin',
|
|
7
|
+
linux: 'linux',
|
|
8
|
+
win32: 'windows',
|
|
9
|
+
};
|
|
10
|
+
const archMap = {
|
|
11
|
+
x64: 'x64',
|
|
12
|
+
arm64: 'arm64',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const platform = platformMap[os.platform()] || os.platform();
|
|
16
|
+
const arch = archMap[os.arch()] || os.arch();
|
|
17
|
+
|
|
18
|
+
if ((platform !== 'darwin' && platform !== 'linux' && platform !== 'windows') || (arch !== 'x64' && arch !== 'arm64')) {
|
|
19
|
+
console.warn(`[discode] No prebuilt binary available for ${platform}/${arch}.`);
|
|
20
|
+
console.warn('[discode] You can still run from source with Node + Bun installed.');
|
|
21
|
+
}
|