@brutalsystems/muster 3.1.1 → 3.2.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/CANONICAL_ID.md CHANGED
@@ -288,7 +288,7 @@ sessions; the send was a reply, so `reply_misrouted` caught it, but a new
288
288
  message would have gone through.
289
289
 
290
290
  This is not an exotic collision. It happens whenever a short name and a longer
291
- one built on it coexist — `muster` / `muster-b1`, `ferry` / `ferry-9b`.
291
+ one built on it coexist — `muster` / `muster-b1`, `atlas` / `atlas-9b`.
292
292
 
293
293
  **It is kept because it cannot be fixed from the listing alone.** The hazard is
294
294
  indistinguishable from the behaviour directly above it in this document: `auth`
@@ -513,7 +513,7 @@ The entire delta across both files is three hunks:
513
513
  - `CANONICAL_ID.md`: the normative version line, `1.9.2` → `2.0.0`.
514
514
  - `CANONICAL_ID.md`: the new paragraph described above.
515
515
  - the fixture: `tincan_version`, and one `rawName` in the prefix-collision
516
- case, `cxx-be` → `billing-api`.
516
+ case, renamed to `billing-api`.
517
517
 
518
518
  `naming.ts` does not appear and no expected value moved. The renamed peer is
519
519
  **input**, not an expected output, and that case's expected `canonicalId`
@@ -528,6 +528,19 @@ fixture moved `publish.yml`, `RELEASING.md` and `docs/verification.md` on
528
528
  their own. Only the two frozen hashes in `test/naming.test.ts` are hand-written,
529
529
  and those are the third site behaving as documented.
530
530
 
531
+ ## Local text edit — 2026-10-17
532
+
533
+ One example pair in `CANONICAL_ID.md`'s prefix-collision paragraph used a
534
+ project name from a real machine; it now reads `atlas` / `atlas-9b`. The
535
+ example illustrates the hazard and is not normative, so no rule, case or
536
+ expected value changed. The fixture is untouched.
537
+
538
+ | File | SHA-256 |
539
+ | --- | --- |
540
+ | `CANONICAL_ID.md` | `516d55ba07d191a29c68b74ea1181bc79149ab53b6c821cc9fe6dab14a78b58a` |
541
+
542
+ A future re-sync takes upstream's text for that line.
543
+
531
544
  ## What the vendored copy covers, and what it deliberately does not
532
545
 
533
546
  The copies pin **the address format Muster generates** — slugify, suffix,
package/README.md CHANGED
@@ -77,6 +77,10 @@ muster run codex --kind task --prompt 'Review' --on-exit 'notify "$MUSTER_STATE"
77
77
  # What is running, and stopping it.
78
78
  muster list --format human
79
79
  muster stop <id>
80
+
81
+ # Show a session in a terminal: terminal, iterm2, ghostty, or one you define
82
+ # under [open.apps] in config.toml (see docs/launching.md).
83
+ muster run claude --prompt 'Review this project' --open kitty
80
84
  ```
81
85
 
82
86
  **A Claude launch with no containment flag opens exactly as you would open it
package/dist/agents.js CHANGED
@@ -7,8 +7,7 @@ import { setCodexWorkspaceTrust, setWorkspaceTrust } from "./identity-copy.js";
7
7
  * child's argv twice.
8
8
  *
9
9
  * Exactly these two spellings. Anything else a caller passes after `--` is in
10
- * neither list and is refused, which is what keeps `--config=sandbox_mode=...`
11
- * an error rather than something quietly dropped.
10
+ * neither list and is refused rather than quietly dropped.
12
11
  */
13
12
  export const MODEL_FLAGS = ["--model", "-m"];
14
13
  /**
@@ -20,7 +19,10 @@ export const AGENTS = {
20
19
  claude: {
21
20
  configHomeVar: "CLAUDE_CONFIG_DIR",
22
21
  enforcement: "kernel",
23
- runtimeOptions: { forwarded: ["--effort"], consumed: MODEL_FLAGS },
22
+ runtimeOptions: {
23
+ forwarded: ["--effort", "--name"],
24
+ consumed: MODEL_FLAGS,
25
+ },
24
26
  trustWorkspace: setWorkspaceTrust,
25
27
  beforeStop: null,
26
28
  refreshMetadata: refreshClaude,
@@ -28,7 +30,10 @@ export const AGENTS = {
28
30
  codex: {
29
31
  configHomeVar: "CODEX_HOME",
30
32
  enforcement: "kernel",
31
- runtimeOptions: { forwarded: [], consumed: MODEL_FLAGS },
33
+ runtimeOptions: {
34
+ forwarded: ["--add-dir", "-c", "--sandbox"],
35
+ consumed: MODEL_FLAGS,
36
+ },
32
37
  trustWorkspace: setCodexWorkspaceTrust,
33
38
  beforeStop: null,
34
39
  refreshMetadata: refreshCodex,
package/dist/config.js CHANGED
@@ -6,6 +6,7 @@ import { z } from "zod";
6
6
  import { mcpName, mcpServerSchema } from "./mcp.js";
7
7
  import { pluginName, pluginSchema } from "./plugins.js";
8
8
  import { openCodeSectionSchema } from "./providers.js";
9
+ import { builtinTerminals } from "./hosts/types.js";
9
10
  import { identityNameSchema } from "./identity-store.js";
10
11
  import { authoritySchema, subjectSchema } from "./requester.js";
11
12
  import { RequesterConfigError } from "./requester-policy.js";
@@ -13,7 +14,7 @@ import { RequesterConfigError } from "./requester-policy.js";
13
14
  * What a remote requester is permitted here.
14
15
  *
15
16
  * `level` defaults to `normal`: an enrolled remote caller gets a launch like
16
- * the operator's own, as a local one does (Mike, 2026-10-14). Naming `read` or
17
+ * the operator's own, as a local one does. Naming `read` or
17
18
  * `work` is how a profile caps it. `min_enforcement` follows the level it is
18
19
  * written beside — `none` for a normal profile, which muster does not enforce,
19
20
  * and `kernel` for a capped one — unless the profile names it.
@@ -68,6 +69,46 @@ export const requesterEnrolmentSchema = z
68
69
  profile: z.string().min(1),
69
70
  })
70
71
  .strict();
72
+ const openerPlaceholder = /\{([^{}]*)\}/g;
73
+ const openerPlaceholders = new Set(["tmux", "server", "target"]);
74
+ /**
75
+ * An extra `--open` target: the argv to run, with `{tmux}`, `{server}` and
76
+ * `{target}` filled in per launch. An argv array and not a string, so a value
77
+ * is never re-parsed by a shell; a program that needs one says so itself, as in
78
+ * `["sh", "-c", "{tmux} -L {server} attach -t {target}"]`.
79
+ */
80
+ const openerSchema = z
81
+ .object({
82
+ command: z
83
+ .array(z.string().min(1))
84
+ .min(1)
85
+ .superRefine((argv, ctx) => {
86
+ for (const arg of argv)
87
+ for (const m of arg.matchAll(openerPlaceholder))
88
+ if (!openerPlaceholders.has(m[1]))
89
+ ctx.addIssue({
90
+ code: "custom",
91
+ message: `unknown placeholder {${m[1]}}; use {tmux}, {server} or {target}`,
92
+ });
93
+ if (!argv.some((a) => a.includes("{target}")))
94
+ ctx.addIssue({
95
+ code: "custom",
96
+ message: "command must contain {target}",
97
+ });
98
+ }),
99
+ })
100
+ .strict();
101
+ const openSectionSchema = z
102
+ .object({
103
+ apps: z
104
+ .record(z
105
+ .string()
106
+ .regex(/^[a-zA-Z0-9_-]+$/)
107
+ .refine((name) => !["auto", ...builtinTerminals].includes(name), "built-in terminal names cannot be redefined"), openerSchema)
108
+ .default({}),
109
+ })
110
+ .strict()
111
+ .default({});
71
112
  /** Raw strings, not parsed durations: a bad value in config.toml must fail with
72
113
  * the same message a bad flag does, and that parser lives in one place. */
73
114
  const sessionSectionSchema = z
@@ -83,7 +124,7 @@ const sessionSectionSchema = z
83
124
  })
84
125
  .strict()
85
126
  .default({});
86
- export const configSchema = z
127
+ const configObject = z
87
128
  .object({
88
129
  host: z.enum(["auto", "tmux", "pty", "macos-terminal"]).default("auto"),
89
130
  /**
@@ -97,9 +138,8 @@ export const configSchema = z
97
138
  * hardcoded to Terminal.app, so a developer on Ghostty or iTerm2 had to pass
98
139
  * `--terminal` on every launch with no way to state it once.
99
140
  */
100
- terminal: z
101
- .enum(["auto", "terminal", "iterm2", "ghostty"])
102
- .default("terminal"),
141
+ terminal: z.string().min(1).default("terminal"),
142
+ open: openSectionSchema,
103
143
  launch_timeout_sec: z.number().finite().positive().max(600).default(30),
104
144
  max_concurrent: z.number().int().positive().max(1000).default(4),
105
145
  allowed_roots: z.array(z.string().min(1)).default([]),
@@ -143,7 +183,18 @@ export const configSchema = z
143
183
  // stay strict for the same reason — an unknown key inside a requester
144
184
  // profile is a policy statement that would not apply.
145
185
  .strip();
146
- const knownConfigKeys = new Set(Object.keys(configSchema.shape));
186
+ export const configSchema = configObject.superRefine((config, ctx) => {
187
+ const t = config.terminal;
188
+ if (t !== "auto" &&
189
+ !builtinTerminals.includes(t) &&
190
+ !config.open.apps[t])
191
+ ctx.addIssue({
192
+ code: "custom",
193
+ path: ["terminal"],
194
+ message: `terminal "${t}" is not a built-in (${builtinTerminals.join(", ")}) or defined under [open.apps.${t}]`,
195
+ });
196
+ });
197
+ const knownConfigKeys = new Set(Object.keys(configObject.shape));
147
198
  /**
148
199
  * Top-level keys this build does not know, sorted. Reported rather than
149
200
  * refused, so a newer config degrades to "this Muster ignores that" instead of
package/dist/format.js CHANGED
@@ -88,7 +88,7 @@ function recordHuman(record, color) {
88
88
  if (record.host)
89
89
  lines.push(field("Host", `${clean(record.host)}${record.host === "pty" ? " (not watchable or attachable)" : ""}`));
90
90
  if (record.terminal_opened)
91
- lines.push(field("Terminal", `${record.terminal === "ghostty" ? "Ghostty" : record.terminal === "iterm2" ? "iTerm2" : "Terminal.app"} (opened at launch)`));
91
+ lines.push(field("Terminal", `${record.terminal === "ghostty" ? "Ghostty" : record.terminal === "iterm2" ? "iTerm2" : record.terminal && record.terminal !== "terminal" ? record.terminal : "Terminal.app"} (opened at launch)`));
92
92
  if (record.exit_code !== undefined)
93
93
  lines.push(field("Exit code", badExit ? paint(clean(record.exit_code), 31) : clean(record.exit_code)));
94
94
  if (record.signal)
package/dist/guard.js CHANGED
@@ -6,6 +6,7 @@ import { homedir } from "node:os";
6
6
  import { isAbsolute, join, relative, resolve } from "node:path";
7
7
  import TOML from "@iarna/toml";
8
8
  import { claudeConfigDir } from "./identity/claude.js";
9
+ import { builtinTerminals } from "./hosts/types.js";
9
10
  import { mcpName } from "./mcp.js";
10
11
  import { claudeMcpConfig } from "./claude-policy.js";
11
12
  import { pluginName } from "./plugins.js";
@@ -28,7 +29,10 @@ export const runSchema = z
28
29
  kind: z.enum(["session", "task"]).default("session"),
29
30
  host: z.enum(["auto", "tmux", "pty", "macos-terminal"]).optional(),
30
31
  open: z.boolean().optional(),
31
- terminal: z.enum(["auto", "terminal", "iterm2", "ghostty"]).optional(),
32
+ terminal: z
33
+ .string()
34
+ .regex(/^[a-zA-Z0-9_-]+$/)
35
+ .optional(),
32
36
  permissions: z.enum(["auto", "deny", "bypass"]).optional(),
33
37
  sandbox: z.enum(["read-only", "workspace-write", "full-access"]).optional(),
34
38
  mcp: z.array(mcpName).optional(),
@@ -431,17 +435,33 @@ export function resolveTitle(req, titleable) {
431
435
  throw new Error(`--title applies to tmux sessions; this launch is ${req.kind === "task" ? "a task" : `hosted on ${req.host ?? "the configured host"}`}`);
432
436
  return sanitiseTitle(req.title);
433
437
  }
438
+ /**
439
+ * `--sandbox` and `-c` are passed to Codex, so they must not be a way round the
440
+ * policy Muster sets: no full-access sandbox, and no config override of the
441
+ * sandbox or the approval policy.
442
+ */
443
+ function refuseWidening(flag, value) {
444
+ if (flag === "--sandbox" && /full-access/i.test(value))
445
+ throw new Error(`Runtime option refused: --sandbox ${value}`);
446
+ if (flag === "-c" && /^\s*(sandbox|approval_policy)/i.test(value))
447
+ throw new Error(`Runtime option refused: -c ${value}`);
448
+ }
434
449
  export function runtimeArgs(req) {
435
450
  const result = [];
436
451
  const { forwarded, consumed } = AGENTS[req.runtime].runtimeOptions;
437
452
  for (let i = 0; i < req.args.length; i++) {
438
453
  const arg = req.args[i], flag = normaliseFlag(arg);
439
- const keep = forwarded.includes(flag);
440
- if (!keep && !consumed.includes(flag))
454
+ // A single-dash flag is case-sensitive: codex's `-C` (working directory)
455
+ // must not pass as `-c` (config override) just because `flag` is lowercased.
456
+ const caseChanged = !arg.startsWith("--") && arg.split("=")[0] !== flag;
457
+ const keep = !caseChanged && forwarded.includes(flag);
458
+ if (caseChanged || (!keep && !consumed.includes(flag)))
441
459
  throw new Error(`Runtime option refused: ${arg}`);
442
460
  if (arg.includes("=")) {
443
- if (keep)
461
+ if (keep) {
462
+ refuseWidening(flag, arg.slice(arg.indexOf("=") + 1));
444
463
  result.push(arg);
464
+ }
445
465
  }
446
466
  else {
447
467
  // The value is stepped over for a consumed flag exactly as for a
@@ -450,8 +470,10 @@ export function runtimeArgs(req) {
450
470
  const value = req.args[++i];
451
471
  if (!value || value.startsWith("-"))
452
472
  throw new Error(`Missing value for ${arg}`);
453
- if (keep)
473
+ if (keep) {
474
+ refuseWidening(flag, value);
454
475
  result.push(arg, value);
476
+ }
455
477
  }
456
478
  }
457
479
  return result;
@@ -542,9 +564,15 @@ export function normalizeOpenFlag(argv) {
542
564
  * app, so it defers to config exactly as an absent value does.
543
565
  */
544
566
  export function resolveTerminal(req, config) {
545
- if (req.terminal && req.terminal !== "auto")
546
- return req.terminal;
547
- return config.terminal === "auto" ? "terminal" : config.terminal;
567
+ const name = req.terminal && req.terminal !== "auto"
568
+ ? req.terminal
569
+ : config.terminal === "auto"
570
+ ? "terminal"
571
+ : config.terminal;
572
+ if (!builtinTerminals.includes(name) &&
573
+ !config.open.apps[name])
574
+ throw new Error(`Unknown terminal "${name}": use ${builtinTerminals.join(", ")}, or define it under [open.apps.${name}] in config.toml`);
575
+ return name;
548
576
  }
549
577
  export function launchArgs(req, config, mcp = [], bridgePath, env = process.env,
550
578
  /** Muster's own home, write-protected at workspace-write (#44). */
package/dist/help.js CHANGED
@@ -45,7 +45,7 @@ export const COMMANDS = [
45
45
  ["--identity NAME", "run as a configured identity"],
46
46
  [
47
47
  "--open [APP]",
48
- "display the tmux session in a terminal (macOS); APP is terminal, iterm2 or ghostty, defaulting to the configured terminal",
48
+ 'display the tmux session in a terminal; APP is terminal, iterm2, ghostty (macOS) or a name from [open.apps] in config.toml, defaulting to the configured terminal. An app is an argv template, e.g. [open.apps.kitty] command = ["kitty", "-e", "{tmux}", "-L", "{server}", "attach-session", "-t", "{target}"]',
49
49
  ],
50
50
  [
51
51
  "--terminal APP",
@@ -1,9 +1,13 @@
1
1
  import { access, realpath } from "node:fs/promises";
2
+ import { spawn } from "node:child_process";
2
3
  import { homedir } from "node:os";
4
+ import { builtinTerminals } from "./types.js";
3
5
  import { constants } from "node:fs";
4
6
  import { delimiter, join } from "node:path";
5
7
  import { command } from "../identity/processes.js";
6
- export async function terminalAvailable(platform = process.platform, terminal = "terminal") {
8
+ export async function terminalAvailable(platform = process.platform, terminal = "terminal", opener) {
9
+ if (!isBuiltin(terminal))
10
+ return opener ? openerAvailable(opener) : false;
7
11
  if (platform !== "darwin")
8
12
  return false;
9
13
  try {
@@ -31,6 +35,63 @@ export async function terminalAvailable(platform = process.platform, terminal =
31
35
  return false;
32
36
  }
33
37
  }
38
+ function isBuiltin(terminal) {
39
+ return builtinTerminals.includes(terminal);
40
+ }
41
+ /** The command a configured opener runs: an absolute path, or a name on PATH. */
42
+ async function openerExecutable(file) {
43
+ const dirs = file.includes("/")
44
+ ? [""]
45
+ : (process.env.PATH ?? "").split(delimiter).filter(Boolean);
46
+ for (const dir of dirs) {
47
+ const path = file.includes("/") ? file : join(dir, file);
48
+ try {
49
+ await access(path, constants.X_OK);
50
+ return path;
51
+ }
52
+ catch { }
53
+ }
54
+ }
55
+ async function openerAvailable(opener) {
56
+ return (await openerExecutable(opener[0])) !== undefined;
57
+ }
58
+ /**
59
+ * A configured opener's argv with the placeholders filled in. The values are
60
+ * validated by the caller; each argv element stays one element, so nothing here
61
+ * is parsed by a shell.
62
+ */
63
+ export function openerArgv(opener, values) {
64
+ return opener.map((arg) => arg.replace(/\{(tmux|server|target)\}/g, (_, key) => values[key]));
65
+ }
66
+ /**
67
+ * Starts a configured opener without waiting for it to finish: a terminal that
68
+ * stays in the foreground (`kitty -e …`) runs until its window closes, so
69
+ * waiting would kill it at the timeout. Success is a clean spawn that has not
70
+ * failed within a short grace; a spawn error or a non-zero exit inside the
71
+ * grace is reported.
72
+ */
73
+ function startOpener(name, file, args, timeout) {
74
+ const grace = Math.min(1500, timeout);
75
+ return new Promise((resolve, reject) => {
76
+ const child = spawn(file, args, { detached: true, stdio: "ignore" });
77
+ const fail = (why) => reject(new Error(`Opener "${name}" failed: ${why}`));
78
+ const timer = setTimeout(() => {
79
+ child.unref();
80
+ resolve();
81
+ }, grace);
82
+ child.once("error", (e) => {
83
+ clearTimeout(timer);
84
+ fail(e.message);
85
+ });
86
+ child.once("exit", (code, signal) => {
87
+ clearTimeout(timer);
88
+ if (code === 0)
89
+ resolve();
90
+ else
91
+ fail(signal ? `killed by ${signal}` : `exit ${code}`);
92
+ });
93
+ });
94
+ }
34
95
  export async function tmuxExecutable() {
35
96
  for (const dir of (process.env.PATH ?? "").split(delimiter)) {
36
97
  if (!dir)
@@ -44,12 +105,26 @@ export async function tmuxExecutable() {
44
105
  }
45
106
  throw new Error("Cannot locate tmux executable for terminal viewer");
46
107
  }
47
- export async function openTerminal(tmux, server, hostRef, deadline, terminal = "terminal") {
108
+ export async function openTerminal(tmux, server, hostRef, deadline, terminal = "terminal", opener) {
48
109
  if (!/^[a-zA-Z0-9_-]+$/.test(server) || !/^@\d+$/.test(hostRef))
49
110
  throw new Error("Invalid tmux terminal target");
50
111
  if (Date.now() >= deadline)
51
112
  throw new Error("Launch deadline expired before opening terminal");
52
113
  const timeout = Math.max(1, Math.min(10000, deadline - Date.now()));
114
+ if (!isBuiltin(terminal)) {
115
+ if (!opener)
116
+ throw new Error(`No opener configured for terminal "${terminal}"`);
117
+ const [file, ...args] = openerArgv(opener, {
118
+ tmux,
119
+ server,
120
+ target: hostRef,
121
+ });
122
+ const found = await openerExecutable(file);
123
+ if (!found)
124
+ throw new Error(`Opener "${terminal}": cannot find command "${file}"`);
125
+ await startOpener(terminal, found, args, timeout);
126
+ return;
127
+ }
53
128
  if (terminal === "ghostty") {
54
129
  await command("/usr/bin/open", [
55
130
  "-na",
@@ -176,11 +176,11 @@ export class TmuxHost {
176
176
  return false;
177
177
  }
178
178
  }
179
- openAvailable(terminal = "terminal") {
180
- return terminalAvailable(process.platform, terminal);
179
+ openAvailable(terminal = "terminal", opener) {
180
+ return terminalAvailable(process.platform, terminal, opener);
181
181
  }
182
- async open(hostRef, deadline, terminal = "terminal") {
183
- await openTerminal(await tmuxExecutable(), this.server, hostRef, deadline, terminal);
182
+ async open(hostRef, deadline, terminal = "terminal", opener) {
183
+ await openTerminal(await tmuxExecutable(), this.server, hostRef, deadline, terminal, opener);
184
184
  }
185
185
  capabilities() {
186
186
  return { watchable: true, attachable: true, survivesParentExit: true };
@@ -1 +1 @@
1
- export {};
1
+ export const builtinTerminals = ["terminal", "iterm2", "ghostty"];
package/dist/mcp-tools.js CHANGED
@@ -38,8 +38,7 @@ export const MCP_TOOLS = [
38
38
  },
39
39
  terminal: {
40
40
  type: "string",
41
- enum: ["auto", "terminal", "iterm2", "ghostty"],
42
- description: "Viewer app; requires open. Auto uses Terminal.app.",
41
+ description: "Viewer app; requires open. terminal, iterm2, ghostty, or a name defined under [open.apps] in config. Auto uses the configured default.",
43
42
  },
44
43
  permissions: {
45
44
  type: "string",
package/dist/run.js CHANGED
@@ -404,9 +404,12 @@ export class Muster {
404
404
  const driver = req.kind === "session"
405
405
  ? await selectHost(req.open ? "tmux" : requestedHost, this.drivers)
406
406
  : undefined;
407
+ const opener = this.config.open.apps[terminal]?.command;
407
408
  if (req.open &&
408
- (!driver?.open || !(await driver.openAvailable?.(terminal))))
409
- throw new Error(`--open requires the selected terminal (${terminal}) installed on macOS and tmux`);
409
+ (!driver?.open || !(await driver.openAvailable?.(terminal, opener))))
410
+ throw new Error(opener
411
+ ? `--open: the command for "${terminal}" was not found (${opener[0]}); check [open.apps.${terminal}] in config.toml, and that tmux is installed`
412
+ : `--open requires the selected terminal (${terminal}) installed on macOS and tmux`);
410
413
  // Managed policy is refused because muster cannot prove its own inline
411
414
  // settings win over it. A normal launch has no settings of muster's to
412
415
  // prove, and obeys managed policy exactly as a hand launch does.
@@ -865,7 +868,7 @@ export class Muster {
865
868
  };
866
869
  if (req.open) {
867
870
  openingTerminal = true;
868
- await driver.open(launched.hostRef, deadline, terminal);
871
+ await driver.open(launched.hostRef, deadline, terminal, this.config.open.apps[terminal]?.command);
869
872
  peer.terminal_opened = true;
870
873
  peer.terminal = terminal;
871
874
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brutalsystems/muster",
3
- "version": "3.1.1",
3
+ "version": "3.2.0",
4
4
  "description": "Launch instructed, reachable local agent sessions",
5
5
  "type": "module",
6
6
  "engines": {
@@ -14,6 +14,7 @@
14
14
  "test": "vitest run",
15
15
  "typecheck": "tsc -p tsconfig.check.json",
16
16
  "check": "npm run typecheck && npm test && node scripts/check-tarball.mjs",
17
+ "check:public": "scripts/check-public.sh --tree",
17
18
  "test:contract": "MUSTER_CONTRACT=1 vitest run test/contract.test.ts",
18
19
  "postinstall": "node scripts/prepare-pty.mjs",
19
20
  "pretest": "npm run build",