@brutalsystems/muster 0.7.16 → 0.7.17

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 CHANGED
@@ -892,8 +892,12 @@ selected external tool may have its own write or network capabilities.
892
892
  Installed deliberately, in the one session that should hold spawn authority —
893
893
  never at user scope.
894
894
 
895
- Muster exposes `run`, `list`, `stop`, and `output` over stdio. No arguments or
896
- `mcp` starts the server. Diagnostics go to stderr, never protocol stdout.
895
+ Muster exposes `run`, `list`, `stop`, and `output` over stdio. `mcp` starts the
896
+ server, and so does no arguments when stdin is not a terminal — which is what an
897
+ MCP client provides. Typing `muster` at a terminal prints the usage text and a
898
+ short status instead, because a server waiting for JSON-RPC on stdin is
899
+ indistinguishable from a hang. `muster mcp` starts the server either way, so it
900
+ can still be driven by hand to debug it. Diagnostics go to stderr, never protocol stdout.
897
901
  Schemas match the CLI (`args` is the array of optional runtime arguments).
898
902
 
899
903
  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/muster.js CHANGED
@@ -7,7 +7,7 @@ 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";
@@ -102,14 +102,38 @@ async function main() {
102
102
  process.stdout.write(`${version}\n`);
103
103
  return;
104
104
  }
105
- const mcp = argv.length === 0 || argv[0] === "mcp";
106
- if (mcp && argv.length > 1)
105
+ // No arguments starts the MCP server: a documented entry point, and how a
106
+ // client configured with `command: "muster"` and no args reaches it. From a
107
+ // terminal that is indistinguishable from a hang — the process is waiting for
108
+ // JSON-RPC on stdin and has nothing to say. A client always arrives down a
109
+ // pipe and never a TTY, so a person can be answered without changing what any
110
+ // client gets. `muster mcp` stays unconditional, so the server can still be
111
+ // driven by hand in a terminal to debug it.
112
+ const bareAtTerminal = argv.length === 0 && Boolean(process.stdin.isTTY);
113
+ const mcp = !bareAtTerminal && (argv.length === 0 || argv[0] === "mcp");
114
+ if (argv[0] === "mcp" && argv.length > 1)
107
115
  throw new Error("mcp takes no arguments");
108
116
  const muster = await Muster.create();
109
117
  let shutdown;
110
118
  const close = () => (shutdown ??= (async () => {
111
119
  await muster.close();
112
120
  })());
121
+ if (bareAtTerminal) {
122
+ try {
123
+ const cfg = await loadConfig();
124
+ const view = await describeIdentities(musterHome(), process.env, Object.fromEntries(Object.entries(cfg.requester_profiles).map(([n, p]) => [
125
+ n,
126
+ p.identities,
127
+ ])));
128
+ process.stdout.write(help +
129
+ "\n" +
130
+ statusText(view.identities, (await muster.summary()).sessions));
131
+ }
132
+ finally {
133
+ await close();
134
+ }
135
+ return;
136
+ }
113
137
  for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"])
114
138
  process.once(signal, () => {
115
139
  void close().then(() => process.exit(0), (e) => {
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 = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brutalsystems/muster",
3
- "version": "0.7.16",
3
+ "version": "0.7.17",
4
4
  "description": "Launch instructed, reachable local agent sessions",
5
5
  "type": "module",
6
6
  "engines": {