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
119 changes: 119 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -125307,6 +125307,125 @@
}
}
},
"machines.agent.claude": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Claude Code"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "Claude Code"
}
}
}
},
"machines.agent.codex": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Codex"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "Codex"
}
}
}
},
"machines.agent.copyPrompt": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Copy Cloud Prompt"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "クラウドプロンプトをコピー"
}
}
}
},
"machines.agent.error.missingSkill": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "This build is missing the bundled cmux Cloud skill file."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "このビルドには cmux Cloud スキルファイルが含まれていません。"
}
}
}
},
"machines.agent.operation.starting": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Starting %@…"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "%@ を起動しています…"
}
}
}
},
"machines.agent.menuLabel": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Open Cloud Agent"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "クラウドエージェントを開く"
}
}
}
},
"machines.agent.opencode": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "OpenCode"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "OpenCode"
}
}
}
},
"machines.auth.checking": {
"extractionState": "manual",
"localizations": {
Expand Down
114 changes: 114 additions & 0 deletions Resources/cloud-agent-skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# cmux Cloud skill

You are helping the user work with cmux Cloud machines through the `cmux` CLI. This file is regenerated by the cmux app; do not edit it. `cmux vm <subcommand> --help` is authoritative when this file disagrees.

## Mental model

- A machine is a persistent cloud VM owned by the signed-in cmux user. Its generated name (like `brave-otter`) is its address everywhere; `cmux vm rename` sets a display label only. The machine outlives panes, closed laptops, and reconnects.
- Every machine runs a cmux session daemon (cmux-tui on current images, cmuxd-remote on older ones) that owns terminal sessions and scrollback. Clients attach through short-lived leases minted by the backend; the transport depends on what the provider and image support. SSH is a fallback some providers and images cannot mint, and its absence is not an error.
- New machines boot a desktop image (xfce + noVNC) plus a shell, with a persistent per-machine home. `--base` gives a shell-only machine.
- Base is a separate single per-user persistent slot, pinned to the top of the sidebar. `cmux vm new` mints fresh machines; `cmux vm base` always reopens the same one.
- Terminals on a machine live in its cmux-tui session (workspaces `ws_…`, terminals `term_…`). They keep running detached. `cmux vm tree` catalogs every surface, and every line is an address `cmux vm open` (machine targets) or `cmux surface open` (any entry, including This Mac) accepts: `brave-otter/main/term_2f9c…`, `brave-otter:desktop`, `brave-otter:port/3000`.
- Pool machines (labeled `agent-pool` in `cmux vm ls`) are provisioned by the `vm run`/`vm agent` router and reused for routed work. The router never drafts machines a person made by hand.
- Plans cap active machine count and memory. `cmux vm ls` prints the meter and, on free plans, when free cloud access expires.

## Commands

List and inspect:

```
cmux vm ls # NAME LABEL STATE PROVIDER IMAGE + plan meter
cmux vm status <id> # provider, status, image
cmux vm stats <id> # live CPU/memory
cmux vm ports <id> # listening TCP ports inside the machine
cmux vm tools <id> # probe common tools inside the machine
cmux vm tree [<machine>|local] [--refresh]
```

Create and name:

```
cmux vm new [--base] [--size <2g|4g|8g|16g|32g>] [--detach|-d]
cmux vm rename <id> <new-label> # label only; the id stays the address
```

`vm new` takes no positional arguments (rejected so a typo cannot provision a paid machine). A bare `vm new` creates a persistent machine with its own durable home, up to the plan limit. The backend picks the provider.

Base:

```
cmux vm base # open Base, reuses the same VM
cmux vm base reset [--reason <text>] # new generation; the old VM is retained
```

Attach and open:

```
cmux vm shell <id> # terminal workspace (WebSocket attach)
cmux vm tui <id> # the machine's full cmux-tui client in a pane
cmux vm desktop <id> # noVNC screen as a browser pane
cmux vm open <machine> # same as vm shell
cmux vm open <machine>/<ws>[/<term>] # a cmux-tui workspace or one terminal
cmux vm open <machine>:desktop
cmux vm open <machine> <port> [--print] # private tokened URL for an HTTP port
cmux vm ssh <id> # SSH fallback; unavailable on some providers/images
```

Run commands:

```
cmux vm exec <id> -- <command...> # one command, ~35s limit, exit code passes through
cmux vm run [--sync] [--pull <remote-path>] [--machine <id>] [--new] [--size <s>] [--timeout <seconds>] -- <command...>
cmux vm route [--cwd <dir>] # print which machine vm run/agent would pick, and why
cmux vm wait <id> [--timeout <seconds>] [--wake]
```

`vm run` needs no machine name: it reuses an idle pool machine, wakes a sleeping one, or provisions a fresh one (default timeout 600s, max 15 minutes). `--sync` pushes the current directory to `work/<basename>` first; `--pull` fetches a path back afterward. For longer work start a detached terminal via `vm agent`.

Coding agents on machines:

```
cmux vm agent --agent <claude|codex|opencode|pi> [--machine <id>] [--sync] [--cwd <dir>] [--name <name>] [--no-open] [--new] [--size <s>] -- <prompt or args...>
```

The agent starts as a detached terminal in the machine's cmux-tui session: it keeps running when the pane closes, and `cmux vm open <machine>/<ws>/<term>` reattaches from any device. A bare prompt runs the agent's one-shot form; leading flags or known subcommands pass through verbatim. Credentials for cloud agents come from `cmux ai-accounts upload`.

Files:

```
cmux vm push <id> <local-path> [remote-path] [--exclude <pattern>]...
cmux vm pull <id> <remote-path> [local-path]
```

Directories travel as tarballs with default excludes (node_modules, .git, and similar); transfers are size-capped, so ship repos without build artifacts.

Snapshot, fork, restore:

```
cmux vm snapshot <id> [--name <name>]
cmux vm fork <id> [--name <name>]
cmux vm restore <snapshot-id>
```

Destroy:

```
cmux vm rm <id> # irreversible and unprompted
```

## Inside a machine

The guest `cmux` binary is the machine's relay CLI: commands go to the connected cmux app on the user's Mac; they do not act inside the VM. The most useful verb for agents running on a machine:

```
cmux notify --title "Build done" --subtitle "myrepo" --body "Tests green"
```

## Rules

- Never run `cmux vm rm` on a machine you did not create in this session unless the user names it this turn. Use `cmux vm base reset` for a fresh Base; it retains the old VM.
- Prefer `vm run`/`vm agent` routing over creating machines; creation bills the user and counts against the plan. Check `cmux vm ls` before `vm new`.
- Use `cmux vm wait` instead of polling `vm status`.
- Use `--json` only on commands whose `--help` documents it; interactive verbs (`shell`, `tui`, `desktop`) have none. Do not parse the human table output either way.
- `vm exec` quoting is faithful per argv element; wrap shell constructs as `-- sh -c '<script>'`.
- Read plan limits and sizes from `cmux vm ls` and `--help`, not from memory.
34 changes: 34 additions & 0 deletions Sources/Cloud/CloudAgentSkillLauncher+CodingAgent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Foundation

extension CloudAgentSkillLauncher {
/// Agents the launcher can start locally. Raw values are the executable
/// names resolved through the user's login shell PATH, and are also the
/// `agent` parameter accepted by `vm.cloud_agent_open`.
enum CodingAgent: String, CaseIterable {
case claude
case codex
case opencode

var displayName: String {
switch self {
case .claude:
return String(localized: "machines.agent.claude", defaultValue: "Claude Code")
case .codex:
return String(localized: "machines.agent.codex", defaultValue: "Codex")
case .opencode:
return String(localized: "machines.agent.opencode", defaultValue: "OpenCode")
}
}

/// Interactive-session argv carrying the kickoff prompt. claude and
/// codex take a positional initial prompt; opencode uses `--prompt`.
/// Elements are argv words; the local provider shell-quotes them.
func argv(prompt: String) -> [String] {
switch self {
case .claude: return ["claude", prompt]
case .codex: return ["codex", prompt]
case .opencode: return ["opencode", "--prompt", prompt]
}
}
}
}
17 changes: 17 additions & 0 deletions Sources/Cloud/CloudAgentSkillLauncher+LauncherError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation

extension CloudAgentSkillLauncher {
enum LauncherError: LocalizedError {
case skillResourceMissing

var errorDescription: String? {
switch self {
case .skillResourceMissing:
return String(
localized: "machines.agent.error.missingSkill",
defaultValue: "This build is missing the bundled cmux Cloud skill file."
)
}
}
}
}
89 changes: 89 additions & 0 deletions Sources/Cloud/CloudAgentSkillLauncher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import AppKit
import Foundation

/// One shared path for "give me a coding agent that knows cmux Cloud".
///
/// The Machines panel menu and the `vm.cloud_agent_open` socket method both
/// launch a local terminal through `TerminalController.surfaceNewTerminal`
/// running the chosen agent with a kickoff prompt; "Copy Cloud Prompt" and
/// `vm.cloud_prompt` expose the same prompt for any other terminal. Every
/// entrypoint first installs the bundled skill file at a stable path under
/// `~/.config/cmux/skills/` so the prompt's file reference resolves for any
/// agent, with no network access.
enum CloudAgentSkillLauncher {
static let installedSkillRelativePath = ".config/cmux/skills/cmux-cloud.md"

/// The bundled skill markdown (`Resources/cloud-agent-skill.md`).
static func skillMarkdown(bundle: Bundle = .main) -> String? {
let url = bundle.url(forResource: "cloud-agent-skill", withExtension: "md")
?? bundle.resourceURL?.appendingPathComponent("cloud-agent-skill.md")
guard let url, let data = try? Data(contentsOf: url) else { return nil }
return String(decoding: data, as: UTF8.self)
}

/// Writes the bundled skill to the stable per-user path and returns it.
/// Regenerated on every use so the file always matches the running app;
/// the file's own header says it is managed and not to hand-edit it.
@discardableResult
static func installSkillFile(
fileManager: FileManager = .default,
homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser,
bundle: Bundle = .main
) throws -> URL {
guard let markdown = skillMarkdown(bundle: bundle) else {
throw LauncherError.skillResourceMissing
}
let url = homeDirectory.appendingPathComponent(installedSkillRelativePath)
try fileManager.createDirectory(
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true
)
try Data(markdown.utf8).write(to: url, options: .atomic)
return url
}

/// The kickoff prompt. Deliberately not localized: it is agent input, not
/// UI copy, and the skill file it points at is English.
static func kickoffPrompt(skillPath: String) -> String {
"""
Read \(skillPath) before doing anything else. It explains how to work \
with my cmux Cloud machines through the `cmux` CLI (`cmux vm ...`); when \
it disagrees with the CLI, `cmux vm <subcommand> --help` wins. Start by \
running `cmux vm ls`, summarize my machines in one line each, and ask \
what I want to do.
"""
}

/// Installs the skill file and returns the prompt plus the path it names.
static func promptPayload() throws -> (prompt: String, skillPath: String) {
let url = try installSkillFile()
return (kickoffPrompt(skillPath: url.path), url.path)
}

/// Installs the skill file and opens a local terminal pane running the
/// agent with the kickoff prompt, through the same shared path as
/// `surface.new_terminal` (the pane lands split in the selected
/// workspace). Returns the created surface payload.
@discardableResult
static func openAgent(_ agent: CodingAgent) async throws -> [String: Any] {
let payload = try promptPayload()
return try await TerminalController.surfaceNewTerminal(
machine: .local,
command: agent.argv(prompt: payload.prompt),
cwd: nil,
name: agent.displayName,
remoteWorkspaceID: nil,
destination: nil,
focus: true
)
}

/// Installs the skill file and puts the kickoff prompt on the clipboard.
@MainActor
static func copyPrompt() throws {
let payload = try promptPayload()
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(payload.prompt, forType: .string)
}
}
Loading