@brutalsystems/muster 0.4.0 → 0.5.0
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/README.md +13 -5
- package/dist/format.js +1 -1
- package/dist/guard.js +1 -0
- package/dist/hosts/terminal.js +53 -8
- package/dist/hosts/tmux.js +4 -4
- package/dist/muster.js +11 -4
- package/dist/run.js +9 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,16 +76,24 @@ Add `--open` to display the launched session in a new Terminal.app window on mac
|
|
|
76
76
|
|
|
77
77
|
```sh
|
|
78
78
|
muster run codex --open --prompt "Review this project" --format human
|
|
79
|
-
muster run claude --open --prompt "Review this project" --format human
|
|
79
|
+
muster run claude --open --terminal ghostty --prompt "Review this project" --format human
|
|
80
|
+
muster run codex --open --terminal iterm2 --prompt "Review this project" --format human
|
|
80
81
|
```
|
|
81
82
|
|
|
82
83
|
MCP `run` accepts `open: true`. This requires tmux and selects it when the host
|
|
83
84
|
is auto; explicit pty hosts and tasks are rejected. Detached launch remains the
|
|
84
|
-
default.
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
default. `--terminal auto|terminal|iterm2|ghostty` requires `--open`; omitted or
|
|
86
|
+
`auto` uses Terminal.app. MCP accepts the same `terminal` values. Explicit
|
|
87
|
+
choices never silently fall back. iTerm2 and Ghostty must be installed as
|
|
88
|
+
`iTerm.app` or `Ghostty.app` in `/Applications` or `~/Applications`.
|
|
89
|
+
The host remains tmux. Successful results include `terminal_opened: true` and
|
|
90
|
+
the resolved `terminal` (a launch event, not a live window-status check).
|
|
87
91
|
Closing the Terminal window leaves the session running; use `muster stop <id>`
|
|
88
|
-
to stop it.
|
|
92
|
+
to stop it. Terminal.app and iTerm2 may require macOS Automation permission.
|
|
93
|
+
Ghostty opens a separate app instance that exits when its last window closes;
|
|
94
|
+
it uses the [Ghostty macOS launch interface](https://github.com/ghostty-org/ghostty/discussions/9221).
|
|
95
|
+
iTerm2 uses its [documented scripting interface](https://iterm2.com/documentation-scripting.html).
|
|
96
|
+
If the launch request to the viewer fails,
|
|
89
97
|
Muster stops the launched session and returns an error.
|
|
90
98
|
|
|
91
99
|
Auto-selection tries tmux, then pty. Every peer includes `host`, `capabilities`
|
package/dist/format.js
CHANGED
|
@@ -43,7 +43,7 @@ function recordHuman(record, color) {
|
|
|
43
43
|
if (record.host)
|
|
44
44
|
lines.push(field("Host", `${clean(record.host)}${record.host === "pty" ? " (not watchable or attachable)" : ""}`));
|
|
45
45
|
if (record.terminal_opened)
|
|
46
|
-
lines.push(field("Terminal", "Terminal.app (opened at launch)
|
|
46
|
+
lines.push(field("Terminal", `${record.terminal === "ghostty" ? "Ghostty" : record.terminal === "iterm2" ? "iTerm2" : "Terminal.app"} (opened at launch)`));
|
|
47
47
|
if (record.exit_code !== undefined)
|
|
48
48
|
lines.push(field("Exit code", badExit ? paint(clean(record.exit_code), 31) : clean(record.exit_code)));
|
|
49
49
|
if (record.signal)
|
package/dist/guard.js
CHANGED
|
@@ -14,6 +14,7 @@ export const runSchema = z
|
|
|
14
14
|
kind: z.enum(["session", "task"]).default("session"),
|
|
15
15
|
host: z.enum(["auto", "tmux", "pty", "macos-terminal"]).optional(),
|
|
16
16
|
open: z.boolean().optional(),
|
|
17
|
+
terminal: z.enum(["auto", "terminal", "iterm2", "ghostty"]).optional(),
|
|
17
18
|
permissions: z.enum(["auto", "deny", "bypass"]).optional(),
|
|
18
19
|
sandbox: z.enum(["read-only", "workspace-write", "full-access"]).optional(),
|
|
19
20
|
args: z.array(z.string()).default([]),
|
package/dist/hosts/terminal.js
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
import { access, realpath } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { constants } from "node:fs";
|
|
3
4
|
import { delimiter, join } from "node:path";
|
|
4
5
|
import { command } from "../identity/processes.js";
|
|
5
|
-
export async function terminalAvailable(platform = process.platform) {
|
|
6
|
+
export async function terminalAvailable(platform = process.platform, terminal = "terminal") {
|
|
6
7
|
if (platform !== "darwin")
|
|
7
8
|
return false;
|
|
8
9
|
try {
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
if (terminal === "terminal") {
|
|
11
|
+
await access("/System/Applications/Utilities/Terminal.app");
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
const name = terminal === "ghostty" ? "Ghostty.app" : "iTerm.app";
|
|
15
|
+
const found = await Promise.all(["/Applications", join(homedir(), "Applications")].map(async (dir) => {
|
|
16
|
+
try {
|
|
17
|
+
await access(join(dir, name));
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}));
|
|
24
|
+
if (!found.some(Boolean))
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
await access(terminal === "ghostty" ? "/usr/bin/open" : "/usr/bin/osascript", constants.X_OK);
|
|
11
28
|
return true;
|
|
12
29
|
}
|
|
13
30
|
catch {
|
|
@@ -25,23 +42,51 @@ export async function tmuxExecutable() {
|
|
|
25
42
|
}
|
|
26
43
|
catch { }
|
|
27
44
|
}
|
|
28
|
-
throw new Error("Cannot locate tmux executable for
|
|
45
|
+
throw new Error("Cannot locate tmux executable for terminal viewer");
|
|
29
46
|
}
|
|
30
|
-
export async function openTerminal(tmux, server, hostRef, deadline) {
|
|
47
|
+
export async function openTerminal(tmux, server, hostRef, deadline, terminal = "terminal") {
|
|
31
48
|
if (!/^[a-zA-Z0-9_-]+$/.test(server) || !/^@\d+$/.test(hostRef))
|
|
32
49
|
throw new Error("Invalid tmux terminal target");
|
|
33
50
|
if (Date.now() >= deadline)
|
|
34
|
-
throw new Error("Launch deadline expired before opening
|
|
51
|
+
throw new Error("Launch deadline expired before opening terminal");
|
|
52
|
+
const timeout = Math.max(1, Math.min(10000, deadline - Date.now()));
|
|
53
|
+
if (terminal === "ghostty") {
|
|
54
|
+
await command("/usr/bin/open", [
|
|
55
|
+
"-na",
|
|
56
|
+
"Ghostty",
|
|
57
|
+
"--args",
|
|
58
|
+
"--quit-after-last-window-closed=true",
|
|
59
|
+
"-e",
|
|
60
|
+
tmux,
|
|
61
|
+
"-L",
|
|
62
|
+
server,
|
|
63
|
+
"attach-session",
|
|
64
|
+
"-t",
|
|
65
|
+
"muster",
|
|
66
|
+
";",
|
|
67
|
+
"select-window",
|
|
68
|
+
"-t",
|
|
69
|
+
hostRef,
|
|
70
|
+
], timeout);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
35
73
|
const quoted = "'" + tmux.replaceAll("'", "'\\''") + "'";
|
|
36
74
|
const shellCommand = `${quoted} -L ${server} attach-session -t muster \\; select-window -t ${hostRef}`;
|
|
37
75
|
await command("/usr/bin/osascript", [
|
|
38
76
|
"-e",
|
|
39
|
-
|
|
77
|
+
terminal === "iterm2"
|
|
78
|
+
? `on run argv
|
|
79
|
+
tell application "iTerm"
|
|
80
|
+
create window with default profile command (item 1 of argv)
|
|
81
|
+
activate
|
|
82
|
+
end tell
|
|
83
|
+
end run`
|
|
84
|
+
: `on run argv
|
|
40
85
|
tell application "Terminal"
|
|
41
86
|
activate
|
|
42
87
|
do script (item 1 of argv)
|
|
43
88
|
end tell
|
|
44
89
|
end run`,
|
|
45
90
|
shellCommand,
|
|
46
|
-
],
|
|
91
|
+
], timeout);
|
|
47
92
|
}
|
package/dist/hosts/tmux.js
CHANGED
|
@@ -24,11 +24,11 @@ export class TmuxHost {
|
|
|
24
24
|
return false;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
openAvailable() {
|
|
28
|
-
return terminalAvailable();
|
|
27
|
+
openAvailable(terminal = "terminal") {
|
|
28
|
+
return terminalAvailable(process.platform, terminal);
|
|
29
29
|
}
|
|
30
|
-
async open(hostRef, deadline) {
|
|
31
|
-
await openTerminal(await tmuxExecutable(), this.server, hostRef, deadline);
|
|
30
|
+
async open(hostRef, deadline, terminal = "terminal") {
|
|
31
|
+
await openTerminal(await tmuxExecutable(), this.server, hostRef, deadline, terminal);
|
|
32
32
|
}
|
|
33
33
|
capabilities() {
|
|
34
34
|
return { watchable: true, attachable: true, survivesParentExit: true };
|
package/dist/muster.js
CHANGED
|
@@ -13,7 +13,7 @@ const listSchema = z
|
|
|
13
13
|
.strict();
|
|
14
14
|
const idSchema = z.object({ id: z.string().min(1) }).strict();
|
|
15
15
|
const help = `Usage:
|
|
16
|
-
muster run <codex|claude> --prompt TEXT [--cwd DIR] [--kind session|task] [--host ID] [--open] [-- RUNTIME_OPTIONS]
|
|
16
|
+
muster run <codex|claude> --prompt TEXT [--cwd DIR] [--kind session|task] [--host ID] [--open] [--terminal APP] [-- RUNTIME_OPTIONS]
|
|
17
17
|
muster list [--kind session|task] [--format json|human]
|
|
18
18
|
muster stop <id> [--format json|human]
|
|
19
19
|
muster output <id>
|
|
@@ -21,7 +21,8 @@ const help = `Usage:
|
|
|
21
21
|
|
|
22
22
|
run also accepts --format json|human, --permissions auto|deny|bypass,
|
|
23
23
|
and --sandbox read-only|workspace-write|full-access before -- RUNTIME_OPTIONS.
|
|
24
|
-
--open displays a tmux session
|
|
24
|
+
--open displays a tmux session on macOS. --terminal accepts auto (Terminal.app),
|
|
25
|
+
terminal, iterm2, or ghostty, and requires --open.
|
|
25
26
|
run, list and stop default to JSON. output prints raw task text.
|
|
26
27
|
|
|
27
28
|
pty launches print their record and keep this process running. Ctrl-C stops the
|
|
@@ -53,7 +54,7 @@ async function main() {
|
|
|
53
54
|
});
|
|
54
55
|
});
|
|
55
56
|
if (mcp) {
|
|
56
|
-
const server = new Server({ name: "muster", version: "0.
|
|
57
|
+
const server = new Server({ name: "muster", version: "0.5.0" }, { capabilities: { tools: {} } });
|
|
57
58
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
58
59
|
tools: [
|
|
59
60
|
{
|
|
@@ -76,7 +77,12 @@ async function main() {
|
|
|
76
77
|
},
|
|
77
78
|
open: {
|
|
78
79
|
type: "boolean",
|
|
79
|
-
description: "Open the tmux session in
|
|
80
|
+
description: "Open the tmux session in a terminal viewer (macOS only).",
|
|
81
|
+
},
|
|
82
|
+
terminal: {
|
|
83
|
+
type: "string",
|
|
84
|
+
enum: ["auto", "terminal", "iterm2", "ghostty"],
|
|
85
|
+
description: "Viewer app; requires open. Auto uses Terminal.app.",
|
|
80
86
|
},
|
|
81
87
|
permissions: {
|
|
82
88
|
type: "string",
|
|
@@ -170,6 +176,7 @@ async function main() {
|
|
|
170
176
|
kind: { type: "string" },
|
|
171
177
|
host: { type: "string" },
|
|
172
178
|
open: { type: "boolean" },
|
|
179
|
+
terminal: { type: "string" },
|
|
173
180
|
format: { type: "string" },
|
|
174
181
|
permissions: { type: "string" },
|
|
175
182
|
sandbox: { type: "string" },
|
package/dist/run.js
CHANGED
|
@@ -52,6 +52,9 @@ export class Muster {
|
|
|
52
52
|
throw new Error("cwd is not a directory");
|
|
53
53
|
if (req.kind === "task" && req.host)
|
|
54
54
|
throw new Error("host applies only to session launches");
|
|
55
|
+
if (req.terminal && !req.open)
|
|
56
|
+
throw new Error("--terminal requires --open");
|
|
57
|
+
const terminal = !req.terminal || req.terminal === "auto" ? "terminal" : req.terminal;
|
|
55
58
|
const requestedHost = req.host ?? this.config.host;
|
|
56
59
|
if (req.open &&
|
|
57
60
|
(req.kind !== "session" || !["auto", "tmux"].includes(requestedHost)))
|
|
@@ -60,8 +63,9 @@ export class Muster {
|
|
|
60
63
|
const argv = launchArgs(req, this.config), driver = req.kind === "session"
|
|
61
64
|
? await selectHost(req.open ? "tmux" : requestedHost, this.drivers)
|
|
62
65
|
: undefined;
|
|
63
|
-
if (req.open &&
|
|
64
|
-
|
|
66
|
+
if (req.open &&
|
|
67
|
+
(!driver?.open || !(await driver.openAvailable?.(terminal))))
|
|
68
|
+
throw new Error(`--open requires the selected terminal (${terminal}) installed on macOS and tmux`);
|
|
65
69
|
let policy = [];
|
|
66
70
|
if (req.runtime === "claude")
|
|
67
71
|
await assertClaudePolicy(this.env);
|
|
@@ -81,7 +85,7 @@ export class Muster {
|
|
|
81
85
|
prompt: req.prompt,
|
|
82
86
|
host: driver?.id ?? null,
|
|
83
87
|
requester,
|
|
84
|
-
...(req.open ? { open: true } : {}),
|
|
88
|
+
...(req.open ? { open: true, terminal } : {}),
|
|
85
89
|
args: req.args,
|
|
86
90
|
...permissions,
|
|
87
91
|
});
|
|
@@ -186,8 +190,9 @@ export class Muster {
|
|
|
186
190
|
};
|
|
187
191
|
if (req.open) {
|
|
188
192
|
openingTerminal = true;
|
|
189
|
-
await driver.open(launched.hostRef, deadline);
|
|
193
|
+
await driver.open(launched.hostRef, deadline, terminal);
|
|
190
194
|
peer.terminal_opened = true;
|
|
195
|
+
peer.terminal = terminal;
|
|
191
196
|
}
|
|
192
197
|
entry = await this.registry.update(entry.launchId, {
|
|
193
198
|
id: identity.id,
|