@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 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
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
14
+ [![Bun](https://img.shields.io/badge/Bun-1.3+-green.svg)](https://bun.sh/)
15
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
16
+ [![Tests](https://img.shields.io/badge/Tests-322%20passing-brightgreen.svg)](./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
+ ![Discode Demo](./discode-demo.gif)
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
+ }
@@ -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
+ }