@brutalsystems/muster 0.6.0 → 0.7.1
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/CONTRACT_PROVENANCE.md +69 -0
- package/README.md +76 -22
- package/dist/format.js +2 -0
- package/dist/guard.js +9 -5
- package/dist/identity/opencode.js +86 -0
- package/dist/muster.js +13 -3
- package/dist/naming.js +3 -2
- package/dist/opencode-http.js +241 -0
- package/dist/opencode-policy.js +382 -0
- package/dist/reach/opencode.js +17 -0
- package/dist/run.js +87 -12
- package/package.json +5 -2
- package/test/fixtures/canonical-id.json +2 -2
package/CONTRACT_PROVENANCE.md
CHANGED
|
@@ -38,6 +38,75 @@ with the keys `slugify`, `suffix`, `assign`, `resolve` in that order is:
|
|
|
38
38
|
`a92936ef4b713641d29da3fbafccab0e6da79a54e27bfea3c6a779e801a925b3`.
|
|
39
39
|
Both revisions produce this hash.
|
|
40
40
|
|
|
41
|
+
## Privacy remediation edit — 2026-09-20
|
|
42
|
+
|
|
43
|
+
`test/fixtures/canonical-id.json` originally copied a real internal project
|
|
44
|
+
codename as an example `rawName`/`display`/`canonicalId` value. Tin Can made
|
|
45
|
+
the same change to its own copy and asked Muster to match; both projects
|
|
46
|
+
renamed the same two example strings to a neutral placeholder
|
|
47
|
+
(`billing-api`).
|
|
48
|
+
|
|
49
|
+
This is a **local text edit to the frozen 0.1.0 copy**, not a re-sync with
|
|
50
|
+
Tin Can's current upstream fixture. By the time of this edit, Tin Can's
|
|
51
|
+
fixture had moved to `tincan_version: 0.5.2` and gained real new content —
|
|
52
|
+
`opencode` suffix rules, new fixture cases, and new `CANONICAL_ID.md`
|
|
53
|
+
sections describing opencode session-id behavior. Muster's OpenCode
|
|
54
|
+
integration explicitly defers Tin Can/address-format work (see
|
|
55
|
+
`RELEASE_NOTES.md`), so adopting that upstream fixture wholesale would claim
|
|
56
|
+
verification Muster does not have. Per this document's own earlier
|
|
57
|
+
guidance — "their full-file hashes ... must not be compared to an entire
|
|
58
|
+
newer upstream fixture to infer a format change" — only the two leaked
|
|
59
|
+
strings were changed; every other byte of the 0.1.0-vintage fixture is
|
|
60
|
+
unchanged, including its 31 cases and `tincan_version: 0.1.0`.
|
|
61
|
+
|
|
62
|
+
New hash after this edit:
|
|
63
|
+
|
|
64
|
+
| File | SHA-256 |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| `test/fixtures/canonical-id.json` | `974cef9e06370a562cef588f1cd6d5c1d83b49afdb64808487759d6280bffff2` |
|
|
67
|
+
|
|
68
|
+
`CANONICAL_ID.md`'s hash is unchanged (`8cf4312d6af1c4d85ec0641d8739bec261be2feb35dd16bdd9d8be2b89551915`)
|
|
69
|
+
— the codename never appeared there.
|
|
70
|
+
|
|
71
|
+
Adopting Tin Can's opencode naming rules, if Muster's OpenCode/Tin Can
|
|
72
|
+
integration is ever un-deferred, is a separate, deliberate compatibility
|
|
73
|
+
check — not something this edit performs.
|
|
74
|
+
|
|
75
|
+
The `at commit fbaaea5842fd4a5c86849d7f51c8e16b8683b058` provenance line
|
|
76
|
+
above may no longer resolve in Tin Can's current history: Tin Can applied
|
|
77
|
+
the same codename remediation via a history rewrite (`git filter-repo
|
|
78
|
+
--replace-text`, force-pushed), which changes the hash of every commit
|
|
79
|
+
whose content or ancestry it touched. This is a historical acquisition
|
|
80
|
+
record, not a claim that the commit is still reachable.
|
|
81
|
+
|
|
82
|
+
## Contract baseline moved to 0.5.2 — 2026-09-20
|
|
83
|
+
|
|
84
|
+
`@brutalsystems/tincan@0.2.0`, this document's verified baseline, was
|
|
85
|
+
withdrawn from the npm registry as part of Tin Can's own codename
|
|
86
|
+
remediation (every version through 0.5.1 carried the same leaked string
|
|
87
|
+
0.7.0's fixture did, in Tin Can's README and fixture). Withdrawn version
|
|
88
|
+
numbers cannot be republished. This is a **registry availability problem,
|
|
89
|
+
not a format change** — Tin Can diffed `src/naming.ts` between 0.2.0 and
|
|
90
|
+
0.5.2 and found `slugify`, `suffixOf`, `assignNames`, and `resolvePeer`
|
|
91
|
+
byte-identical; the only source change is the `RuntimeName` union widening
|
|
92
|
+
to add `'opencode'`. `test/fixtures/canonical-id.json` gained new opencode
|
|
93
|
+
cases with every pre-existing expectation unchanged.
|
|
94
|
+
|
|
95
|
+
Muster's own contract suite was run against a real installed
|
|
96
|
+
`@brutalsystems/tincan@0.5.2` and passed all four cases before this
|
|
97
|
+
re-baseline was made.
|
|
98
|
+
|
|
99
|
+
`0.3.0` through `0.5.1` were never verified by Muster's contract suite (see
|
|
100
|
+
"Upgrade boundary" below) and have also been withdrawn from the registry.
|
|
101
|
+
The verified, currently-available set is exactly `{0.5.2}`.
|
|
102
|
+
|
|
103
|
+
One caveat for anyone reconstructing a baseline from Tin Can's git history
|
|
104
|
+
rather than a published artifact: Tin Can's git tags predating 2026-09-20
|
|
105
|
+
no longer match what npm ever served at that version number, because the
|
|
106
|
+
history-rewrite in the section above applies retroactively to every commit,
|
|
107
|
+
tagged ones included. Git content at an old tag is not evidence of what a
|
|
108
|
+
withdrawn npm artifact actually contained.
|
|
109
|
+
|
|
41
110
|
## Upgrade boundary: listing and state
|
|
42
111
|
|
|
43
112
|
The upstream release notes report that 0.3.0 expands
|
package/README.md
CHANGED
|
@@ -2,21 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
[](./LICENSE)
|
|
4
4
|
|
|
5
|
-
Muster launches instructed Codex
|
|
5
|
+
Muster launches instructed Codex, Claude Code, or OpenCode agents on this machine. A
|
|
6
6
|
`session` returns an address only after its runtime is reachable. A `task`
|
|
7
7
|
runs once, captures output, and never advertises a peer address.
|
|
8
8
|
|
|
9
9
|
**Live launch verification:** Codex and Claude both launched and answered their
|
|
10
|
-
initial prompts without terminal interaction.
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
initial prompts without terminal interaction. OpenCode 1.18.31 passed the
|
|
11
|
+
loopback API and local-provider protocol checks. Claude requires a directory
|
|
12
|
+
already trusted by the operator. The automated suite uses fake runtimes, not
|
|
13
|
+
real models.
|
|
13
14
|
|
|
14
15
|
## Build and run locally
|
|
15
16
|
|
|
16
|
-
Requires Node 22.12+ (tested with 24.16), TypeScript 5,
|
|
17
|
-
Code 2.1.267
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
Requires Node 22.12+ (tested with 24.16), TypeScript 5, Codex 0.155.1, Claude
|
|
18
|
+
Code 2.1.267, and OpenCode 1.18.31 or newer. Newer OpenCode releases must retain
|
|
19
|
+
the CLI and loopback HTTP API used by Muster. Terminal drivers use tmux or
|
|
20
|
+
node-pty. Process discovery currently requires a POSIX host with `ps` and
|
|
21
|
+
`lsof`; the terminal interface itself is platform-neutral. The macOS Terminal
|
|
22
|
+
driver is an unavailable v1 stub.
|
|
20
23
|
|
|
21
24
|
Before launching a Claude session, open Claude normally in the target directory
|
|
22
25
|
and complete its workspace-trust review yourself, then exit that setup session.
|
|
@@ -29,7 +32,10 @@ npm ci
|
|
|
29
32
|
npm run build
|
|
30
33
|
node dist/muster.js run codex --prompt 'Review the authentication flow'
|
|
31
34
|
node dist/muster.js run claude --prompt 'Summarize the project' --host tmux
|
|
35
|
+
node dist/muster.js run opencode --prompt 'Review this project'
|
|
36
|
+
node dist/muster.js run opencode --prompt 'Build a plan' -- --model coldfire-local/qwen3-30b-a3b
|
|
32
37
|
node dist/muster.js run codex --kind task --prompt 'Explain the test layout'
|
|
38
|
+
node dist/muster.js run opencode --kind task --prompt 'Summarize the tests'
|
|
33
39
|
node dist/muster.js list
|
|
34
40
|
node dist/muster.js list --format human
|
|
35
41
|
node dist/muster.js list --kind task
|
|
@@ -64,11 +70,25 @@ red for failures or nonzero exit codes, and cyan for follow-up commands. IDs
|
|
|
64
70
|
and paths retain the normal text color. Set `NO_COLOR=1` to disable colors;
|
|
65
71
|
piped output and `TERM=dumb` are always uncolored. JSON never includes colors.
|
|
66
72
|
|
|
67
|
-
`run` accepts an optional `--` followed by runtime arguments. The normal
|
|
68
|
-
is `--model` / `-m`, plus Claude's `--effort
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
`run` accepts an optional `--` followed by runtime arguments. The normal
|
|
74
|
+
allowlist is `--model` / `-m`, plus Claude's `--effort` and OpenCode's `--agent`
|
|
75
|
+
and `--variant`. Unknown options, bundled short options, config injection and
|
|
76
|
+
raw permission overrides are refused, even with `allow_dangerous_flags`
|
|
77
|
+
enabled. Use Muster's normalized permission flags. Prompt strings are always
|
|
78
|
+
passed as one literal argument. Codex and Claude launches and OpenCode tasks put
|
|
79
|
+
that value after `--`; OpenCode sessions use the dedicated `--prompt` value.
|
|
80
|
+
|
|
81
|
+
OpenCode inherits the provider and model definitions from its normal user and
|
|
82
|
+
project configuration, so configured local OpenAI-compatible providers such as
|
|
83
|
+
`coldfire-local/qwen3-30b-a3b` remain available. Muster never rewrites those
|
|
84
|
+
files. Put runtime options after `--`, as in the local-model example above.
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
muster run opencode --prompt 'Review this project'
|
|
88
|
+
muster run opencode --prompt 'Build a plan' -- --model coldfire-local/qwen3-30b-a3b
|
|
89
|
+
muster run opencode --kind task --prompt 'Summarize the tests'
|
|
90
|
+
muster run opencode --mcp tincan --permissions auto --sandbox workspace-write --prompt 'Coordinate'
|
|
91
|
+
```
|
|
72
92
|
|
|
73
93
|
## Terminal lifetime
|
|
74
94
|
|
|
@@ -77,6 +97,7 @@ Add `--open` to display the launched session in a new Terminal.app window on mac
|
|
|
77
97
|
```sh
|
|
78
98
|
muster run codex --open --prompt "Review this project" --format human
|
|
79
99
|
muster run claude --open --terminal ghostty --prompt "Review this project" --format human
|
|
100
|
+
muster run opencode --open --terminal iterm2 --prompt "Review this project" --format human
|
|
80
101
|
muster run codex --open --terminal iterm2 --prompt "Review this project" --format human
|
|
81
102
|
```
|
|
82
103
|
|
|
@@ -116,6 +137,16 @@ and `attach_hint`. Watchability is separate from the runtime's idle/busy state.
|
|
|
116
137
|
or startup failure cleans the process tree and host window, logs failure, and
|
|
117
138
|
returns an error instead of a peer record.
|
|
118
139
|
|
|
140
|
+
Each OpenCode session owns a separate HTTP server bound to `127.0.0.1` on a
|
|
141
|
+
Muster-selected port. Muster verifies that the listener belongs to the launched
|
|
142
|
+
process tree, stores its `server_url` and durable `session_id`, and never attaches
|
|
143
|
+
to an existing personal OpenCode server. `list` refreshes the session through
|
|
144
|
+
that endpoint; `stop` aborts it best-effort and still performs authoritative
|
|
145
|
+
process-tree cleanup. The URL is local control metadata, not a public address.
|
|
146
|
+
OpenCode sessions otherwise use the same tmux/pty, viewer, list, stop, and human
|
|
147
|
+
output behavior described above; OpenCode tasks use the same detached output
|
|
148
|
+
and exit-status lifecycle as other tasks.
|
|
149
|
+
|
|
119
150
|
## Configuration and permissions
|
|
120
151
|
|
|
121
152
|
Muster reads `~/.muster/config.toml` once at startup and **never writes it**.
|
|
@@ -146,11 +177,11 @@ muster run claude --permissions auto --sandbox workspace-write \
|
|
|
146
177
|
--prompt 'Implement the change' --format human
|
|
147
178
|
```
|
|
148
179
|
|
|
149
|
-
| Muster permissions | Codex translation
|
|
150
|
-
|
|
151
|
-
| `deny` (default)
|
|
152
|
-
| `auto`
|
|
153
|
-
| `bypass`
|
|
180
|
+
| Muster permissions | Codex translation | Claude Code translation | OpenCode translation |
|
|
181
|
+
| ------------------ | ---------------------------- | ----------------------------------- | ------------------------------------------------------------------------------- |
|
|
182
|
+
| `deny` (default) | Approval policy `never` | Permission mode `dontAsk` | Deny write, shell, task, external-directory, web, and unselected MCP operations |
|
|
183
|
+
| `auto` | `--approve-for-me` | Permission mode `auto` | `--auto` with explicit deny rules |
|
|
184
|
+
| `bypass` | Bypass approvals and sandbox | Permission mode `bypassPermissions` | Allow supported operations |
|
|
154
185
|
|
|
155
186
|
`deny` refuses actions that would require approval; it does not prohibit tools
|
|
156
187
|
already allowed by the sandbox or permission rules. `auto` delegates permission
|
|
@@ -175,6 +206,12 @@ launch settings, not a continuous attestation of remote policy or user changes.
|
|
|
175
206
|
See [Codex auto-review](https://learn.chatgpt.com/docs/sandboxing/auto-review) and
|
|
176
207
|
[Claude permission modes](https://code.claude.com/docs/en/permission-modes).
|
|
177
208
|
|
|
209
|
+
OpenCode permissions are tool policy, not an OS-level filesystem sandbox.
|
|
210
|
+
Muster's read-only OpenCode policy denies editing, shell, subagent,
|
|
211
|
+
external-directory, web, and MCP operations except explicitly selected MCP
|
|
212
|
+
tools; it does not claim kernel-enforced isolation. Auto still requires
|
|
213
|
+
`workspace-write`, and bypass still requires authorized `full-access`.
|
|
214
|
+
|
|
178
215
|
The concurrency cap is shared by separate CLI/MCP processes, including pending
|
|
179
216
|
launches. Settings apply to both kinds; tasks are read-only by default.
|
|
180
217
|
|
|
@@ -194,10 +231,17 @@ arrive after startup; this v1 is not an enterprise policy-enforcement layer.
|
|
|
194
231
|
Claude permissions and its command sandbox are different mechanisms; neither
|
|
195
232
|
claim implies that every external tool is OS-sandboxed.
|
|
196
233
|
|
|
234
|
+
OpenCode keeps normal provider/model configuration but launches with `--pure`,
|
|
235
|
+
which disables inherited external plugins. Before launch, Muster inspects the
|
|
236
|
+
effective OpenCode configuration, disables every inherited MCP server and tool
|
|
237
|
+
family, enables only the selected Muster definitions, and fails closed if a
|
|
238
|
+
higher-precedence managed policy prevents that isolation from being proven.
|
|
239
|
+
|
|
197
240
|
These child restrictions prevent automatically propagating spawn authority.
|
|
198
241
|
External tools are enabled only through the operator configuration and launch
|
|
199
|
-
selection described below.
|
|
200
|
-
external Tin Can even when
|
|
242
|
+
selection described below. Codex and Claude runtime inboxes remain reachable
|
|
243
|
+
from an external Tin Can even when they have no MCP tools of their own. OpenCode
|
|
244
|
+
delivery remains deferred as documented under Tin Can compatibility.
|
|
201
245
|
No user-level runtime configuration is rewritten, and workspace-trust dialogs
|
|
202
246
|
are never accepted automatically.
|
|
203
247
|
|
|
@@ -224,11 +268,13 @@ tools = ["peers", "send_peer", "message_log"]
|
|
|
224
268
|
required = false
|
|
225
269
|
```
|
|
226
270
|
|
|
227
|
-
Then
|
|
271
|
+
Then any runtime can use it:
|
|
228
272
|
|
|
229
273
|
```sh
|
|
230
274
|
muster run codex --mcp tincan --prompt 'Coordinate with the other agent'
|
|
231
275
|
muster run claude --mcp tincan --prompt 'Coordinate with the other agent'
|
|
276
|
+
muster run opencode --mcp tincan --permissions auto --sandbox workspace-write \
|
|
277
|
+
--prompt 'Coordinate'
|
|
232
278
|
muster run codex --no-mcp --prompt 'Work without external tools'
|
|
233
279
|
```
|
|
234
280
|
|
|
@@ -261,6 +307,9 @@ Codex receives isolated definitions, using a fresh runtime name when an inherite
|
|
|
261
307
|
server has the same name, and its native tool allowlist. Claude receives a private
|
|
262
308
|
Muster stdio connection that filters both tool discovery and calls; newly added
|
|
263
309
|
upstream tools cannot bypass the selection. Only selected tools are preapproved.
|
|
310
|
+
OpenCode disables inherited definitions and tool families in its verified
|
|
311
|
+
per-launch overlay, then enables only selected definitions. It also uses
|
|
312
|
+
`--pure`, so inherited plugins do not enter the child process.
|
|
264
313
|
Claude connection specifications are stored with owner-only permissions under
|
|
265
314
|
`~/.muster/mcp`, removed on stop, task completion, or when `list` observes an ended
|
|
266
315
|
session. MCP server access is separate from filesystem sandbox permissions: a
|
|
@@ -306,6 +355,11 @@ Codex `idle` means reachable and not known to be busy, not guaranteed free.
|
|
|
306
355
|
The querying app-server can report `notLoaded` for a live thread; Muster maps
|
|
307
356
|
that to `idle`. Claude state comes from its session registry.
|
|
308
357
|
|
|
358
|
+
OpenCode peers expose a durable `session_id` and loopback-only `server_url` so a
|
|
359
|
+
future Tin Can adapter has a stable integration seam. Tin Can does not yet send
|
|
360
|
+
to OpenCode through Muster: choosing native OpenCode messaging versus A2A, and
|
|
361
|
+
updating Tin Can for that transport, are explicitly deferred follow-up work.
|
|
362
|
+
|
|
309
363
|
## Verification
|
|
310
364
|
|
|
311
365
|
```sh
|
|
@@ -325,7 +379,7 @@ For reproducible verification, install that exact release outside Muster:
|
|
|
325
379
|
|
|
326
380
|
```sh
|
|
327
381
|
contract_dir=$(mktemp -d)
|
|
328
|
-
npm install --prefix "$contract_dir" --no-save @brutalsystems/tincan@0.2
|
|
382
|
+
npm install --prefix "$contract_dir" --no-save @brutalsystems/tincan@0.5.2
|
|
329
383
|
MUSTER_TINCAN_BIN="$contract_dir/node_modules/.bin/tincan" npm run test:contract
|
|
330
384
|
```
|
|
331
385
|
|
package/dist/format.js
CHANGED
|
@@ -35,6 +35,8 @@ function recordHuman(record, color) {
|
|
|
35
35
|
];
|
|
36
36
|
if (record.canonical_id)
|
|
37
37
|
lines.push(field("Address", clean(record.canonical_id)));
|
|
38
|
+
if (record.runtime === "opencode" && record.server_url)
|
|
39
|
+
lines.push(field("Control URL", clean(record.server_url)));
|
|
38
40
|
lines.push(field("Directory", clean(record.cwd)));
|
|
39
41
|
if (record.permissions)
|
|
40
42
|
lines.push(field("Permissions", clean(record.permissions)));
|
package/dist/guard.js
CHANGED
|
@@ -3,7 +3,7 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import { mcpName, claudeMcpConfig } from "./mcp.js";
|
|
4
4
|
export const runSchema = z
|
|
5
5
|
.object({
|
|
6
|
-
runtime: z.enum(["codex", "claude"]),
|
|
6
|
+
runtime: z.enum(["codex", "claude", "opencode"]),
|
|
7
7
|
prompt: z
|
|
8
8
|
.string()
|
|
9
9
|
.refine((s) => s.trim().length > 0, "prompt must not be blank"),
|
|
@@ -37,12 +37,16 @@ export function resolvePermissions(req, config) {
|
|
|
37
37
|
throw new Error("--permissions bypass requires --sandbox full-access");
|
|
38
38
|
return { permissions, sandbox };
|
|
39
39
|
}
|
|
40
|
-
function
|
|
40
|
+
export function runtimeArgs(req) {
|
|
41
41
|
const result = [];
|
|
42
|
+
const allowed = req.runtime === "claude"
|
|
43
|
+
? ["--model", "-m", "--effort"]
|
|
44
|
+
: req.runtime === "opencode"
|
|
45
|
+
? ["--model", "-m", "--agent", "--variant"]
|
|
46
|
+
: ["--model", "-m"];
|
|
42
47
|
for (let i = 0; i < req.args.length; i++) {
|
|
43
48
|
const arg = req.args[i], flag = arg.split("=")[0].toLowerCase().replaceAll("_", "-");
|
|
44
|
-
if (!
|
|
45
|
-
(flag === "--effort" && req.runtime !== "claude"))
|
|
49
|
+
if (!allowed.includes(flag))
|
|
46
50
|
throw new Error(`Runtime option refused: ${arg}`);
|
|
47
51
|
if (arg.includes("="))
|
|
48
52
|
result.push(arg);
|
|
@@ -56,7 +60,7 @@ function extras(req) {
|
|
|
56
60
|
return result;
|
|
57
61
|
}
|
|
58
62
|
export function launchArgs(req, config, mcp = [], bridgePath) {
|
|
59
|
-
const extra =
|
|
63
|
+
const extra = runtimeArgs(req);
|
|
60
64
|
const policy = resolvePermissions(req, config);
|
|
61
65
|
if (req.runtime === "codex")
|
|
62
66
|
return [
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { OpenCodeHttp, OpenCodeProtocolError, } from "../opencode-http.js";
|
|
3
|
+
import { command, delay, descendants, processRef } from "./processes.js";
|
|
4
|
+
const POLL_INTERVAL_MS = 50;
|
|
5
|
+
function rawName(session) {
|
|
6
|
+
return session.title.trim() === "" ? null : session.title;
|
|
7
|
+
}
|
|
8
|
+
export async function openCodeEndpointOwnedBy(rootPid, serverUrl, deadline) {
|
|
9
|
+
try {
|
|
10
|
+
const endpoint = new URL(serverUrl);
|
|
11
|
+
const port = Number(endpoint.port);
|
|
12
|
+
if (!Number.isInteger(port) || port <= 0 || deadline <= Date.now())
|
|
13
|
+
return false;
|
|
14
|
+
const family = new Set(await descendants(rootPid, deadline));
|
|
15
|
+
const output = await command("lsof", [
|
|
16
|
+
"-nP",
|
|
17
|
+
"-a",
|
|
18
|
+
`-iTCP@${endpoint.hostname}:${port}`,
|
|
19
|
+
"-sTCP:LISTEN",
|
|
20
|
+
"-Fp",
|
|
21
|
+
], Math.max(1, Math.min(1000, deadline - Date.now())));
|
|
22
|
+
const listeners = output
|
|
23
|
+
.split("\n")
|
|
24
|
+
.filter((line) => /^p\d+$/.test(line))
|
|
25
|
+
.map((line) => Number(line.slice(1)));
|
|
26
|
+
return listeners.length > 0 && listeners.every((pid) => family.has(pid));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function resolveOpenCode(rootPid, serverUrl, cwd, createdAfter, deadline) {
|
|
33
|
+
const resolvedCwd = await realpath(cwd);
|
|
34
|
+
const client = new OpenCodeHttp(serverUrl);
|
|
35
|
+
let ownershipProven = false;
|
|
36
|
+
while (Date.now() < deadline) {
|
|
37
|
+
if (!(await processRef(rootPid)))
|
|
38
|
+
return;
|
|
39
|
+
if (!ownershipProven &&
|
|
40
|
+
!(await openCodeEndpointOwnedBy(rootPid, serverUrl, deadline))) {
|
|
41
|
+
const remaining = deadline - Date.now();
|
|
42
|
+
if (remaining > 0)
|
|
43
|
+
await delay(Math.min(POLL_INTERVAL_MS, remaining));
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
ownershipProven = true;
|
|
47
|
+
try {
|
|
48
|
+
const health = await client.health(deadline);
|
|
49
|
+
if (health.healthy) {
|
|
50
|
+
const sessions = await client.sessions(deadline);
|
|
51
|
+
if (!(await processRef(rootPid)))
|
|
52
|
+
return;
|
|
53
|
+
const candidates = sessions.filter((session) => session.directory === resolvedCwd &&
|
|
54
|
+
session.time.created >= createdAfter);
|
|
55
|
+
const statuses = candidates.length
|
|
56
|
+
? await client.statuses(deadline)
|
|
57
|
+
: {};
|
|
58
|
+
if (!(await processRef(rootPid)))
|
|
59
|
+
return;
|
|
60
|
+
if (!(await openCodeEndpointOwnedBy(rootPid, serverUrl, deadline))) {
|
|
61
|
+
ownershipProven = false;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const matches = candidates.filter((session) => Object.prototype.hasOwnProperty.call(statuses, session.id));
|
|
65
|
+
if (matches.length > 1)
|
|
66
|
+
throw new Error("Multiple OpenCode sessions matched the launch endpoint, cwd, and creation window");
|
|
67
|
+
const match = matches[0];
|
|
68
|
+
if (match)
|
|
69
|
+
return {
|
|
70
|
+
id: match.id,
|
|
71
|
+
pid: rootPid,
|
|
72
|
+
rawName: rawName(match),
|
|
73
|
+
cwd: resolvedCwd,
|
|
74
|
+
serverUrl,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (!(error instanceof OpenCodeProtocolError))
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
const remaining = deadline - Date.now();
|
|
83
|
+
if (remaining > 0)
|
|
84
|
+
await delay(Math.min(POLL_INTERVAL_MS, remaining));
|
|
85
|
+
}
|
|
86
|
+
}
|
package/dist/muster.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { parseArgs } from "node:util";
|
|
3
4
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -12,12 +13,14 @@ const listSchema = z
|
|
|
12
13
|
.object({ kind: z.enum(["session", "task"]).optional() })
|
|
13
14
|
.strict();
|
|
14
15
|
const idSchema = z.object({ id: z.string().min(1) }).strict();
|
|
16
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
15
17
|
const help = `Usage:
|
|
16
|
-
muster run <codex|claude> --prompt TEXT [--cwd DIR] [--kind session|task] [--host ID] [--open] [--terminal APP] [-- RUNTIME_OPTIONS]
|
|
18
|
+
muster run <codex|claude|opencode> --prompt TEXT [--cwd DIR] [--kind session|task] [--host ID] [--open] [--terminal APP] [-- RUNTIME_OPTIONS]
|
|
17
19
|
muster list [--kind session|task] [--format json|human]
|
|
18
20
|
muster stop <id> [--format json|human]
|
|
19
21
|
muster output <id>
|
|
20
22
|
muster mcp
|
|
23
|
+
muster --version
|
|
21
24
|
|
|
22
25
|
run also accepts --format json|human, --permissions auto|deny|bypass,
|
|
23
26
|
and --sandbox read-only|workspace-write|full-access before -- RUNTIME_OPTIONS.
|
|
@@ -40,6 +43,10 @@ async function main() {
|
|
|
40
43
|
process.stdout.write(help);
|
|
41
44
|
return;
|
|
42
45
|
}
|
|
46
|
+
if (argv[0] === "--version" || argv[0] === "-v") {
|
|
47
|
+
process.stdout.write(`${version}\n`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
43
50
|
const mcp = argv.length === 0 || argv[0] === "mcp";
|
|
44
51
|
if (mcp && argv.length > 1)
|
|
45
52
|
throw new Error("mcp takes no arguments");
|
|
@@ -56,7 +63,7 @@ async function main() {
|
|
|
56
63
|
});
|
|
57
64
|
});
|
|
58
65
|
if (mcp) {
|
|
59
|
-
const server = new Server({ name: "muster", version
|
|
66
|
+
const server = new Server({ name: "muster", version }, { capabilities: { tools: {} } });
|
|
60
67
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
61
68
|
tools: [
|
|
62
69
|
{
|
|
@@ -65,7 +72,10 @@ async function main() {
|
|
|
65
72
|
inputSchema: {
|
|
66
73
|
type: "object",
|
|
67
74
|
properties: {
|
|
68
|
-
runtime: {
|
|
75
|
+
runtime: {
|
|
76
|
+
type: "string",
|
|
77
|
+
enum: ["codex", "claude", "opencode"],
|
|
78
|
+
},
|
|
69
79
|
prompt: { type: "string", minLength: 1 },
|
|
70
80
|
cwd: { type: "string" },
|
|
71
81
|
kind: {
|
package/dist/naming.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
export function runtimeName(runtime) {
|
|
2
|
+
return runtime === "claude" ? "claude-code" : runtime;
|
|
3
|
+
}
|
|
3
4
|
export function slugify(raw) {
|
|
4
5
|
return raw
|
|
5
6
|
.toLowerCase()
|