@brutalsystems/muster 0.5.0 → 0.7.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/CONTRACT_PROVENANCE.md +69 -0
- package/README.md +146 -28
- package/dist/codex-policy.js +39 -13
- package/dist/config.js +3 -0
- package/dist/format.js +6 -0
- package/dist/guard.js +18 -8
- package/dist/hosts/terminal.js +7 -6
- package/dist/hosts/tmux.js +12 -14
- package/dist/identity/opencode.js +86 -0
- package/dist/mcp-bridge.js +91 -0
- package/dist/mcp.js +207 -0
- package/dist/muster.js +22 -4
- 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 +130 -18
- package/dist/task-worker.js +2 -0
- package/package.json +5 -2
- package/test/fixtures/canonical-id.json +2 -2
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Claude has permission rules, but no MCP exposure allowlist. This stdio
|
|
2
|
+
// connection exposes and dispatches only the operator-selected tools, including
|
|
3
|
+
// when the upstream server changes its tool catalog after launch.
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
8
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import { mcpServerSchema, mcpName, mcpTransport } from "./mcp.js";
|
|
10
|
+
async function main() {
|
|
11
|
+
const [path, name] = process.argv.slice(2);
|
|
12
|
+
if (!path || !name || !mcpName.safeParse(name).success)
|
|
13
|
+
throw new Error("Invalid MCP bridge arguments");
|
|
14
|
+
const definitions = JSON.parse(await readFile(path, "utf8"));
|
|
15
|
+
const config = mcpServerSchema.parse(definitions[name]);
|
|
16
|
+
const client = new Client({ name: "muster-tool-bridge", version: "1" }, { capabilities: {} });
|
|
17
|
+
const server = new Server({ name: `muster-${name}`, version: "1" }, { capabilities: { tools: {} } });
|
|
18
|
+
let shutdown;
|
|
19
|
+
const close = () => (shutdown ??= Promise.resolve().then(async () => {
|
|
20
|
+
await client.close().catch(() => { });
|
|
21
|
+
await server.close().catch(() => { });
|
|
22
|
+
}));
|
|
23
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"])
|
|
24
|
+
process.once(signal, () => {
|
|
25
|
+
void close().finally(() => process.exit(0));
|
|
26
|
+
});
|
|
27
|
+
process.stdin.once("end", () => {
|
|
28
|
+
void close();
|
|
29
|
+
});
|
|
30
|
+
process.stdin.once("close", () => {
|
|
31
|
+
void close();
|
|
32
|
+
});
|
|
33
|
+
server.onclose = () => {
|
|
34
|
+
void close();
|
|
35
|
+
};
|
|
36
|
+
client.onclose = () => {
|
|
37
|
+
void close();
|
|
38
|
+
};
|
|
39
|
+
const transport = mcpTransport(config, process.env, process.cwd());
|
|
40
|
+
const timer = setTimeout(() => {
|
|
41
|
+
void close().finally(() => process.exit(1));
|
|
42
|
+
}, config.startup_timeout_sec * 1000);
|
|
43
|
+
try {
|
|
44
|
+
await client.connect(transport);
|
|
45
|
+
if (shutdown) {
|
|
46
|
+
await close();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
await close();
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
}
|
|
57
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
58
|
+
const tools = [];
|
|
59
|
+
let cursor;
|
|
60
|
+
do {
|
|
61
|
+
const page = await client.listTools(cursor ? { cursor } : {});
|
|
62
|
+
tools.push(...page.tools.filter((t) => config.tools.includes(t.name)));
|
|
63
|
+
cursor = page.nextCursor;
|
|
64
|
+
} while (cursor);
|
|
65
|
+
return { tools };
|
|
66
|
+
});
|
|
67
|
+
server.setRequestHandler(CallToolRequestSchema, async ({ params }) => {
|
|
68
|
+
if (!config.tools.includes(params.name))
|
|
69
|
+
return {
|
|
70
|
+
isError: true,
|
|
71
|
+
content: [
|
|
72
|
+
{ type: "text", text: "Tool not enabled for this Muster launch" },
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
return await client.callTool({
|
|
76
|
+
name: params.name,
|
|
77
|
+
arguments: params.arguments,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
try {
|
|
81
|
+
await server.connect(new StdioServerTransport());
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
await close();
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
main().catch(() => {
|
|
89
|
+
process.stderr.write("Muster MCP connection failed; check the selected server configuration.\n");
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
});
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
5
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
6
|
+
export const mcpName = z.string().regex(/^[a-zA-Z0-9_-]+$/);
|
|
7
|
+
const environmentName = z.string().regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/);
|
|
8
|
+
export const mcpServerSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
command: z.string().min(1).optional(),
|
|
11
|
+
args: z.array(z.string()).default([]),
|
|
12
|
+
url: z
|
|
13
|
+
.string()
|
|
14
|
+
.url()
|
|
15
|
+
.refine((s) => /^https?:\/\//.test(s), "MCP URL must use HTTP(S)")
|
|
16
|
+
.optional(),
|
|
17
|
+
env: z.record(z.string()).default({}),
|
|
18
|
+
env_vars: z.array(environmentName).default([]),
|
|
19
|
+
bearer_token_env_var: environmentName.optional(),
|
|
20
|
+
tools: z.array(mcpName).min(1),
|
|
21
|
+
required: z.boolean().default(false),
|
|
22
|
+
startup_timeout_sec: z.number().positive().max(60).default(10),
|
|
23
|
+
})
|
|
24
|
+
.strict()
|
|
25
|
+
.superRefine((v, ctx) => {
|
|
26
|
+
if (!!v.command === !!v.url)
|
|
27
|
+
ctx.addIssue({
|
|
28
|
+
code: "custom",
|
|
29
|
+
message: "MCP server requires exactly one of command or url",
|
|
30
|
+
});
|
|
31
|
+
if (v.command && v.bearer_token_env_var)
|
|
32
|
+
ctx.addIssue({
|
|
33
|
+
code: "custom",
|
|
34
|
+
message: "bearer_token_env_var requires an HTTP server",
|
|
35
|
+
});
|
|
36
|
+
if (v.url &&
|
|
37
|
+
(v.args.length || Object.keys(v.env).length || v.env_vars.length))
|
|
38
|
+
ctx.addIssue({
|
|
39
|
+
code: "custom",
|
|
40
|
+
message: "args, env and env_vars require a stdio server",
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
export function selectMcp(req, config) {
|
|
44
|
+
// Personal defaults apply to sessions, not unattended one-shot tasks.
|
|
45
|
+
const names = req.mcp ?? (req.kind === "session" ? config.default_mcp : []);
|
|
46
|
+
return [...new Set(names)].map((name) => {
|
|
47
|
+
const server = Object.hasOwn(config.mcp_servers, name)
|
|
48
|
+
? config.mcp_servers[name]
|
|
49
|
+
: undefined;
|
|
50
|
+
if (!server)
|
|
51
|
+
throw new Error(`Unknown MCP server: ${name}`);
|
|
52
|
+
return {
|
|
53
|
+
name,
|
|
54
|
+
config: server,
|
|
55
|
+
required: req.mcp !== undefined || server.required,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export function mcpEnvironment(servers, base, source) {
|
|
60
|
+
const env = { ...base };
|
|
61
|
+
for (const { config } of servers) {
|
|
62
|
+
for (const key of [
|
|
63
|
+
...config.env_vars,
|
|
64
|
+
...(config.bearer_token_env_var ? [config.bearer_token_env_var] : []),
|
|
65
|
+
]) {
|
|
66
|
+
if (source[key] !== undefined)
|
|
67
|
+
env[key] = source[key];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return env;
|
|
71
|
+
}
|
|
72
|
+
function serverEnv(config, env) {
|
|
73
|
+
const result = { ...config.env };
|
|
74
|
+
for (const key of config.env_vars) {
|
|
75
|
+
if (env[key] === undefined)
|
|
76
|
+
throw new Error(`Missing MCP environment variable: ${key}`);
|
|
77
|
+
result[key] = env[key];
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
function headers(config, env) {
|
|
82
|
+
if (!config.bearer_token_env_var)
|
|
83
|
+
return {};
|
|
84
|
+
const value = env[config.bearer_token_env_var];
|
|
85
|
+
if (!value)
|
|
86
|
+
throw new Error("Missing MCP bearer token environment variable");
|
|
87
|
+
return { Authorization: `Bearer ${value}` };
|
|
88
|
+
}
|
|
89
|
+
export function mcpTransport(config, env, cwd) {
|
|
90
|
+
const transport = config.command
|
|
91
|
+
? new StdioClientTransport({
|
|
92
|
+
command: config.command,
|
|
93
|
+
args: config.args,
|
|
94
|
+
env: { ...env, ...serverEnv(config, env) },
|
|
95
|
+
cwd,
|
|
96
|
+
stderr: "pipe",
|
|
97
|
+
})
|
|
98
|
+
: new StreamableHTTPClientTransport(new URL(config.url), {
|
|
99
|
+
requestInit: { headers: headers(config, env) },
|
|
100
|
+
});
|
|
101
|
+
if (transport instanceof StdioClientTransport)
|
|
102
|
+
transport.stderr?.on("data", () => { });
|
|
103
|
+
return transport;
|
|
104
|
+
}
|
|
105
|
+
export async function prepareMcp(selected, env, cwd, deadline) {
|
|
106
|
+
const servers = [], warnings = [];
|
|
107
|
+
for (const server of selected) {
|
|
108
|
+
const client = new Client({ name: "muster-preflight", version: "1" }, { capabilities: {} });
|
|
109
|
+
let timer;
|
|
110
|
+
let diagnostic = "startup or tool discovery failed (check executable, credentials and server configuration)";
|
|
111
|
+
try {
|
|
112
|
+
const timeout = Math.min(server.config.startup_timeout_sec * 1000, deadline - Date.now());
|
|
113
|
+
if (timeout <= 0)
|
|
114
|
+
throw new Error("deadline");
|
|
115
|
+
const transport = mcpTransport(server.config, env, cwd);
|
|
116
|
+
const discover = async () => {
|
|
117
|
+
await client.connect(transport);
|
|
118
|
+
const names = new Set();
|
|
119
|
+
let cursor;
|
|
120
|
+
do {
|
|
121
|
+
const page = await client.listTools(cursor ? { cursor } : {});
|
|
122
|
+
for (const tool of page.tools) {
|
|
123
|
+
if (!mcpName.safeParse(tool.name).success)
|
|
124
|
+
throw new Error("Unsupported tool name");
|
|
125
|
+
names.add(tool.name);
|
|
126
|
+
}
|
|
127
|
+
cursor = page.nextCursor;
|
|
128
|
+
} while (cursor);
|
|
129
|
+
const missing = server.config.tools.filter((name) => !names.has(name));
|
|
130
|
+
if (missing.length) {
|
|
131
|
+
diagnostic = `configured tools not found: ${missing.join(", ")}`;
|
|
132
|
+
throw new Error("missing tools");
|
|
133
|
+
}
|
|
134
|
+
return [...names].filter((name) => !server.config.tools.includes(name));
|
|
135
|
+
};
|
|
136
|
+
const excludedTools = await Promise.race([
|
|
137
|
+
discover(),
|
|
138
|
+
new Promise((_, reject) => {
|
|
139
|
+
timer = setTimeout(() => reject(new Error("timeout")), timeout);
|
|
140
|
+
}),
|
|
141
|
+
]);
|
|
142
|
+
servers.push({ ...server, excludedTools });
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Server errors may include tokens or environment values; never log them.
|
|
146
|
+
const message = `MCP server ${server.name}: ${diagnostic}`;
|
|
147
|
+
if (server.required)
|
|
148
|
+
throw new Error(message);
|
|
149
|
+
warnings.push(`${message}; skipped optional default`);
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
if (timer)
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
await client.close().catch(() => { });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
servers,
|
|
159
|
+
summary: {
|
|
160
|
+
mcp: servers.map((s) => s.name),
|
|
161
|
+
...(warnings.length ? { mcp_warnings: warnings } : {}),
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export function codexMcpConfig(server) {
|
|
166
|
+
const c = server.config;
|
|
167
|
+
return {
|
|
168
|
+
...(c.command
|
|
169
|
+
? { command: c.command, args: c.args, env: c.env, env_vars: c.env_vars }
|
|
170
|
+
: {
|
|
171
|
+
url: c.url,
|
|
172
|
+
...(c.bearer_token_env_var
|
|
173
|
+
? { bearer_token_env_var: c.bearer_token_env_var }
|
|
174
|
+
: {}),
|
|
175
|
+
}),
|
|
176
|
+
enabled: true,
|
|
177
|
+
required: server.required,
|
|
178
|
+
enabled_tools: c.tools,
|
|
179
|
+
disabled_tools: [],
|
|
180
|
+
startup_timeout_sec: c.startup_timeout_sec,
|
|
181
|
+
default_tools_approval_mode: "approve",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
export function claudeMcpConfig(servers, bridgePath) {
|
|
185
|
+
if (servers.length && !bridgePath)
|
|
186
|
+
throw new Error("Missing MCP bridge configuration");
|
|
187
|
+
return {
|
|
188
|
+
mcpServers: Object.fromEntries(servers.map(({ name, config }) => [
|
|
189
|
+
name,
|
|
190
|
+
{
|
|
191
|
+
type: "stdio",
|
|
192
|
+
command: process.execPath,
|
|
193
|
+
args: [
|
|
194
|
+
fileURLToPath(new URL("../dist/mcp-bridge.js", import.meta.url)),
|
|
195
|
+
bridgePath,
|
|
196
|
+
name,
|
|
197
|
+
],
|
|
198
|
+
env: Object.fromEntries([
|
|
199
|
+
...config.env_vars,
|
|
200
|
+
...(config.bearer_token_env_var
|
|
201
|
+
? [config.bearer_token_env_var]
|
|
202
|
+
: []),
|
|
203
|
+
].map((key) => [key, "${" + key + "}"])),
|
|
204
|
+
},
|
|
205
|
+
])),
|
|
206
|
+
};
|
|
207
|
+
}
|
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] [--terminal APP] [-- RUNTIME_OPTIONS]
|
|
16
|
+
muster run <codex|claude|opencode> --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>
|
|
@@ -23,6 +23,8 @@ run also accepts --format json|human, --permissions auto|deny|bypass,
|
|
|
23
23
|
and --sandbox read-only|workspace-write|full-access before -- RUNTIME_OPTIONS.
|
|
24
24
|
--open displays a tmux session on macOS. --terminal accepts auto (Terminal.app),
|
|
25
25
|
terminal, iterm2, or ghostty, and requires --open.
|
|
26
|
+
--mcp NAME is repeatable and replaces configured session defaults; --no-mcp
|
|
27
|
+
clears them. Tasks use no MCP defaults; select servers explicitly.
|
|
26
28
|
run, list and stop default to JSON. output prints raw task text.
|
|
27
29
|
|
|
28
30
|
pty launches print their record and keep this process running. Ctrl-C stops the
|
|
@@ -54,7 +56,7 @@ async function main() {
|
|
|
54
56
|
});
|
|
55
57
|
});
|
|
56
58
|
if (mcp) {
|
|
57
|
-
const server = new Server({ name: "muster", version: "0.
|
|
59
|
+
const server = new Server({ name: "muster", version: "0.6.0" }, { capabilities: { tools: {} } });
|
|
58
60
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
59
61
|
tools: [
|
|
60
62
|
{
|
|
@@ -63,7 +65,10 @@ async function main() {
|
|
|
63
65
|
inputSchema: {
|
|
64
66
|
type: "object",
|
|
65
67
|
properties: {
|
|
66
|
-
runtime: {
|
|
68
|
+
runtime: {
|
|
69
|
+
type: "string",
|
|
70
|
+
enum: ["codex", "claude", "opencode"],
|
|
71
|
+
},
|
|
67
72
|
prompt: { type: "string", minLength: 1 },
|
|
68
73
|
cwd: { type: "string" },
|
|
69
74
|
kind: {
|
|
@@ -94,6 +99,11 @@ async function main() {
|
|
|
94
99
|
enum: ["read-only", "workspace-write", "full-access"],
|
|
95
100
|
description: "Defaults to config (read-only). Full access requires config authorization.",
|
|
96
101
|
},
|
|
102
|
+
mcp: {
|
|
103
|
+
type: "array",
|
|
104
|
+
items: { type: "string", pattern: "^[a-zA-Z0-9_-]+$" },
|
|
105
|
+
description: "Configured MCP server names. Omit for session defaults; [] disables all. Tasks have no defaults.",
|
|
106
|
+
},
|
|
97
107
|
args: { type: "array", items: { type: "string" } },
|
|
98
108
|
},
|
|
99
109
|
required: ["runtime", "prompt"],
|
|
@@ -180,9 +190,17 @@ async function main() {
|
|
|
180
190
|
format: { type: "string" },
|
|
181
191
|
permissions: { type: "string" },
|
|
182
192
|
sandbox: { type: "string" },
|
|
193
|
+
mcp: { type: "string", multiple: true },
|
|
194
|
+
"no-mcp": { type: "boolean" },
|
|
183
195
|
},
|
|
184
196
|
});
|
|
185
|
-
const { format = "json", ...values } = parsed;
|
|
197
|
+
const { format = "json", "no-mcp": noMcp, ...values } = parsed;
|
|
198
|
+
if (noMcp !== undefined && command !== "run")
|
|
199
|
+
throw new Error("--no-mcp applies only to run");
|
|
200
|
+
if (noMcp && values.mcp)
|
|
201
|
+
throw new Error("--mcp and --no-mcp cannot be combined");
|
|
202
|
+
if (noMcp)
|
|
203
|
+
values.mcp = [];
|
|
186
204
|
if (format !== "json" && format !== "human")
|
|
187
205
|
throw new Error("format must be json or human");
|
|
188
206
|
if (command === "output" && parsed.format !== undefined)
|
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()
|