Skip to main content

MUXSH_JS

Constant MUXSH_JS 

Source
const MUXSH_JS: &str = "#!/usr/bin/env node\n// Copyright 2026, AgentMux Corp.\n// SPDX-License-Identifier: Apache-2.0\n//\n// muxsh \u{2014} AgentMux\'s own successor to Wave Terminal\'s `wsh` CLI (which\n// AgentMux inherited and fully retired 2026-04-12,\n// docs/specs/archive/SPEC_RETIRE_WSH_2026_04_12.md \u{2014} zero real usage of the\n// imported 20-subcommand surface). Not that CLI revived: a purpose-built\n// collection over MuxAPI, scoped per\n// docs/specs/SPEC_MUXSH_FULL_COLLECTION_2026_09_16.md \u{2014} every subcommand\n// below maps to a real, verified-unsigned (`$AGENTMUX_AUTH_KEY`-gated, not\n// agent-identity-signed) REST route; a few `wsh` commands that looked\n// buildable on paper (`pane close`, `secret list`) turned out to need either\n// new backend work or a capability a terminal structurally doesn\'t have \u{2014}\n// see that spec\'s \u{a7}5.5/\u{a7}5.6 for why they\'re not here.\n//\n// Grammar: `muxsh <noun> <verb> [args] [flags]`, with `open`/`web`/`view`/\n// `edit`/`run` as bare top-level shortcuts (the spec\'s \u{a7}2.1 \"iconic gesture\"\n// exception) rather than nested under a noun.\n//\n// Same architecture and auth as every muxsh-family core: thin Node wrapper,\n// $AGENTMUX_LOCAL_URL + $AGENTMUX_AUTH_KEY from the pane env\n// (lib/muxclient.mjs), no MCP, no new transport.\n//\n// Deployed to ~/.agentmux/shell/muxsh.mjs; shell `muxsh` functions delegate\n// here. From a tool-spawned subshell call the core directly:\n//   node ~/.agentmux/shell/muxsh.mjs open <file>\n//\n// Exit codes: 0 success, 1 usage/environment error, 2 the server rejected\n// the request or is unreachable.\n\nimport { resolve as resolvePath } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\n\nimport { agentmuxFetch, readAgentmuxEnv } from \"./lib/muxclient.mjs\";\n\nconst HELP = `muxsh \u{2014} a terminal CLI over MuxAPI (no GUI required)\n\nusage:\n  muxsh open <file>              open a file in an editor pane\n  muxsh web <url>                open a URL in a browser pane\n  muxsh view <path-or-url>       open the right pane type automatically\n                                  (URL -> browser, media file -> media, folder/ -> files, else -> editor)\n  muxsh edit <file>               like \'open\', but always forces the editor view\n    --title <t>                  pane/tab title\n    --split <right|left|down|up> split direction relative to the calling pane (default: right)\n    --collapse-tree               [open/edit/view-as-editor only] collapse the file-tree sidebar\n    --floating                    open in a floating window instead of a docked split\n    --no-focus                    open without focusing the new pane\n\n  muxsh pane list                 list tabs/panes in the calling pane\'s workspace\n    --json                       machine-readable output\n\n  muxsh run <cmd...>              run a background shell, print its shell_id\n  muxsh run --status <shell_id>   check whether a shell is still running\n  muxsh run --stop <shell_id>     stop a running shell\n\n  muxsh config edit                open settings.json in an editor pane\n  muxsh config path [data|config|logs|shared]\n                                  print the requested AgentMux directory (default: data)\n\n  muxsh agent list                 list agents reachable from this instance\n    --json                       machine-readable output\n  muxsh agent send <name> <message...>\n                                  send a message to a running agent\n\n  muxsh help                      this text\n\nRequires $AGENTMUX_LOCAL_URL and $AGENTMUX_AUTH_KEY (present in any\nAgentMux-opened pane). Every command operates on the instance this pane\nbelongs to; cross-instance operation is not implemented here.\n\nNot here: \'muxsh pane close\' and \'muxsh secret list\' both looked buildable\non paper but turned out to need either new backend work or an agent\nidentity a terminal doesn\'t have \u{2014} see\ndocs/specs/SPEC_MUXSH_FULL_COLLECTION_2026_09_16.md \u{a7}5.5/\u{a7}5.6.\n\'muxopen <agent>\' (a separate, already-shipped tool) launches an agent into\na pane \u{2014} not duplicated here as \'muxsh agent open\'.`;\n\nconst EDITOR_ONLY_FLAGS = [\"--collapse-tree\"];\nconst SPLIT_DIRECTIONS = [\"right\", \"left\", \"down\", \"up\"];\nconst MEDIA_EXTENSIONS = [\n    \".png\", \".jpg\", \".jpeg\", \".gif\", \".webp\", \".svg\", \".bmp\", \".ico\",\n    \".mp4\", \".webm\", \".mov\", \".avi\",\n    \".mp3\", \".wav\", \".ogg\", \".flac\",\n    \".pdf\",\n];\n// Not derived from a canonical AgentMux list (open question, SPEC_MUXSH_FULL_COLLECTION_2026_09_16.md \u{a7}7.4) \u{2014}\n// hand-rolled and may drift from whatever the frontend\'s own pane-type resolution considers \"media\".\nconst URL_PATTERN = /^\\w+:\\/\\//;\n\nconst CONFIG_PATH_ENV_VARS = {\n    data: \"AGENTMUX_DATA_DIR\",\n    config: \"AGENTMUX_CONFIG_DIR\",\n    logs: \"AGENTMUX_LOG_DIR\",\n    shared: \"AGENTMUX_SHARED_DIR\",\n};\n\n// \u{2500}\u{2500} Parsing \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\n\n/** Parse the shared open/web/view/edit flag set (--title/--split/--collapse-tree/--floating/--no-focus).\n *\n * `isEditor` gates `--collapse-tree` (`tree_expanded` is documented in\n * `rpc_types/block.rs` as \"editor only\"). Codex review (PR #3281, P2): an\n * earlier version of this gated on the *subcommand name* (\"open\" vs \"web\"),\n * which incorrectly accepted --collapse-tree for `muxsh view` on a media\n * target \u{2014} `view` resolves to `command: \"open\"` for BOTH editor and media,\n * but only editor should accept this flag. Gating on the actually-resolved\n * view (via this boolean) instead of the subcommand name fixes both `view`\n * and any future caller that reuses this parser.\n *\n * Pure. Returns `{title, split, collapseTree, floating, focus}` or `{error}`. */\nfunction parsePaneOpenFlags(argv, startIndex, isEditor) {\n    let title = null;\n    let split = null;\n    let collapseTree = false;\n    let floating = false;\n    let focus = true;\n\n    for (let i = startIndex; i < argv.length; i++) {\n        const arg = argv[i];\n        if (arg === \"--title\") {\n            title = argv[++i];\n            if (!title) return { error: \"--title requires a value\" };\n        } else if (arg === \"--split\") {\n            split = argv[++i];\n            if (!split) return { error: \"--split requires a value\" };\n            if (!SPLIT_DIRECTIONS.includes(split)) {\n                return { error: `--split must be one of right/left/down/up, got \'${split}\'` };\n            }\n        } else if (arg === \"--collapse-tree\") {\n            collapseTree = true;\n        } else if (arg === \"--floating\") {\n            floating = true;\n        } else if (arg === \"--no-focus\") {\n            focus = false;\n        } else {\n            return { error: `unknown argument \'${arg}\'` };\n        }\n\n        if (!isEditor && EDITOR_ONLY_FLAGS.includes(arg)) {\n            return { error: `\'${arg}\' is only valid for an editor pane (\'muxsh open\'/\'muxsh edit\', or \'muxsh view\' on a non-media, non-url target)` };\n        }\n    }\n    return { title, split, collapseTree, floating, focus };\n}\n\n/** Guess the pane.open `view` for `muxsh view <path-or-url>`. Pure.\n *\n * A folder is recognised only by its shape (a trailing separator, `.`, `..`\n * or `~`), since telling any other folder apart would take a stat. */\nexport function guessView(target) {\n    if (URL_PATTERN.test(target)) return \"browser\";\n    if (/[\\\\/]$/.test(target) || target === \".\" || target === \"..\" || target === \"~\") return \"files\";\n    const lower = target.toLowerCase();\n    if (MEDIA_EXTENSIONS.some((ext) => lower.endsWith(ext))) return \"media\";\n    return \"editor\";\n}\n\n/** Parse argv (already stripped of node + script path).\n *\n * Returns `{help}` | `{error}` | a command-specific shape discriminated by\n * `.command`. Pure \u{2014} no I/O, no process.exit.\n */\nexport function parseArgs(argv) {\n    if (argv.length === 0) return { help: true, exitCode: 1 };\n    if (argv[0] === \"help\" || argv[0] === \"--help\" || argv[0] === \"-h\") {\n        return { help: true, exitCode: 0 };\n    }\n\n    const head = argv[0];\n\n    if (head === \"open\" || head === \"web\" || head === \"edit\") {\n        const target = argv[1];\n        if (!target || target.startsWith(\"-\")) {\n            return { error: `\'muxsh ${head}\' requires a ${head === \"web\" ? \"url\" : \"file path\"} as its first argument` };\n        }\n        const isEditor = head === \"open\" || head === \"edit\";\n        const flags = parsePaneOpenFlags(argv, 2, isEditor);\n        if (flags.error) return flags;\n        // \'edit\' is \'open\' forcing the editor view, everywhere else identical.\n        const subcommand = head === \"edit\" ? \"open\" : head;\n        if (subcommand === \"open\") {\n            return { command: \"open\", subcommand, file: target, ...flags };\n        }\n        return { command: \"web\", subcommand, url: target, ...flags };\n    }\n\n    if (head === \"view\") {\n        const target = argv[1];\n        if (!target || target.startsWith(\"-\")) {\n            return { error: \"\'muxsh view\' requires a file path or url as its first argument\" };\n        }\n        const view = guessView(target);\n        const flags = parsePaneOpenFlags(argv, 2, view === \"editor\");\n        if (flags.error) return flags;\n        if (view === \"browser\") {\n            return { command: \"web\", subcommand: \"web\", url: target, ...flags };\n        }\n        if (view === \"files\") {\n            // srv resolves `~` itself, but a relative folder means nothing\n            // outside this shell\'s working directory, so it goes absolute here.\n            const folder = target.startsWith(\"~\") ? target : resolvePath(target);\n            return { command: \"open\", subcommand: \"open\", file: folder, view, ...flags };\n        }\n        // \"media\" and \"editor\" both use the pane.open editor-shaped request\n        // (view/file), matching what buildPaneOpenBody already emits for \"open\".\n        return { command: \"open\", subcommand: \"open\", file: target, view, ...flags };\n    }\n\n    if (head === \"pane\") {\n        if (argv[1] !== \"list\") {\n            return { error: `unknown \'muxsh pane\' verb \'${argv[1] ?? \"\"}\' (expected \'list\')` };\n        }\n        const json = argv.slice(2).includes(\"--json\");\n        const extra = argv.slice(2).filter((a) => a !== \"--json\");\n        if (extra.length > 0) return { error: `unknown argument \'${extra[0]}\'` };\n        return { command: \"pane-list\", json };\n    }\n\n    if (head === \"run\") {\n        if (argv[1] === \"--status\" || argv[1] === \"--stop\") {\n            const verb = argv[1] === \"--status\" ? \"run-status\" : \"run-stop\";\n            const shellId = argv[2];\n            if (!shellId) return { error: `\'muxsh run ${argv[1]}\' requires a shell id` };\n            if (argv.length > 3) return { error: `unknown argument \'${argv[3]}\'` };\n            return { command: verb, shellId };\n        }\n        const cmdParts = argv.slice(1);\n        if (cmdParts.length === 0) return { error: \"\'muxsh run\' requires a command\" };\n        return { command: \"run-create\", cmd: cmdParts.join(\" \") };\n    }\n\n    if (head === \"config\") {\n        if (argv[1] === \"edit\") {\n            if (argv.length > 2) return { error: `unknown argument \'${argv[2]}\'` };\n            return { command: \"config-edit\" };\n        }\n        if (argv[1] === \"path\") {\n            // Zero further args validly defaults to \"data\" \u{2014} checked against\n            // CONFIG_PATH_ENV_VARS, not treated as a missing-verb error.\n            const kind = argv[2] ?? \"data\";\n            if (argv.length > 3) return { error: `unknown argument \'${argv[3]}\'` };\n            if (!Object.hasOwn(CONFIG_PATH_ENV_VARS, kind)) {\n                return { error: `unknown \'muxsh config path\' kind \'${kind}\' (expected data/config/logs/shared)` };\n            }\n            return { command: \"config-path\", kind };\n        }\n        return { error: `unknown \'muxsh config\' verb \'${argv[1] ?? \"\"}\' (expected \'edit\' or \'path\')` };\n    }\n\n    if (head === \"agent\") {\n        if (argv[1] === \"list\") {\n            const json = argv.slice(2).includes(\"--json\");\n            const extra = argv.slice(2).filter((a) => a !== \"--json\");\n            if (extra.length > 0) return { error: `unknown argument \'${extra[0]}\'` };\n            return { command: \"agent-list\", json };\n        }\n        if (argv[1] === \"send\") {\n            const name = argv[2];\n            const messageParts = argv.slice(3);\n            if (!name) return { error: \"\'muxsh agent send\' requires an agent name\" };\n            if (messageParts.length === 0) return { error: \"\'muxsh agent send\' requires a message\" };\n            return { command: \"agent-send\", name, message: messageParts.join(\" \") };\n        }\n        return { error: `unknown \'muxsh agent\' verb \'${argv[1] ?? \"\"}\' (expected \'list\' or \'send\')` };\n    }\n\n    return { error: `unknown command \'${head}\'` };\n}\n\n// \u{2500}\u{2500} pane.open (open / web / view / edit) \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\n\n/** Build the /api/v1/pane/open request body for a parsed open/web/view command.\n *\n * `env` carries `AGENTMUX_BLOCKID`/`AGENTMUX_TABID` \u{2014} injected into every\n * terminal pane for exactly this purpose (`blockcontroller/shell/lifecycle.rs`).\n * Without a `split_reference_block_id`, the server\'s `resolve_placement`\n * (`server/app_api/pane.rs`) always falls back to plain \"insert\" regardless\n * of `split_direction` \u{2014} so `split_direction` is only meaningful, and is\n * therefore only sent, when a block id is actually known. Mirrors exactly\n * how the `OpenEditor` MCP tool derives this from `AGENTMUX_BLOCKID`\n * (`crates/mcp/src/main.rs`). Ignored when floating (`floating` panes are\n * documented as ignoring `split_direction`/`split_reference_block_id`\n * entirely \u{2014} `rpc_types/block.rs`), so they\'re omitted rather than sent and\n * silently discarded. Pure, for testability.\n */\nexport function buildRequestBody(parsed, env = {}) {\n    const body = { focus: parsed.focus, floating: parsed.floating };\n    if (parsed.title) body.title = parsed.title;\n\n    const blockId = env.AGENTMUX_BLOCKID;\n    const tabId = env.AGENTMUX_TABID;\n    if (tabId) body.tab_id = tabId;\n    if (blockId && !parsed.floating) {\n        body.split_direction = parsed.split ?? \"right\";\n        body.split_reference_block_id = blockId;\n    }\n\n    if (parsed.command === \"open\") {\n        body.view = parsed.view ?? \"editor\";\n        body.file = parsed.file;\n        if (parsed.collapseTree && body.view === \"editor\") body.tree_expanded = false;\n    } else {\n        body.view = \"browser\";\n        body.url = parsed.url;\n    }\n    return body;\n}\n\n/** Render the success line(s) for a /api/v1/pane/open response body.\n * Pure \u{2014} returns the text rather than printing, for testability. */\nexport function renderResult(body) {\n    return [`opened: ${body.view} pane`, `  block ${body.block_id}`, `  tab   ${body.tab_id}`].join(\"\\n\");\n}\n\n// \u{2500}\u{2500} pane list \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\n\n/** Render `GET /api/v1/tabs` response as a human-readable outline. Pure. */\n/** Render `GET /api/v1/tabs`\'s real response shape \u{2014} `{tabs: [{tab_id, name,\n * pane_count}]}` (`service/introspect.rs::agent_tabs`). Pure.\n *\n * Codex review (PR #3281, P2): an earlier version of this assumed a richer,\n * never-verified shape (`tab_name`/`active`/a nested `panes` array with\n * per-pane `view`/`block_id`/`title`) that the route has never produced \u{2014}\n * caught because the test that \"proved\" it used a hand-invented fixture\n * instead of the real response shape. There is no per-pane detail in what\n * this route actually returns; `pane_count` is the closest available\n * signal. */\nexport function renderTabs(tabs) {\n    if (!Array.isArray(tabs) || tabs.length === 0) return \"no tabs found\";\n    return tabs\n        .map((tab) => `tab ${tab.name || tab.tab_id}  (${tab.pane_count} pane${tab.pane_count === 1 ? \"\" : \"s\"})  ${tab.tab_id}`)\n        .join(\"\\n\");\n}\n\n// \u{2500}\u{2500} run \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\n\nexport function renderShellCreate(body) {\n    return `shell_id: ${body.shell_id}`;\n}\n\nexport function renderShellStatus(body) {\n    const lines = [`running: ${body.running}`];\n    if (body.exit_code !== undefined && body.exit_code !== null) lines.push(`exit_code: ${body.exit_code}`);\n    lines.push(`line_count: ${body.line_count}`);\n    return lines.join(\"\\n\");\n}\n\nexport function renderShellStop(body) {\n    return `stopped: ${body.stopped}`;\n}\n\n/** Build the /api/v1/shell/create request body. Pure \u{2014} separated out (rather\n * than inlined in main()) specifically so muxsh.contract.test.mjs can assert\n * its field names against docs/specs/app-api-manifest.json, same reason\n * buildRequestBody exists for pane.open. */\nexport function buildShellCreateBody(parsed, env = {}) {\n    return { agent_block_id: env.AGENTMUX_BLOCKID, cmd: parsed.cmd };\n}\n\nexport function buildShellStatusBody(parsed) {\n    return { shell_id: parsed.shellId };\n}\n\nexport function buildShellStopBody(parsed) {\n    return { shell_id: parsed.shellId };\n}\n\n// \u{2500}\u{2500} agent list / send \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\n\n/** Render `GET /agentmux/discovery`\'s host.agents into a human-readable list. Pure. */\n/** Render GET /agentmux/discovery\'s full response (`server/mod.rs::handle_discovery`).\n *\n * Codex review (PR #3281, P2): an earlier version only read `host.agents`,\n * omitting `host.cross_channel` (other AgentMux channels on this same host \u{2014}\n * a real, `agent send`-reachable tier) and `lan` (mDNS peer instances, each\n * carrying its own flat `agents: string[]`, per `LanInstance` in\n * `lan_discovery.rs` \u{2014} not a richer per-agent object, verified against\n * source rather than assumed this time) entirely, so `muxsh agent list`\n * silently under-reported what `muxsh agent send` can actually reach.\n */\nexport function renderAgentList(discovery) {\n    const lines = [];\n    for (const a of discovery?.host?.agents ?? []) {\n        lines.push(`${a.addressable ? \"*\" : \" \"} ${a.name}${a.addressable ? \"\" : \"  (not addressable)\"}`);\n    }\n    for (const a of discovery?.host?.cross_channel ?? []) {\n        lines.push(`* ${a.name}  (channel: ${a.channel})`);\n    }\n    for (const peer of discovery?.lan ?? []) {\n        for (const name of peer.agents ?? []) {\n            lines.push(`* ${name}  (lan: ${peer.hostname})`);\n        }\n    }\n    if (lines.length === 0) return \"no agents found\";\n    return lines.join(\"\\n\");\n}\n\nexport function renderAgentSend(body) {\n    return body.success ? \"sent\" : `not sent: ${body.error ?? \"unknown error\"}`;\n}\n\n/** Build the /agentmux/reactive/inject request body. Pure \u{2014} see\n * buildShellCreateBody\'s doc comment for why this is a separate, exported\n * function rather than inlined in main(). */\nexport function buildAgentSendBody(parsed) {\n    return { target_agent: parsed.name, message: parsed.message };\n}\n\n// \u{2500}\u{2500} main \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\n\nfunction fail(msg, code = 1) {\n    process.stderr.write(`muxsh: ${msg}\\n`);\n    process.exit(code);\n}\n\nasync function apiCall(url, authKey, path, opts, notFoundHint) {\n    let result;\n    try {\n        result = await agentmuxFetch(url, authKey, path, opts);\n    } catch (e) {\n        fail(`cannot reach ${url}: ${e.message ?? e}`, 2);\n    }\n    let body;\n    try {\n        body = await result.resp.json();\n    } catch {\n        body = {};\n    }\n    if (!result.ok) {\n        const hint = result.status === 404 && notFoundHint\n            ? ` (HTTP 404 \u{2014} this AgentMux instance may predate ${notFoundHint})`\n            : \"\";\n        fail(`${body.error ?? `HTTP ${result.status}`}${hint}`, 2);\n    }\n    return body;\n}\n\nexport async function main() {\n    const parsed = parseArgs(process.argv.slice(2));\n\n    if (parsed.help) {\n        console.log(HELP);\n        process.exit(parsed.exitCode);\n    }\n    if (parsed.error) fail(`${parsed.error}\\n\\n${HELP}`);\n\n    if (parsed.command === \"config-path\") {\n        const envVar = CONFIG_PATH_ENV_VARS[parsed.kind];\n        const value = process.env[envVar];\n        if (!value) fail(`${envVar} is not set in this pane\'s environment`);\n        console.log(value);\n        return;\n    }\n\n    const { url, authKey } = readAgentmuxEnv();\n    if (!url || !authKey) {\n        fail(\n            \"AGENTMUX_LOCAL_URL / AGENTMUX_AUTH_KEY not set \u{2014} run from a pane \" +\n            \"AgentMux opened (agent or shell), or export them from one.\",\n        );\n    }\n\n    switch (parsed.command) {\n        case \"open\":\n        case \"web\": {\n            const body = await apiCall(url, authKey, \"/api/v1/pane/open\", {\n                method: \"POST\",\n                body: buildRequestBody(parsed, process.env),\n            }, \"/api/v1/pane/open\");\n            console.log(renderResult(body));\n            return;\n        }\n        case \"config-edit\": {\n            const configDir = process.env.AGENTMUX_CONFIG_DIR;\n            if (!configDir) fail(\"AGENTMUX_CONFIG_DIR is not set in this pane\'s environment\");\n            const openParsed = { command: \"open\", subcommand: \"open\", file: `${configDir}/settings.json`, title: \"settings.json\", split: null, collapseTree: false, floating: false, focus: true };\n            const body = await apiCall(url, authKey, \"/api/v1/pane/open\", {\n                method: \"POST\",\n                body: buildRequestBody(openParsed, process.env),\n            }, \"/api/v1/pane/open\");\n            console.log(renderResult(body));\n            return;\n        }\n        case \"pane-list\": {\n            const blockId = process.env.AGENTMUX_BLOCKID ?? \"\";\n            const body = await apiCall(url, authKey, `/api/v1/tabs?block_id=${encodeURIComponent(blockId)}`, {}, \"/api/v1/tabs\");\n            if (parsed.json) {\n                console.log(JSON.stringify(body, null, 2));\n            } else {\n                console.log(renderTabs(body.tabs ?? body));\n            }\n            return;\n        }\n        case \"run-create\": {\n            if (!process.env.AGENTMUX_BLOCKID) fail(\"AGENTMUX_BLOCKID is not set in this pane\'s environment \u{2014} \'muxsh run\' needs it to attribute the shell\");\n            const body = await apiCall(url, authKey, \"/api/v1/shell/create\", {\n                method: \"POST\",\n                body: buildShellCreateBody(parsed, process.env),\n            }, \"/api/v1/shell/create\");\n            console.log(renderShellCreate(body));\n            return;\n        }\n        case \"run-status\": {\n            const body = await apiCall(url, authKey, \"/api/v1/shell/status\", {\n                method: \"POST\",\n                body: buildShellStatusBody(parsed),\n            }, \"/api/v1/shell/status\");\n            console.log(renderShellStatus(body));\n            return;\n        }\n        case \"run-stop\": {\n            const body = await apiCall(url, authKey, \"/api/v1/shell/stop\", {\n                method: \"POST\",\n                body: buildShellStopBody(parsed),\n            }, \"/api/v1/shell/stop\");\n            console.log(renderShellStop(body));\n            return;\n        }\n        case \"agent-list\": {\n            const body = await apiCall(url, authKey, \"/agentmux/discovery\", {}, \"/agentmux/discovery\");\n            if (parsed.json) {\n                console.log(JSON.stringify(body, null, 2));\n            } else {\n                console.log(renderAgentList(body));\n            }\n            return;\n        }\n        case \"agent-send\": {\n            const body = await apiCall(url, authKey, \"/agentmux/reactive/inject\", {\n                method: \"POST\",\n                body: buildAgentSendBody(parsed),\n            }, \"/agentmux/reactive/inject\");\n            // /agentmux/reactive/inject returns HTTP 200 even when delivery\n            // fails (success: false, e.g. \"agent not found\") \u{2014} apiCall\'s\n            // !result.ok check alone can\'t catch this, so it\'s checked here\n            // explicitly. Without this, a script relying on muxsh\'s own\n            // exit-code contract (Codex, PR #3281 P1) would see exit 0 for\n            // an undelivered message.\n            if (!body.success) fail(renderAgentSend(body), 2);\n            console.log(renderAgentSend(body));\n            return;\n        }\n        default:\n            fail(`internal error: unhandled command \'${parsed.command}\'`);\n    }\n}\n\nif (import.meta.url === pathToFileURL(process.argv[1] ?? \"\").href) {\n    main().catch((e) => fail(e.message ?? String(e), 2));\n}\n";
Expand description

muxsh — open editor/browser panes from a terminal (REPORT_WSH_STYLE_CLI_FOR_AGENT_APP_API_2026_09_16.md, SPEC_MUXSH_CLI_2026_09_16.md). Deployed beside its three siblings; shell muxsh functions delegate here.