Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions benchmarks/terminal_bench/mux-run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ MUX_TRUNK="${MUX_TRUNK:-main}"
MUX_WORKSPACE_ID="${MUX_WORKSPACE_ID:-mux-bench}"
MUX_THINKING_LEVEL="${MUX_THINKING_LEVEL:-high}"
MUX_MODE="${MUX_MODE:-exec}"
MUX_RUNTIME="${MUX_RUNTIME:-}"

resolve_project_path() {
if [[ -n "${MUX_PROJECT_PATH}" ]]; then
Expand Down Expand Up @@ -77,21 +78,21 @@ ensure_git_repo "${project_path}"
log "starting mux agent session for ${project_path}"
cd "${MUX_APP_ROOT}"

cmd=(bun src/cli/debug/agentSessionCli.ts
--config-root "${MUX_CONFIG_ROOT}"
--project-path "${project_path}"
--workspace-path "${project_path}"
--workspace-id "${MUX_WORKSPACE_ID}"
cmd=(bun src/cli/run.ts
--dir "${project_path}"
--model "${MUX_MODEL}"
--mode "${MUX_MODE}"
--json-streaming)
--thinking "${MUX_THINKING_LEVEL}"
--config-root "${MUX_CONFIG_ROOT}"
--workspace-id "${MUX_WORKSPACE_ID}"
--json)

if [[ -n "${MUX_TIMEOUT_MS}" ]]; then
cmd+=(--timeout "${MUX_TIMEOUT_MS}")
fi

if [[ -n "${MUX_THINKING_LEVEL}" ]]; then
cmd+=(--thinking-level "${MUX_THINKING_LEVEL}")
if [[ -n "${MUX_RUNTIME}" ]]; then
cmd+=(--runtime "${MUX_RUNTIME}")
fi

# Terminal-bench enforces timeouts via --global-agent-timeout-sec
Expand Down
1 change: 1 addition & 0 deletions benchmarks/terminal_bench/mux_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class MuxAgent(AbstractInstalledAgent):
"MUX_APP_ROOT",
"MUX_WORKSPACE_ID",
"MUX_MODE",
"MUX_RUNTIME",
)

def __init__(
Expand Down
3 changes: 3 additions & 0 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"motion": "^12.23.24",
"ollama-ai-provider-v2": "^1.5.4",
"openai": "^6.9.1",
"parse-duration": "^2.1.4",
"rehype-harden": "^1.1.5",
"shescape": "^2.1.6",
"source-map-support": "^0.5.21",
Expand Down Expand Up @@ -2937,6 +2938,8 @@

"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],

"parse-duration": ["parse-duration@2.1.4", "", {}, "sha512-b98m6MsCh+akxfyoz9w9dt0AlH2dfYLOBss5SdDsr9pkhKNvkWBXU/r8A4ahmIGByBOLV2+4YwfCuFxbDDaGyg=="],

"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],

"parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
Expand Down
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- [Introduction](./intro.md)
- [Install](./install.md)
- [CLI](./cli.md)
- [Why Parallelize?](./why-parallelize.md)

# Features
Expand Down
3 changes: 2 additions & 1 deletion docs/benchmarking.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Optional environment overrides:
| `MUX_MODEL` | Preferred model (supports `provider/model` syntax) | `anthropic/claude-sonnet-4-5` |
| `MUX_THINKING_LEVEL` | Optional reasoning level (`off`, `low`, `medium`, `high`) | `high` |
| `MUX_MODE` | Starting mode (`plan` or `exec`) | `exec` |
| `MUX_RUNTIME` | Runtime type (`local`, `worktree`, or `ssh <host>`) | `worktree` |
| `MUX_TIMEOUT_MS` | Optional stream timeout in milliseconds | no timeout |
| `MUX_CONFIG_ROOT` | Location for mux session data inside the container | `/root/.mux` |
| `MUX_APP_ROOT` | Path where the mux sources are staged | `/opt/mux-app` |
Expand Down Expand Up @@ -65,7 +66,7 @@ The adapter lives in `benchmarks/terminal_bench/mux_agent.py`. For each task it:

1. Copies the mux repository (package manifests + `src/`) into `/tmp/mux-app` inside the container.
2. Ensures Bun exists, then runs `bun install --frozen-lockfile`.
3. Launches `src/cli/debug/agentSessionCli.ts` to prepare workspace metadata and stream the instruction, storing state under `MUX_CONFIG_ROOT` (default `/root/.mux`).
3. Launches `mux run` (`src/cli/run.ts`) to prepare workspace metadata and stream the instruction, storing state under `MUX_CONFIG_ROOT` (default `/root/.mux`).

`MUX_MODEL` accepts either the mux colon form (`anthropic:claude-sonnet-4-5`) or the Terminal-Bench slash form (`anthropic/claude-sonnet-4-5`); the adapter normalises whichever you provide.

Expand Down
101 changes: 101 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Command Line Interface

Mux provides a CLI for running one-off agent tasks without the desktop app. Unlike the interactive desktop experience, `mux run` executes a single request to completion and exits.

## `mux run`

Execute a one-off agent task:

```bash
# Basic usage - run in current directory
mux run "Fix the failing tests"

# Specify a directory
mux run --dir /path/to/project "Add authentication"

# Use SSH runtime
mux run --runtime "ssh user@myserver" "Deploy changes"

# Pipe instructions via stdin
echo "Add logging to all API endpoints" | mux run

# JSON output for scripts
mux run --json "List all TypeScript files" | jq '.type'
```

### Options

| Option | Short | Description | Default |
| ---------------------- | ----- | -------------------------------------------------- | ----------------- |
| `--dir <path>` | `-d` | Project directory | Current directory |
| `--model <model>` | `-m` | Model to use (e.g., `anthropic:claude-sonnet-4-5`) | Default model |
| `--runtime <runtime>` | `-r` | Runtime: `local`, `worktree`, or `ssh <host>` | `local` |
| `--mode <mode>` | | Agent mode: `plan` or `exec` | `exec` |
| `--thinking <level>` | `-t` | Thinking level: `off`, `low`, `medium`, `high` | `medium` |
| `--timeout <duration>` | | Timeout (e.g., `5m`, `300s`, `300000`) | No timeout |
| `--json` | | Output NDJSON for programmatic use | Off |
| `--quiet` | `-q` | Only output final result | Off |
| `--workspace-id <id>` | | Explicit workspace ID | Auto-generated |
| `--config-root <path>` | | Mux config directory | `~/.mux` |

### Runtimes

- **`local`** (default): Runs directly in the specified directory. Best for one-off tasks.
- **`worktree`**: Creates an isolated git worktree under `~/.mux/src`. Useful for parallel work.
- **`ssh <host>`**: Runs on a remote machine via SSH. Example: `--runtime "ssh user@myserver.com"`

### Output Modes

- **Default (TTY)**: Human-readable streaming with tool call formatting
- **`--json`**: NDJSON streaming - each line is a JSON object with event data
- **`--quiet`**: Suppresses streaming output, only shows final assistant response

### Examples

```bash
# Quick fix in current directory
mux run "Fix the TypeScript errors"

# Use a specific model with extended thinking
mux run -m anthropic:claude-sonnet-4-5 -t high "Optimize database queries"

# Run on remote server
mux run -r "ssh dev@staging.example.com" -d /app "Update dependencies"

# Scripted usage with timeout
mux run --json --timeout 5m "Generate API documentation" > output.jsonl
```

## `mux server`

Start the HTTP/WebSocket server for remote access (e.g., from mobile devices):

```bash
mux server --port 3000 --host 0.0.0.0
```

Options:

- `--host <host>` - Host to bind to (default: `localhost`)
- `--port <port>` - Port to bind to (default: `3000`)
- `--auth-token <token>` - Optional bearer token for authentication
- `--add-project <path>` - Add and open project at the specified path

## `mux desktop`

Launch the desktop app. This is automatically invoked when running the packaged app or via `electron .`:

```bash
mux desktop
```

Note: Requires Electron. When running `mux` with no arguments under Electron, the desktop app launches automatically.

## `mux --version`

Print the version and git commit:

```bash
mux --version
# v0.8.4 (abc123)
```
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"motion": "^12.23.24",
"ollama-ai-provider-v2": "^1.5.4",
"openai": "^6.9.1",
"parse-duration": "^2.1.4",
"rehype-harden": "^1.1.5",
"shescape": "^2.1.6",
"source-map-support": "^0.5.21",
Expand Down
2 changes: 1 addition & 1 deletion scripts/check-bench-agent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ if [[ ! -f "$MUX_RUN_SH" ]]; then
fi

# Extract the agent CLI path from mux-run.sh
# Looks for line like: cmd=(bun src/cli/debug/agentSessionCli.ts
# Looks for line like: cmd=(bun src/cli/run.ts
CLI_PATH_MATCH=$(grep -o "bun src/.*\.ts" "$MUX_RUN_SH" | head -1 | cut -d' ' -f2)

if [[ -z "$CLI_PATH_MATCH" ]]; then
Expand Down
3 changes: 2 additions & 1 deletion src/browser/stories/mockFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
MuxImagePart,
MuxToolPart,
} from "@/common/types/message";
import { DEFAULT_MODEL } from "@/common/constants/knownModels";

/** Part type for message construction */
type MuxPart = MuxTextPart | MuxReasoningPart | MuxImagePart | MuxToolPart;
Expand Down Expand Up @@ -196,7 +197,7 @@ export function createAssistantMessage(
metadata: {
historySequence: opts.historySequence,
timestamp: opts.timestamp ?? STABLE_TIMESTAMP,
model: opts.model ?? "anthropic:claude-sonnet-4-5",
model: opts.model ?? DEFAULT_MODEL,
usage: { inputTokens: 100, outputTokens: 50, totalTokens: 150 },
duration: 1000,
},
Expand Down
3 changes: 2 additions & 1 deletion src/browser/stories/storyHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getInputKey,
getModelKey,
} from "@/common/constants/storage";
import { DEFAULT_MODEL } from "@/common/constants/knownModels";
import {
createWorkspace,
groupWorkspacesByProject,
Expand Down Expand Up @@ -178,7 +179,7 @@ export function setupStreamingChatStory(opts: StreamingChatSetupOptions): APICli
createStreamingChatHandler({
messages: opts.messages,
streamingMessageId: opts.streamingMessageId,
model: opts.model ?? "anthropic:claude-sonnet-4-5",
model: opts.model ?? DEFAULT_MODEL,
historySequence: opts.historySequence,
streamText: opts.streamText,
pendingTool: opts.pendingTool,
Expand Down
95 changes: 52 additions & 43 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,60 @@
#!/usr/bin/env node

/**
* Mux CLI entry point.
*
* LAZY LOADING REQUIREMENT:
* We manually route subcommands before calling program.parse() to avoid
* eagerly importing heavy modules. The desktop app imports Electron, which
* fails when running CLI commands in non-GUI environments. Subcommands like
* `run` and `server` import the AI SDK which has significant startup cost.
*
* By checking argv[2] first, we only load the code path actually needed.
*
* ELECTRON DETECTION:
* When run via `electron .` or as a packaged app, Electron sets process.versions.electron.
* In that case, we launch the desktop app automatically. When run via `bun` or `node`,
* we show CLI help instead.
*/
import { Command } from "commander";
import { VERSION } from "../version";

const program = new Command();

program
.name("mux")
.description("mux - coder multiplexer")
.version(`mux ${VERSION.git_describe} (${VERSION.git_commit})`, "-v, --version");

// Subcommands with their own CLI parsers - disable help interception so --help passes through
program
.command("server")
.description("Start the HTTP/WebSocket oRPC server")
.helpOption(false)
.allowUnknownOption()
.allowExcessArguments()
.action(() => {
process.argv.splice(2, 1);
// eslint-disable-next-line @typescript-eslint/no-require-imports
require("./server");
});

program
.command("api")
.description("Interact with the mux API via a running server")
.helpOption(false)
.allowUnknownOption()
.allowExcessArguments()
.action(() => {
process.argv.splice(2, 1);
// eslint-disable-next-line @typescript-eslint/no-require-imports
require("./api");
});
const subcommand = process.argv[2];
const isElectron = "electron" in process.versions;

program
.command("version")
.description("Show version information")
.action(() => {
console.log(`mux ${VERSION.git_describe} (${VERSION.git_commit})`);
});

// Default action: launch desktop app when no subcommand given
program.action(() => {
function launchDesktop(): void {
// eslint-disable-next-line @typescript-eslint/no-require-imports
require("../desktop/main");
});
}

program.parse();
// Route known subcommands to their dedicated entry points (each has its own Commander instance)
if (subcommand === "run") {
process.argv.splice(2, 1); // Remove "run" since run.ts defines .name("mux run")
// eslint-disable-next-line @typescript-eslint/no-require-imports
require("./run");
} else if (subcommand === "server") {
process.argv.splice(2, 1);
// eslint-disable-next-line @typescript-eslint/no-require-imports
require("./server");
} else if (subcommand === "api") {
process.argv.splice(2, 1);
// eslint-disable-next-line @typescript-eslint/no-require-imports
require("./api");
} else if (subcommand === "desktop" || (subcommand === undefined && isElectron)) {
// Explicit `mux desktop` or no args when running under Electron
launchDesktop();
} else {
// No subcommand (non-Electron), flags (--help, --version), or unknown commands
const program = new Command();
program
.name("mux")
.description("Mux - AI agent orchestration")
.version(`${VERSION.git_describe} (${VERSION.git_commit})`, "-v, --version");

// Register subcommand stubs for help display (actual implementations are above)
program.command("run").description("Run a one-off agent task");
program.command("server").description("Start the HTTP/WebSocket ORPC server");
program.command("api").description("Interact with the mux API via a running server");
program.command("desktop").description("Launch the desktop app (requires Electron)");

program.parse();
}
3 changes: 2 additions & 1 deletion src/cli/orpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { router } from "@/node/orpc/router";
import type { ORPCContext } from "@/node/orpc/context";
import { extractWsHeaders } from "@/node/orpc/authMiddleware";
import { VERSION } from "@/version";
import { log } from "@/node/services/log";

// --- Types ---

Expand Down Expand Up @@ -71,7 +72,7 @@ export async function createOrpcServer({
context,
serveStatic = false,
staticDir = path.join(__dirname, ".."),
onOrpcError = (error) => console.error("ORPC Error:", error),
onOrpcError = (error) => log.error("ORPC Error:", error),
}: OrpcServerOptions): Promise<OrpcServer> {
// Express app setup
const app = express();
Expand Down
Loading