@brutalsystems/muster 0.7.16 → 0.7.18
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 +10 -2
- package/dist/format.js +26 -0
- package/dist/help.js +195 -0
- package/dist/muster.js +56 -58
- package/dist/prompt.js +50 -0
- package/dist/run.js +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -882,6 +882,10 @@ any non-empty plugin list, so the child also loads whatever plugins the target
|
|
|
882
882
|
repository ships. It globs both spellings — `.opencode/plugin` and
|
|
883
883
|
`.opencode/plugins` — so auditing only one of them misses half of what a
|
|
884
884
|
repository can load. Select plugins only for repositories trusted with that.
|
|
885
|
+
`--plugin NAME` is repeatable and replaces whatever plugins the configuration
|
|
886
|
+
selects by default, rather than adding to them; `--no-plugin` selects none and
|
|
887
|
+
is how a launch gets the `--pure` treatment above when the configuration would
|
|
888
|
+
otherwise have chosen some. Both apply to OpenCode only.
|
|
885
889
|
Claude connection specifications are stored with owner-only permissions under
|
|
886
890
|
`~/.muster/mcp`, removed on stop, task completion, or when `list` observes an ended
|
|
887
891
|
session. MCP server access is separate from filesystem sandbox permissions: a
|
|
@@ -892,8 +896,12 @@ selected external tool may have its own write or network capabilities.
|
|
|
892
896
|
Installed deliberately, in the one session that should hold spawn authority —
|
|
893
897
|
never at user scope.
|
|
894
898
|
|
|
895
|
-
Muster exposes `run`, `list`, `stop`, and `output` over stdio.
|
|
896
|
-
|
|
899
|
+
Muster exposes `run`, `list`, `stop`, and `output` over stdio. `mcp` starts the
|
|
900
|
+
server, and so does no arguments when stdin is not a terminal — which is what an
|
|
901
|
+
MCP client provides. Typing `muster` at a terminal prints the usage text and a
|
|
902
|
+
short status instead, because a server waiting for JSON-RPC on stdin is
|
|
903
|
+
indistinguishable from a hang. `muster mcp` starts the server either way, so it
|
|
904
|
+
can still be driven by hand to debug it. Diagnostics go to stderr, never protocol stdout.
|
|
897
905
|
Schemas match the CLI (`args` is the array of optional runtime arguments).
|
|
898
906
|
|
|
899
907
|
For a single Codex session, use per-invocation configuration:
|
package/dist/format.js
CHANGED
|
@@ -100,3 +100,29 @@ export function formatHuman(value, color = false) {
|
|
|
100
100
|
: "No Muster runs.\n";
|
|
101
101
|
return recordHuman(value, color) + "\n";
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* What a human sees when they type `muster` with no arguments at a terminal.
|
|
105
|
+
*
|
|
106
|
+
* With no arguments Muster starts the MCP server, which from a terminal looks
|
|
107
|
+
* exactly like a hang — the process is waiting for JSON-RPC on stdin and has
|
|
108
|
+
* nothing to say. The server path is reached by a pipe, never a TTY, so a
|
|
109
|
+
* terminal can be answered without changing what any MCP client gets.
|
|
110
|
+
*
|
|
111
|
+
* The session count is what the registry recorded, not what is running: it is
|
|
112
|
+
* read without the refresh `list` performs, because that refresh also reaps a
|
|
113
|
+
* terminal entry's identity copy and reaches an OpenCode session over HTTP.
|
|
114
|
+
* Neither belongs in the output of typing a bare command, so the wording says
|
|
115
|
+
* "recorded" and points at `list` for the authoritative answer.
|
|
116
|
+
*/
|
|
117
|
+
export function statusText(identities, sessions) {
|
|
118
|
+
const lines = [];
|
|
119
|
+
if (!identities.length)
|
|
120
|
+
lines.push("No identities yet — add one with:", " muster setup-identity --identity NAME --agent <codex|claude|opencode> --interactive");
|
|
121
|
+
else
|
|
122
|
+
lines.push(`${identities.length} ${identities.length === 1 ? "identity" : "identities"} — ` +
|
|
123
|
+
identities.map((i) => `${i.name} ${i.auth.state}`).join(", "));
|
|
124
|
+
lines.push(sessions === 0
|
|
125
|
+
? "No recorded sessions."
|
|
126
|
+
: `${sessions} recorded session${sessions === 1 ? "" : "s"} (muster list refreshes)`, "", "Starting the MCP server? Use `muster mcp`.");
|
|
127
|
+
return lines.join("\n") + "\n";
|
|
128
|
+
}
|
package/dist/help.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
const WIDTH = 80;
|
|
2
|
+
const INDENT = " ";
|
|
3
|
+
const GUTTER = 2;
|
|
4
|
+
function wrap(text, width) {
|
|
5
|
+
const words = text.split(/\s+/).filter(Boolean);
|
|
6
|
+
const lines = [];
|
|
7
|
+
let line = "";
|
|
8
|
+
for (const word of words) {
|
|
9
|
+
if (line && line.length + 1 + word.length > width) {
|
|
10
|
+
lines.push(line);
|
|
11
|
+
line = word;
|
|
12
|
+
}
|
|
13
|
+
else
|
|
14
|
+
line = line ? `${line} ${word}` : word;
|
|
15
|
+
}
|
|
16
|
+
if (line)
|
|
17
|
+
lines.push(line);
|
|
18
|
+
return lines.length ? lines : [""];
|
|
19
|
+
}
|
|
20
|
+
/** Two columns, with a label too wide for its column taking a line of its own
|
|
21
|
+
* rather than pushing every description of that row out past the margin. */
|
|
22
|
+
function table(rows) {
|
|
23
|
+
const width = Math.min(26, Math.max(...rows.map(([label]) => label.length), 0));
|
|
24
|
+
const lead = INDENT.length + width + GUTTER;
|
|
25
|
+
return rows.flatMap(([label, description]) => {
|
|
26
|
+
const body = wrap(description, WIDTH - lead);
|
|
27
|
+
if (label.length > width)
|
|
28
|
+
return [INDENT + label, ...body.map((l) => " ".repeat(lead) + l)];
|
|
29
|
+
return body.map((line, i) => i === 0
|
|
30
|
+
? INDENT + label.padEnd(width) + " ".repeat(GUTTER) + line
|
|
31
|
+
: " ".repeat(lead) + line);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export const COMMANDS = [
|
|
35
|
+
{
|
|
36
|
+
name: "run",
|
|
37
|
+
summary: "launch an agent session or task",
|
|
38
|
+
synopsis: "muster run <codex|claude|opencode> --prompt TEXT [options]",
|
|
39
|
+
options: [
|
|
40
|
+
["--prompt TEXT", "the instruction to launch with"],
|
|
41
|
+
["--cwd DIR", "the directory to launch in"],
|
|
42
|
+
["--project NAME", "a directory named under [projects]; not with --cwd"],
|
|
43
|
+
["--kind KIND", "session (default) or task"],
|
|
44
|
+
["--host ID", "tmux, pty, or auto (default: tmux, then pty)"],
|
|
45
|
+
["--identity NAME", "run as a configured identity"],
|
|
46
|
+
["--open", "display the tmux session in a terminal (macOS)"],
|
|
47
|
+
["--terminal APP", "auto, terminal, iterm2, or ghostty; needs --open"],
|
|
48
|
+
[
|
|
49
|
+
"--level LEVEL",
|
|
50
|
+
"read, work, or open: a legal permissions and sandbox pair",
|
|
51
|
+
],
|
|
52
|
+
["--permissions MODE", "auto, deny, or bypass"],
|
|
53
|
+
["--sandbox MODE", "read-only, workspace-write, or full-access"],
|
|
54
|
+
["--mcp NAME", "repeatable; replaces configured defaults"],
|
|
55
|
+
["--no-mcp", "launch with no MCP servers"],
|
|
56
|
+
["--plugin NAME", "repeatable; replaces configured defaults (OpenCode)"],
|
|
57
|
+
["--no-plugin", "launch with no plugins"],
|
|
58
|
+
["--model PROVIDER/MODEL", "select the OpenCode model"],
|
|
59
|
+
[
|
|
60
|
+
"--request-key KEY",
|
|
61
|
+
"name the request; sending it again returns the same launch",
|
|
62
|
+
],
|
|
63
|
+
["--format FORMAT", "json (default) or human"],
|
|
64
|
+
["-- RUNTIME_OPTIONS", "everything after -- is passed to the runtime"],
|
|
65
|
+
],
|
|
66
|
+
notes: [
|
|
67
|
+
"Naming both --level and --permissions or --sandbox is an error rather than a precedence rule.",
|
|
68
|
+
"A pty launch keeps this process running and Ctrl-C stops it; a tmux launch survives this process exiting.",
|
|
69
|
+
],
|
|
70
|
+
examples: [
|
|
71
|
+
"muster run claude --prompt 'Review this project' --cwd .",
|
|
72
|
+
"muster run codex --identity work --level work --prompt 'Ship it'",
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "list",
|
|
77
|
+
summary: "show sessions and tasks",
|
|
78
|
+
synopsis: "muster list [--kind session|task] [--format json|human]",
|
|
79
|
+
options: [
|
|
80
|
+
["--kind KIND", "show only session or only task entries"],
|
|
81
|
+
["--format FORMAT", "json (default) or human"],
|
|
82
|
+
],
|
|
83
|
+
notes: [
|
|
84
|
+
"Refreshes live metadata, and removes the per-launch identity copy of any entry that has ended.",
|
|
85
|
+
],
|
|
86
|
+
examples: ["muster list --format human"],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "stop",
|
|
90
|
+
summary: "stop a session or task",
|
|
91
|
+
synopsis: "muster stop <id> [--format json|human]",
|
|
92
|
+
options: [["--format FORMAT", "json (default) or human"]],
|
|
93
|
+
examples: ["muster stop 6fae755b-8f5d-4425-8ed7-69e84bfac8c5"],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "output",
|
|
97
|
+
summary: "print a task's captured output",
|
|
98
|
+
synopsis: "muster output <id>",
|
|
99
|
+
options: [],
|
|
100
|
+
notes: ["Prints raw text; --format does not apply."],
|
|
101
|
+
examples: ["muster output 6fae755b-8f5d-4425-8ed7-69e84bfac8c5"],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "identities",
|
|
105
|
+
summary: "show configured identities",
|
|
106
|
+
synopsis: "muster identities [--format json|human]",
|
|
107
|
+
options: [["--format FORMAT", "json (default) or human"]],
|
|
108
|
+
examples: ["muster identities --format human"],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "setup-identity",
|
|
112
|
+
summary: "create or configure an identity",
|
|
113
|
+
synopsis: "muster setup-identity --identity NAME --agent AGENT [options]",
|
|
114
|
+
options: [
|
|
115
|
+
["--identity NAME", "the identity to create or configure"],
|
|
116
|
+
["--agent AGENT", "codex, claude, or opencode"],
|
|
117
|
+
["--interactive", "run the login here and store what it returns"],
|
|
118
|
+
[
|
|
119
|
+
"--token-env VAR",
|
|
120
|
+
"read the token from that variable instead (Claude only)",
|
|
121
|
+
],
|
|
122
|
+
],
|
|
123
|
+
notes: [
|
|
124
|
+
"--interactive and --token-env are the two credential routes and cannot be combined; an identity holds one at a time.",
|
|
125
|
+
"Without --interactive the command prints the login for you to run yourself.",
|
|
126
|
+
],
|
|
127
|
+
examples: [
|
|
128
|
+
"muster setup-identity --identity work --agent claude --interactive",
|
|
129
|
+
"muster setup-identity --identity ci --agent claude --token-env CLAUDE_TOKEN",
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: "doctor",
|
|
134
|
+
summary: "check path-configured plugins for staleness",
|
|
135
|
+
synopsis: "muster doctor [--format json|human]",
|
|
136
|
+
options: [["--format FORMAT", "json (default) or human"]],
|
|
137
|
+
notes: [
|
|
138
|
+
"Compares each copy's mtime against the package it came from, and exits 1 when any is stale. Plugins named by npm are not checked.",
|
|
139
|
+
],
|
|
140
|
+
examples: ["muster doctor --format human"],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "mcp",
|
|
144
|
+
summary: "start the MCP server on stdio",
|
|
145
|
+
synopsis: "muster mcp",
|
|
146
|
+
options: [],
|
|
147
|
+
notes: [
|
|
148
|
+
"Exposes run, list, stop and output. No arguments does the same when stdin is not a terminal, which is what an MCP client provides.",
|
|
149
|
+
],
|
|
150
|
+
examples: ["muster mcp"],
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
export function topLevelHelp() {
|
|
154
|
+
const lines = [
|
|
155
|
+
"Usage: muster <command> [options]",
|
|
156
|
+
"",
|
|
157
|
+
"Commands:",
|
|
158
|
+
...table(COMMANDS.map((c) => [c.name, c.summary])),
|
|
159
|
+
"",
|
|
160
|
+
"Options:",
|
|
161
|
+
...table([
|
|
162
|
+
["--format FORMAT", "json (default) or human, where a command takes it"],
|
|
163
|
+
["--version", "print the version"],
|
|
164
|
+
["--help", "print this, or a command's own help"],
|
|
165
|
+
]),
|
|
166
|
+
"",
|
|
167
|
+
"See `muster <command> --help` for a command's options.",
|
|
168
|
+
"",
|
|
169
|
+
// Kept here rather than moved to the README: this is a safety statement
|
|
170
|
+
// about who gets launch authority, not reference detail, and it is only
|
|
171
|
+
// load-bearing where someone is about to install.
|
|
172
|
+
...wrap("MCP: install deliberately in the one authorized session, never at user scope.", WIDTH),
|
|
173
|
+
];
|
|
174
|
+
return lines.join("\n") + "\n";
|
|
175
|
+
}
|
|
176
|
+
export function commandHelp(name) {
|
|
177
|
+
const command = COMMANDS.find((c) => c.name === name);
|
|
178
|
+
if (!command)
|
|
179
|
+
return undefined;
|
|
180
|
+
const lines = [
|
|
181
|
+
`Usage: ${command.synopsis}`,
|
|
182
|
+
"",
|
|
183
|
+
...wrap(command.summary, WIDTH),
|
|
184
|
+
];
|
|
185
|
+
if (command.options.length)
|
|
186
|
+
lines.push("", "Options:", ...table(command.options));
|
|
187
|
+
for (const note of command.notes ?? [])
|
|
188
|
+
lines.push("", ...wrap(note, WIDTH));
|
|
189
|
+
if (command.examples.length) {
|
|
190
|
+
lines.push("", "Examples:");
|
|
191
|
+
for (const example of command.examples)
|
|
192
|
+
lines.push(INDENT + example);
|
|
193
|
+
}
|
|
194
|
+
return lines.join("\n") + "\n";
|
|
195
|
+
}
|
package/dist/muster.js
CHANGED
|
@@ -7,67 +7,19 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextpro
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { Muster } from "./run.js";
|
|
9
9
|
import { delay } from "./identity/processes.js";
|
|
10
|
-
import { formatHuman, humanColorEnabled } from "./format.js";
|
|
10
|
+
import { formatHuman, humanColorEnabled, statusText } from "./format.js";
|
|
11
11
|
import { checkPlugins, npmRegistry } from "./doctor.js";
|
|
12
12
|
import { loadConfig, musterHome } from "./config.js";
|
|
13
13
|
import { describeIdentities, interactiveSetup, setupIdentity, } from "./identity-cli.js";
|
|
14
14
|
import { IdentityError } from "./identity-store.js";
|
|
15
15
|
import { MCP_TOOLS } from "./mcp-tools.js";
|
|
16
|
+
import { commandHelp, topLevelHelp } from "./help.js";
|
|
17
|
+
import { promptForMissing, terminalReader } from "./prompt.js";
|
|
16
18
|
const listSchema = z
|
|
17
19
|
.object({ kind: z.enum(["session", "task"]).optional() })
|
|
18
20
|
.strict();
|
|
19
21
|
const idSchema = z.object({ id: z.string().min(1) }).strict();
|
|
20
22
|
const { version } = createRequire(import.meta.url)("../package.json");
|
|
21
|
-
const help = `Usage:
|
|
22
|
-
muster run <codex|claude|opencode> --prompt TEXT [--cwd DIR] [--kind session|task] [--host ID] [--open] [--terminal APP] [--request-key KEY] [-- RUNTIME_OPTIONS]
|
|
23
|
-
muster list [--kind session|task] [--format json|human]
|
|
24
|
-
muster stop <id> [--format json|human]
|
|
25
|
-
muster output <id>
|
|
26
|
-
muster doctor [--format json|human]
|
|
27
|
-
muster setup-identity --identity NAME --agent <codex|claude|opencode> [--token-env VAR] [--interactive]
|
|
28
|
-
muster identities [--format json|human]
|
|
29
|
-
muster mcp
|
|
30
|
-
muster --version
|
|
31
|
-
|
|
32
|
-
run also accepts --format json|human, --permissions auto|deny|bypass,
|
|
33
|
-
and --sandbox read-only|workspace-write|full-access before -- RUNTIME_OPTIONS.
|
|
34
|
-
--level read|work|open names a legal permissions and sandbox pair: read is
|
|
35
|
-
deny + read-only, work is auto + workspace-write, open is bypass + full-access
|
|
36
|
-
and still requires allow_dangerous_flags. Naming both a level and either flag
|
|
37
|
-
is an error rather than a precedence rule.
|
|
38
|
-
--open displays a tmux session on macOS. --terminal accepts auto (Terminal.app),
|
|
39
|
-
terminal, iterm2, or ghostty, and requires --open.
|
|
40
|
-
--mcp NAME is repeatable and replaces configured session defaults; --no-mcp
|
|
41
|
-
clears them. Tasks use no MCP defaults; select servers explicitly.
|
|
42
|
-
--plugin NAME is repeatable and replaces configured session defaults;
|
|
43
|
-
--no-plugin clears them. OpenCode only: selected plugins load in the child,
|
|
44
|
-
which also admits any plugin the target repository ships.
|
|
45
|
-
--model PROVIDER/MODEL selects the OpenCode model, overriding the configured
|
|
46
|
-
[opencode] model. Providers defined under [opencode.provider] are supplied to
|
|
47
|
-
the child, so launches do not depend on the user's own OpenCode config.
|
|
48
|
-
--project NAME launches in a directory named under [projects] in config.toml,
|
|
49
|
-
instead of --cwd; naming both is an error. When allowed_roots or [projects] are
|
|
50
|
-
configured, a launch outside all of them is refused.
|
|
51
|
-
--request-key KEY names the launch request itself. Sending the same key again
|
|
52
|
-
returns the launch it already produced rather than starting a second one, and
|
|
53
|
-
sending it with different parameters is refused rather than answered.
|
|
54
|
-
setup-identity takes either --interactive (Muster runs the Claude login and
|
|
55
|
-
stores the token in the identity) or --token-env VAR (the identity reads its
|
|
56
|
-
token from that variable instead, keeping nothing at rest). They are the two
|
|
57
|
-
mutually exclusive credential routes; passing both is an error rather than a
|
|
58
|
-
precedence rule.
|
|
59
|
-
run, list and stop default to JSON. output prints raw task text.
|
|
60
|
-
|
|
61
|
-
doctor checks path-configured plugins against the packages they came from: a
|
|
62
|
-
copied file goes stale silently while the package moves on. It reads the copy's
|
|
63
|
-
mtime, so a re-copied old file reads as current. Exits 1 when any plugin is
|
|
64
|
-
stale. Plugins named by npm are not checked — OpenCode caches those instead,
|
|
65
|
-
and the README covers clearing that cache.
|
|
66
|
-
|
|
67
|
-
pty launches print their record and keep this process running. Ctrl-C stops the
|
|
68
|
-
owned pty session. tmux launches survive this process exiting.
|
|
69
|
-
MCP: install deliberately in the one authorized session, never at user scope.
|
|
70
|
-
`;
|
|
71
23
|
const text = (value) => ({
|
|
72
24
|
content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
|
|
73
25
|
});
|
|
@@ -95,21 +47,53 @@ function identitiesHuman(view, color) {
|
|
|
95
47
|
async function main() {
|
|
96
48
|
const argv = process.argv.slice(2);
|
|
97
49
|
if (argv[0] === "--help" || argv[0] === "-h") {
|
|
98
|
-
process.stdout.write(
|
|
50
|
+
process.stdout.write(topLevelHelp());
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Recognised before parsing, not during it: the parser is strict and has no
|
|
54
|
+
// `help` option, so `muster setup-identity --help` used to be answered with
|
|
55
|
+
// "Unknown option '--help'". Only in second position, so a `--help` that is
|
|
56
|
+
// some flag's value still reaches the command it was written for.
|
|
57
|
+
if ((argv[1] === "--help" || argv[1] === "-h") && commandHelp(argv[0])) {
|
|
58
|
+
process.stdout.write(commandHelp(argv[0]));
|
|
99
59
|
return;
|
|
100
60
|
}
|
|
101
61
|
if (argv[0] === "--version" || argv[0] === "-v") {
|
|
102
62
|
process.stdout.write(`${version}\n`);
|
|
103
63
|
return;
|
|
104
64
|
}
|
|
105
|
-
|
|
106
|
-
|
|
65
|
+
// No arguments starts the MCP server: a documented entry point, and how a
|
|
66
|
+
// client configured with `command: "muster"` and no args reaches it. From a
|
|
67
|
+
// terminal that is indistinguishable from a hang — the process is waiting for
|
|
68
|
+
// JSON-RPC on stdin and has nothing to say. A client always arrives down a
|
|
69
|
+
// pipe and never a TTY, so a person can be answered without changing what any
|
|
70
|
+
// client gets. `muster mcp` stays unconditional, so the server can still be
|
|
71
|
+
// driven by hand in a terminal to debug it.
|
|
72
|
+
const bareAtTerminal = argv.length === 0 && Boolean(process.stdin.isTTY);
|
|
73
|
+
const mcp = !bareAtTerminal && (argv.length === 0 || argv[0] === "mcp");
|
|
74
|
+
if (argv[0] === "mcp" && argv.length > 1)
|
|
107
75
|
throw new Error("mcp takes no arguments");
|
|
108
76
|
const muster = await Muster.create();
|
|
109
77
|
let shutdown;
|
|
110
78
|
const close = () => (shutdown ??= (async () => {
|
|
111
79
|
await muster.close();
|
|
112
80
|
})());
|
|
81
|
+
if (bareAtTerminal) {
|
|
82
|
+
try {
|
|
83
|
+
const cfg = await loadConfig();
|
|
84
|
+
const view = await describeIdentities(musterHome(), process.env, Object.fromEntries(Object.entries(cfg.requester_profiles).map(([n, p]) => [
|
|
85
|
+
n,
|
|
86
|
+
p.identities,
|
|
87
|
+
])));
|
|
88
|
+
process.stdout.write(topLevelHelp() +
|
|
89
|
+
"\n" +
|
|
90
|
+
statusText(view.identities, (await muster.summary()).sessions));
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
await close();
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
113
97
|
for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"])
|
|
114
98
|
process.once(signal, () => {
|
|
115
99
|
void close().then(() => process.exit(0), (e) => {
|
|
@@ -239,9 +223,23 @@ async function main() {
|
|
|
239
223
|
else if (command === "setup-identity") {
|
|
240
224
|
if (positionals.length)
|
|
241
225
|
throw new Error("setup-identity takes no positional arguments");
|
|
242
|
-
const {
|
|
243
|
-
|
|
244
|
-
|
|
226
|
+
const { "token-env": tokenEnv } = values;
|
|
227
|
+
let { identity, agent } = values;
|
|
228
|
+
if (!identity || !agent) {
|
|
229
|
+
// Asking is what `--interactive` already implied. Without it the
|
|
230
|
+
// command is the unattended route and still requires both flags, and
|
|
231
|
+
// with it but no terminal there is nobody to ask — which is a different
|
|
232
|
+
// problem from having forgotten a flag, so it says so.
|
|
233
|
+
if (!values.interactive)
|
|
234
|
+
throw new Error("setup-identity requires --identity and --agent");
|
|
235
|
+
if (!process.stdin.isTTY)
|
|
236
|
+
throw new Error("--interactive needs a terminal to ask for --identity and --agent; pass both as flags for an unattended run");
|
|
237
|
+
({ identity, agent } = await promptForMissing({
|
|
238
|
+
identity,
|
|
239
|
+
agent,
|
|
240
|
+
ask: terminalReader(),
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
245
243
|
if (values.interactive) {
|
|
246
244
|
// Refused, not ranked. These are the two mutually exclusive credential
|
|
247
245
|
// routes, and the interactive branch is taken before tokenEnv is read:
|
|
@@ -300,7 +298,7 @@ async function main() {
|
|
|
300
298
|
process.stdout.write(await muster.output(id));
|
|
301
299
|
}
|
|
302
300
|
else
|
|
303
|
-
throw new Error(
|
|
301
|
+
throw new Error(topLevelHelp());
|
|
304
302
|
}
|
|
305
303
|
finally {
|
|
306
304
|
await close();
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
export const AGENTS = ["codex", "claude", "opencode"];
|
|
3
|
+
/** The store's own rule (`identity-store.ts`), repeated here so a name this
|
|
4
|
+
* accepts cannot be rejected a moment later by the thing it is typed for. */
|
|
5
|
+
const NAME = /^[a-zA-Z0-9_-]+$/;
|
|
6
|
+
const ATTEMPTS = 3;
|
|
7
|
+
async function until(ask, question, parse, complaint, write) {
|
|
8
|
+
for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
|
|
9
|
+
const value = parse((await ask(question)).trim());
|
|
10
|
+
if (value !== undefined)
|
|
11
|
+
return value;
|
|
12
|
+
if (attempt < ATTEMPTS)
|
|
13
|
+
write(`${complaint}\n`);
|
|
14
|
+
}
|
|
15
|
+
throw new Error(`${complaint} — no valid answer after three attempts.`);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Fills in what `setup-identity --interactive` was not given.
|
|
19
|
+
*
|
|
20
|
+
* `--interactive` used to mean only that the login runs here; `--identity` and
|
|
21
|
+
* `--agent` were still required, so the way a person naturally types the
|
|
22
|
+
* command was answered with "setup-identity requires --identity and --agent".
|
|
23
|
+
* Asking is what "interactive" already implied. Anything passed as a flag is
|
|
24
|
+
* kept as-is and not asked about, so a scripted invocation is unchanged.
|
|
25
|
+
*/
|
|
26
|
+
export async function promptForMissing(opts) {
|
|
27
|
+
const write = opts.write ?? ((text) => process.stderr.write(text));
|
|
28
|
+
const identity = opts.identity ??
|
|
29
|
+
(await until(opts.ask, "Identity name: ", (answer) => (NAME.test(answer) ? answer : undefined), "An identity name is letters, digits, dash or underscore", write));
|
|
30
|
+
const agent = opts.agent ??
|
|
31
|
+
(await until(opts.ask, `Agent [${AGENTS.join("/")}]: `, (answer) => AGENTS.includes(answer)
|
|
32
|
+
? answer
|
|
33
|
+
: undefined, `An agent is one of ${AGENTS.join(", ")}`, write));
|
|
34
|
+
return { identity, agent };
|
|
35
|
+
}
|
|
36
|
+
/** The real terminal. Kept apart from the logic above so the logic needs none. */
|
|
37
|
+
export function terminalReader() {
|
|
38
|
+
return async (question) => {
|
|
39
|
+
const rl = createInterface({
|
|
40
|
+
input: process.stdin,
|
|
41
|
+
output: process.stderr,
|
|
42
|
+
});
|
|
43
|
+
try {
|
|
44
|
+
return await rl.question(question);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
rl.close();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
package/dist/run.js
CHANGED
|
@@ -823,6 +823,21 @@ export class Muster {
|
|
|
823
823
|
* with the supported entry point (#44), which is also what would need to
|
|
824
824
|
* choose between hiding other requesters' entries and redacting them.
|
|
825
825
|
*/
|
|
826
|
+
/**
|
|
827
|
+
* Counts for the status line a bare `muster` prints at a terminal.
|
|
828
|
+
*
|
|
829
|
+
* Deliberately not `list`. That refreshes every entry, and a refresh removes
|
|
830
|
+
* a terminal entry's identity copy and MCP config and reaches an OpenCode
|
|
831
|
+
* session over HTTP — none of which belongs in the output of typing a bare
|
|
832
|
+
* command. This reads the registry and changes nothing, so what it reports is
|
|
833
|
+
* what was recorded rather than what is running, and the caller says so.
|
|
834
|
+
*/
|
|
835
|
+
async summary() {
|
|
836
|
+
const entries = await this.registry.all();
|
|
837
|
+
return {
|
|
838
|
+
sessions: entries.filter((e) => e.kind === "session" && ["starting", "running"].includes(e.status)).length,
|
|
839
|
+
};
|
|
840
|
+
}
|
|
826
841
|
async list(kind) {
|
|
827
842
|
const entries = await this.registry.all();
|
|
828
843
|
const result = [];
|