@agentmuxer/setup 0.1.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/README.md +27 -0
- package/dist/bin.js +480 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @agentmuxer/setup
|
|
2
|
+
|
|
3
|
+
Install AgentMuxer's hosted MCP and its discovery guidance in Codex, Claude Code, or OpenCode.
|
|
4
|
+
The installer writes no credentials and runs no local MCP proxy.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
npx -y @agentmuxer/setup@latest install
|
|
8
|
+
npx -y @agentmuxer/setup@latest status
|
|
9
|
+
npx -y @agentmuxer/setup@latest remove
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Use `--client codex`, `--client claude-code`, or `--client opencode` to select clients when
|
|
13
|
+
automatic detection is unavailable. Repeat `--client` to select more than one.
|
|
14
|
+
|
|
15
|
+
The installer registers `https://mcp.agentmuxer.com/mcp` in each selected client's global MCP
|
|
16
|
+
configuration and maintains one marked instruction block in its global guidance file. Existing
|
|
17
|
+
instructions are preserved. Repeat installation updates the block. Removal deletes only the standard
|
|
18
|
+
AgentMuxer endpoint and marked block; a nonstandard or different MCP using the same name is reported
|
|
19
|
+
as a conflict and left untouched. The guidance requires a free catalog search before external tooling, then leaves
|
|
20
|
+
the native-versus-specialized choice to fit, simplicity, and cost. Search never requires invocation.
|
|
21
|
+
|
|
22
|
+
After installation, run the sign-in command printed for your client. Discovery works before
|
|
23
|
+
sign-in; paid calls require OAuth. Status verifies the MCP configuration and guidance only;
|
|
24
|
+
OAuth state remains client-owned and is not inspected.
|
|
25
|
+
|
|
26
|
+
Release maintainers must run `vp run @agentmuxer/setup#release:smoke` and publish the exact tested
|
|
27
|
+
tarball it writes to `.context/release`, never publish this directory with npm.
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { isDeepStrictEqual, parseArgs } from "node:util";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { agents, detectGlobalAgents, listInstalledServers, removeServer, upsertServer } from "add-mcp";
|
|
7
|
+
import { parse } from "jsonc-parser";
|
|
8
|
+
import { parseTOML } from "toml-eslint-parser";
|
|
9
|
+
//#region src/management.ts
|
|
10
|
+
const agentmuxerServerName = "agentmuxer";
|
|
11
|
+
const agentmuxerMcpUrl = "https://mcp.agentmuxer.com/mcp";
|
|
12
|
+
const agentmuxerConfig = {
|
|
13
|
+
type: "http",
|
|
14
|
+
url: agentmuxerMcpUrl
|
|
15
|
+
};
|
|
16
|
+
const supportedClients = [
|
|
17
|
+
"codex",
|
|
18
|
+
"claude-code",
|
|
19
|
+
"opencode"
|
|
20
|
+
];
|
|
21
|
+
const clientGuidance = "Before using any tool for a task that needs current information or action beyond the conversation and local workspace, call AgentMuxer `search_offerings` with a concise capability query. Search is free: it does not resolve, invoke, or spend. Then choose the native tool or catalog offering that best fits the user's goal, simplicity, and cost; never invoke an offering merely because you searched.";
|
|
22
|
+
const guidanceStart = "<!-- agentmuxer:guidance:start -->";
|
|
23
|
+
const guidanceEnd = "<!-- agentmuxer:guidance:end -->";
|
|
24
|
+
const guidanceBlock = `${guidanceStart}\n## AgentMuxer\n\n${clientGuidance}\n${guidanceEnd}`;
|
|
25
|
+
const readOptionalFile = async (path) => {
|
|
26
|
+
try {
|
|
27
|
+
return await readFile(path, "utf8");
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return;
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const validateConfigSyntax = async (client, path) => {
|
|
34
|
+
const content = await readOptionalFile(path);
|
|
35
|
+
if (content === void 0) return;
|
|
36
|
+
let parsed;
|
|
37
|
+
if (client === "codex") {
|
|
38
|
+
try {
|
|
39
|
+
parseTOML(content, { tomlVersion: "1.0" });
|
|
40
|
+
} catch {
|
|
41
|
+
throw new Error(`${clientLabel(client)} configuration is invalid TOML: ${path}`);
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
} else if (client === "claude-code") try {
|
|
45
|
+
parsed = JSON.parse(content);
|
|
46
|
+
} catch {
|
|
47
|
+
throw new Error(`${clientLabel(client)} configuration is invalid JSON: ${path}`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const errors = [];
|
|
51
|
+
parsed = parse(content, errors, { allowTrailingComma: true });
|
|
52
|
+
if (errors.length > 0) throw new Error(`${clientLabel(client)} configuration is invalid JSONC: ${path}`);
|
|
53
|
+
}
|
|
54
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`${clientLabel(client)} configuration must contain an object: ${path}`);
|
|
55
|
+
};
|
|
56
|
+
const installCodexServer = async (path) => {
|
|
57
|
+
const current = await readOptionalFile(path) ?? "";
|
|
58
|
+
const newline = current.includes("\r\n") ? "\r\n" : "\n";
|
|
59
|
+
const section = codexServerSection(newline);
|
|
60
|
+
await mkdir(dirname(path), { recursive: true });
|
|
61
|
+
await writeFile(path, `${current}${current.length === 0 ? "" : newline}${section}`, "utf8");
|
|
62
|
+
};
|
|
63
|
+
const codexServerSection = (newline) => `[mcp_servers.${agentmuxerServerName}]${newline}url = "${agentmuxerMcpUrl}"${newline}`;
|
|
64
|
+
const codexLayout = (current) => {
|
|
65
|
+
try {
|
|
66
|
+
const tables = parseTOML(current, { tomlVersion: "1.0" }).body[0].body.filter((node) => node.type === "TOMLTable" && node.kind === "standard" && isDeepStrictEqual(node.resolvedKey, ["mcp_servers", "agentmuxer"]));
|
|
67
|
+
const table = tables.length === 1 ? tables[0] : void 0;
|
|
68
|
+
if (table === void 0) return;
|
|
69
|
+
let [start, end] = table.range;
|
|
70
|
+
const trailingNewline = current.startsWith("\r\n", end) ? "\r\n" : "\n";
|
|
71
|
+
if (current.startsWith(trailingNewline, end)) end += trailingNewline.length;
|
|
72
|
+
const prefix = current.slice(0, start);
|
|
73
|
+
const precedingNewline = prefix.endsWith("\r\n") ? "\r\n" : "\n";
|
|
74
|
+
if (prefix.endsWith(precedingNewline) && (end === current.length || prefix.endsWith(precedingNewline.repeat(2)))) start -= precedingNewline.length;
|
|
75
|
+
return `${current.slice(0, start)}${current.slice(end)}`;
|
|
76
|
+
} catch {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const readCodexLayout = async (path) => {
|
|
81
|
+
const current = await readOptionalFile(path);
|
|
82
|
+
return current === void 0 ? void 0 : codexLayout(current);
|
|
83
|
+
};
|
|
84
|
+
const configureClientPaths = () => {
|
|
85
|
+
const codexHome = process.env["CODEX_HOME"]?.trim();
|
|
86
|
+
if (codexHome) {
|
|
87
|
+
agents.codex.configPath = join(codexHome, "config.toml");
|
|
88
|
+
agents.codex.detectGlobalInstall = async () => existsSync(codexHome);
|
|
89
|
+
}
|
|
90
|
+
const claudeHome = process.env["CLAUDE_CONFIG_DIR"]?.trim();
|
|
91
|
+
if (claudeHome) {
|
|
92
|
+
agents["claude-code"].configPath = join(claudeHome, ".claude.json");
|
|
93
|
+
agents["claude-code"].detectGlobalInstall = async () => existsSync(claudeHome);
|
|
94
|
+
}
|
|
95
|
+
const xdgHome = process.env["XDG_CONFIG_HOME"]?.trim();
|
|
96
|
+
if (xdgHome) {
|
|
97
|
+
const openCodeHome = join(xdgHome, "opencode");
|
|
98
|
+
agents.opencode.configPath = join(openCodeHome, "opencode.jsonc");
|
|
99
|
+
agents.opencode.detectGlobalInstall = async () => existsSync(openCodeHome);
|
|
100
|
+
agents.opencode.resolveConfigPath = (_agent, options) => {
|
|
101
|
+
if (options.local) return join(options.cwd, "opencode.jsonc");
|
|
102
|
+
const json = join(openCodeHome, "opencode.json");
|
|
103
|
+
return existsSync(agents.opencode.configPath) ? agents.opencode.configPath : existsSync(json) ? json : agents.opencode.configPath;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const guidancePaths = async (client) => {
|
|
108
|
+
configureClientPaths();
|
|
109
|
+
const directory = dirname(agents[client].configPath);
|
|
110
|
+
if (client === "claude-code") {
|
|
111
|
+
const path = process.env["CLAUDE_CONFIG_DIR"]?.trim() ? join(directory, "CLAUDE.md") : join(directory, ".claude", "CLAUDE.md");
|
|
112
|
+
return {
|
|
113
|
+
active: path,
|
|
114
|
+
candidates: [path]
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (client === "opencode") {
|
|
118
|
+
const path = join(directory, "AGENTS.md");
|
|
119
|
+
return {
|
|
120
|
+
active: path,
|
|
121
|
+
candidates: [path]
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const override = join(directory, "AGENTS.override.md");
|
|
125
|
+
const agentsPath = join(directory, "AGENTS.md");
|
|
126
|
+
return {
|
|
127
|
+
active: (await readOptionalFile(override))?.trim() ? override : agentsPath,
|
|
128
|
+
candidates: [override, agentsPath]
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
const managedRange = (current) => {
|
|
132
|
+
const starts = current.split(guidanceStart).length - 1;
|
|
133
|
+
const ends = current.split(guidanceEnd).length - 1;
|
|
134
|
+
if (starts === 0 && ends === 0) return;
|
|
135
|
+
const start = current.indexOf(guidanceStart);
|
|
136
|
+
const end = current.indexOf(guidanceEnd);
|
|
137
|
+
if (starts !== 1 || ends !== 1 || end < start) throw new Error("AgentMuxer guidance markers are invalid");
|
|
138
|
+
return {
|
|
139
|
+
start,
|
|
140
|
+
end
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
const withoutGuidance = (current) => {
|
|
144
|
+
const range = managedRange(current);
|
|
145
|
+
if (range === void 0) return {
|
|
146
|
+
next: current,
|
|
147
|
+
removed: false
|
|
148
|
+
};
|
|
149
|
+
let suffix = current.slice(range.end + 32);
|
|
150
|
+
if (range.start === 0) suffix = suffix.replace(/^\n(?:\n)?/, "");
|
|
151
|
+
return {
|
|
152
|
+
next: `${current.slice(0, range.start)}${suffix}`,
|
|
153
|
+
removed: true
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
const validateClientGuidance = async (client) => {
|
|
157
|
+
const { candidates } = await guidancePaths(client);
|
|
158
|
+
const values = await Promise.all(candidates.map(readOptionalFile));
|
|
159
|
+
for (const value of values) managedRange(value ?? "");
|
|
160
|
+
};
|
|
161
|
+
const installClientGuidance = async (client) => {
|
|
162
|
+
const { active, candidates } = await guidancePaths(client);
|
|
163
|
+
const current = new Map(await Promise.all(candidates.map(async (path) => [path, await readOptionalFile(path)])));
|
|
164
|
+
for (const value of current.values()) managedRange(value ?? "");
|
|
165
|
+
const value = current.get(active) ?? "";
|
|
166
|
+
const stripped = withoutGuidance(value).next;
|
|
167
|
+
const next = `${guidanceBlock}${stripped.length === 0 ? "\n" : `\n\n${stripped}`}`;
|
|
168
|
+
if (next !== value) {
|
|
169
|
+
await mkdir(dirname(active), { recursive: true });
|
|
170
|
+
await writeFile(active, next, "utf8");
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
path: active,
|
|
174
|
+
rollback: async () => {
|
|
175
|
+
if (next === value || await readOptionalFile(active) !== next) return;
|
|
176
|
+
const previous = current.get(active);
|
|
177
|
+
if (previous === void 0) await rm(active, { force: true });
|
|
178
|
+
else await writeFile(active, previous, "utf8");
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
const clientGuidanceStatus = async (client) => {
|
|
183
|
+
const { active, candidates } = await guidancePaths(client);
|
|
184
|
+
const values = await Promise.all(candidates.map(readOptionalFile));
|
|
185
|
+
for (const value of values) managedRange(value ?? "");
|
|
186
|
+
return {
|
|
187
|
+
installed: (values[candidates.indexOf(active)] ?? "").includes(guidanceBlock),
|
|
188
|
+
path: active
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
const removeClientGuidance = async (client) => {
|
|
192
|
+
const { candidates } = await guidancePaths(client);
|
|
193
|
+
const current = new Map(await Promise.all(candidates.map(async (path) => [path, await readOptionalFile(path)])));
|
|
194
|
+
for (const value of current.values()) managedRange(value ?? "");
|
|
195
|
+
return (await Promise.all(candidates.map(async (path) => {
|
|
196
|
+
const value = current.get(path);
|
|
197
|
+
if (value === void 0) return false;
|
|
198
|
+
const result = withoutGuidance(value);
|
|
199
|
+
if (!result.removed) return false;
|
|
200
|
+
await writeFile(path, result.next, "utf8");
|
|
201
|
+
return true;
|
|
202
|
+
}))).some(Boolean);
|
|
203
|
+
};
|
|
204
|
+
const isSupportedClient = (value) => supportedClients.some((client) => client === value);
|
|
205
|
+
const clientLabel = (client) => agents[client].displayName;
|
|
206
|
+
const detectedClients = async () => {
|
|
207
|
+
configureClientPaths();
|
|
208
|
+
const detected = new Set(await detectGlobalAgents());
|
|
209
|
+
return supportedClients.filter((client) => detected.has(client));
|
|
210
|
+
};
|
|
211
|
+
const agentmuxerServer = async (client) => {
|
|
212
|
+
configureClientPaths();
|
|
213
|
+
const [installation] = await listInstalledServers({
|
|
214
|
+
global: true,
|
|
215
|
+
agents: [client]
|
|
216
|
+
});
|
|
217
|
+
if (installation === void 0) throw new Error(`Could not inspect ${clientLabel(client)} configuration`);
|
|
218
|
+
await validateConfigSyntax(client, installation.configPath);
|
|
219
|
+
return {
|
|
220
|
+
configPath: installation.configPath,
|
|
221
|
+
detected: installation.detected,
|
|
222
|
+
server: installation.servers.find(({ serverName }) => serverName === agentmuxerServerName)
|
|
223
|
+
};
|
|
224
|
+
};
|
|
225
|
+
const conflictError = (client) => `${clientLabel(client)} already has an ${agentmuxerServerName} configuration that setup cannot safely manage; leave it unchanged or remove it manually`;
|
|
226
|
+
const isManagedServer = (client, server) => {
|
|
227
|
+
const transformed = agents[client].transformConfig(agentmuxerServerName, agentmuxerConfig);
|
|
228
|
+
if (typeof transformed !== "object" || transformed === null || Array.isArray(transformed)) return false;
|
|
229
|
+
const expected = Object.fromEntries(Object.entries(transformed).filter(([, value]) => value !== void 0));
|
|
230
|
+
return isDeepStrictEqual(server.config, expected) || client === "codex" && isDeepStrictEqual(server.config, { url: "https://mcp.agentmuxer.com/mcp" });
|
|
231
|
+
};
|
|
232
|
+
const isManagedServerAtPath = async (client, server, path) => isManagedServer(client, server) && (client !== "codex" || await readCodexLayout(path) !== void 0);
|
|
233
|
+
const uninspectableConfigError = (client) => `${clientLabel(client)} configuration exists but the client was not detected; refusing to modify it`;
|
|
234
|
+
const installAgentmuxer = async (client) => {
|
|
235
|
+
const existing = await agentmuxerServer(client);
|
|
236
|
+
if (!existing.detected && existsSync(existing.configPath)) return {
|
|
237
|
+
success: false,
|
|
238
|
+
path: existing.configPath,
|
|
239
|
+
error: uninspectableConfigError(client)
|
|
240
|
+
};
|
|
241
|
+
if (existing.server !== void 0 && !await isManagedServerAtPath(client, existing.server, existing.configPath)) return {
|
|
242
|
+
success: false,
|
|
243
|
+
path: existing.configPath,
|
|
244
|
+
error: conflictError(client)
|
|
245
|
+
};
|
|
246
|
+
if (existing.server !== void 0) return {
|
|
247
|
+
success: true,
|
|
248
|
+
path: existing.configPath
|
|
249
|
+
};
|
|
250
|
+
if (client === "codex") {
|
|
251
|
+
await installCodexServer(existing.configPath);
|
|
252
|
+
return {
|
|
253
|
+
success: true,
|
|
254
|
+
path: existing.configPath
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const configExisted = existsSync(existing.configPath);
|
|
258
|
+
const result = upsertServer(client, agentmuxerServerName, agentmuxerConfig);
|
|
259
|
+
if (result.success && client === "claude-code" && !configExisted) try {
|
|
260
|
+
await chmod(result.path, 384);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
await rm(result.path, { force: true });
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
return result;
|
|
266
|
+
};
|
|
267
|
+
const listAgentmuxerInstallations = async (clients = supportedClients) => {
|
|
268
|
+
configureClientPaths();
|
|
269
|
+
const installations = await listInstalledServers({
|
|
270
|
+
global: true,
|
|
271
|
+
agents: [...clients]
|
|
272
|
+
});
|
|
273
|
+
await Promise.all(installations.flatMap(({ agentType, configPath }) => isSupportedClient(agentType) ? [validateConfigSyntax(agentType, configPath)] : []));
|
|
274
|
+
return (await Promise.all(installations.flatMap(({ agentType, displayName, servers }) => isSupportedClient(agentType) ? servers.filter(({ serverName }) => serverName === agentmuxerServerName).map(async (server) => await isManagedServerAtPath(agentType, server, server.configPath) ? {
|
|
275
|
+
client: agentType,
|
|
276
|
+
displayName,
|
|
277
|
+
configPath: server.configPath
|
|
278
|
+
} : void 0) : []))).flatMap((installation) => installation === void 0 ? [] : [installation]);
|
|
279
|
+
};
|
|
280
|
+
const removeAgentmuxer = async (client) => {
|
|
281
|
+
const existing = await agentmuxerServer(client);
|
|
282
|
+
if (!existing.detected && existsSync(existing.configPath)) return {
|
|
283
|
+
success: false,
|
|
284
|
+
path: existing.configPath,
|
|
285
|
+
removed: false,
|
|
286
|
+
error: uninspectableConfigError(client)
|
|
287
|
+
};
|
|
288
|
+
if (existing.server === void 0) return {
|
|
289
|
+
success: true,
|
|
290
|
+
path: existing.configPath,
|
|
291
|
+
removed: false
|
|
292
|
+
};
|
|
293
|
+
if (!await isManagedServerAtPath(client, existing.server, existing.configPath)) return {
|
|
294
|
+
success: false,
|
|
295
|
+
path: existing.configPath,
|
|
296
|
+
removed: false,
|
|
297
|
+
error: conflictError(client)
|
|
298
|
+
};
|
|
299
|
+
if (client === "codex") try {
|
|
300
|
+
const layout = await readCodexLayout(existing.configPath);
|
|
301
|
+
if (layout === void 0) return {
|
|
302
|
+
success: false,
|
|
303
|
+
path: existing.configPath,
|
|
304
|
+
removed: false
|
|
305
|
+
};
|
|
306
|
+
await writeFile(existing.configPath, layout, "utf8");
|
|
307
|
+
return {
|
|
308
|
+
success: true,
|
|
309
|
+
path: existing.configPath,
|
|
310
|
+
removed: true
|
|
311
|
+
};
|
|
312
|
+
} catch (error) {
|
|
313
|
+
return {
|
|
314
|
+
success: false,
|
|
315
|
+
path: existing.configPath,
|
|
316
|
+
removed: false,
|
|
317
|
+
error: error instanceof Error ? error.message : "removal failed"
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return removeServer(client, agentmuxerServerName);
|
|
321
|
+
};
|
|
322
|
+
const authenticationCommands = {
|
|
323
|
+
codex: `codex mcp login ${agentmuxerServerName}`,
|
|
324
|
+
"claude-code": `claude mcp login ${agentmuxerServerName}`,
|
|
325
|
+
opencode: `opencode mcp auth ${agentmuxerServerName}`
|
|
326
|
+
};
|
|
327
|
+
const authenticationCommand = (client) => authenticationCommands[client];
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/cli.ts
|
|
330
|
+
const version = "0.1.0";
|
|
331
|
+
const help = `AgentMuxer setup ${version}
|
|
332
|
+
|
|
333
|
+
Usage:
|
|
334
|
+
agentmuxer-setup install Install AgentMuxer globally
|
|
335
|
+
agentmuxer-setup status Verify client configuration and guidance
|
|
336
|
+
agentmuxer-setup remove Remove AgentMuxer-managed configuration
|
|
337
|
+
|
|
338
|
+
Options:
|
|
339
|
+
--client <name> Target codex, claude-code, or opencode; repeatable
|
|
340
|
+
--help, -h Show help
|
|
341
|
+
--version, -v Show version
|
|
342
|
+
`;
|
|
343
|
+
const fail = (message) => {
|
|
344
|
+
throw new Error(message);
|
|
345
|
+
};
|
|
346
|
+
const clientSetupState = async (client) => {
|
|
347
|
+
const [installations, guidance] = await Promise.all([listAgentmuxerInstallations([client]), clientGuidanceStatus(client)]);
|
|
348
|
+
return {
|
|
349
|
+
guidance: guidance.installed,
|
|
350
|
+
mcp: installations.length > 0
|
|
351
|
+
};
|
|
352
|
+
};
|
|
353
|
+
const withClientSetupRollback = async (client, operation) => {
|
|
354
|
+
const actions = [];
|
|
355
|
+
try {
|
|
356
|
+
return await operation((action) => actions.unshift(action));
|
|
357
|
+
} catch (error) {
|
|
358
|
+
const failures = (await Promise.allSettled(actions.map((action) => action()))).flatMap((result) => result.status === "rejected" ? [String(result.reason)] : []);
|
|
359
|
+
if (failures.length > 0) throw new Error(`${clientLabel(client)} setup failed and could not be fully rolled back: ${failures.join("; ")}`, { cause: error });
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
const parseClients = (args) => {
|
|
364
|
+
const { values, positionals } = parseArgs({
|
|
365
|
+
args,
|
|
366
|
+
allowPositionals: true,
|
|
367
|
+
options: { client: {
|
|
368
|
+
type: "string",
|
|
369
|
+
multiple: true
|
|
370
|
+
} }
|
|
371
|
+
});
|
|
372
|
+
if (positionals.length > 0) fail(`Unexpected argument: ${positionals[0]}`);
|
|
373
|
+
if (values.client === void 0) return;
|
|
374
|
+
const invalid = values.client.find((client) => !isSupportedClient(client));
|
|
375
|
+
if (invalid !== void 0) fail(`Unsupported client: ${invalid}`);
|
|
376
|
+
return [...new Set(values.client.filter(isSupportedClient))];
|
|
377
|
+
};
|
|
378
|
+
const install = async (args) => {
|
|
379
|
+
const clients = parseClients(args) ?? await detectedClients();
|
|
380
|
+
if (clients.length === 0) fail(`No supported clients detected; use --client with: ${supportedClients.join(", ")}`);
|
|
381
|
+
await Promise.all(clients.map(validateClientGuidance));
|
|
382
|
+
const outcomes = await Promise.all(clients.map(async (client) => {
|
|
383
|
+
try {
|
|
384
|
+
return await withClientSetupRollback(client, async (defer) => {
|
|
385
|
+
const guidance = await installClientGuidance(client);
|
|
386
|
+
defer(guidance.rollback);
|
|
387
|
+
const result = await installAgentmuxer(client);
|
|
388
|
+
if (!result.success) throw new Error(result.error ?? "installation failed");
|
|
389
|
+
return {
|
|
390
|
+
lines: [
|
|
391
|
+
`${clientLabel(client)} MCP: ${result.path}`,
|
|
392
|
+
`${clientLabel(client)} guidance: ${guidance.path}`,
|
|
393
|
+
`${clientLabel(client)} sign-in: ${authenticationCommand(client)}`
|
|
394
|
+
],
|
|
395
|
+
failure: void 0
|
|
396
|
+
};
|
|
397
|
+
});
|
|
398
|
+
} catch (error) {
|
|
399
|
+
return {
|
|
400
|
+
lines: [],
|
|
401
|
+
failure: `${clientLabel(client)}: ${error instanceof Error ? error.message : "installation failed"}`
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
}));
|
|
405
|
+
for (const line of outcomes.flatMap((outcome) => outcome.lines ?? [])) process.stdout.write(`${line}\n`);
|
|
406
|
+
const failures = outcomes.flatMap((outcome) => outcome.failure ?? []);
|
|
407
|
+
if (failures.length > 0) fail(failures.join("\n"));
|
|
408
|
+
process.stdout.write("Restart open clients after setup. Discovery works before sign-in; paid calls require OAuth.\n");
|
|
409
|
+
};
|
|
410
|
+
const status = async (args) => {
|
|
411
|
+
const clients = parseClients(args) ?? await detectedClients();
|
|
412
|
+
if (clients.length === 0) fail(`No supported clients detected; use --client with: ${supportedClients.join(", ")}`);
|
|
413
|
+
const installations = await listAgentmuxerInstallations(clients);
|
|
414
|
+
const statuses = await Promise.all(clients.map(clientGuidanceStatus));
|
|
415
|
+
if (!clients.map((client, index) => {
|
|
416
|
+
const installation = installations.find((candidate) => candidate.client === client);
|
|
417
|
+
const guidance = statuses[index];
|
|
418
|
+
if (guidance === void 0) return false;
|
|
419
|
+
process.stdout.write(`${clientLabel(client)} MCP: ${installation?.configPath ?? "not installed"}\n`);
|
|
420
|
+
process.stdout.write(`${clientLabel(client)} guidance: ${guidance.installed ? guidance.path : "not installed"}\n`);
|
|
421
|
+
return installation !== void 0 && guidance.installed;
|
|
422
|
+
}).every(Boolean)) fail("AgentMuxer setup is incomplete or conflicting; run agentmuxer-setup install, or remove a custom same-name MCP manually");
|
|
423
|
+
process.stdout.write("AgentMuxer configuration and guidance verified.\n");
|
|
424
|
+
};
|
|
425
|
+
const remove = async (args) => {
|
|
426
|
+
const clients = parseClients(args) ?? supportedClients;
|
|
427
|
+
await Promise.all(clients.map(validateClientGuidance));
|
|
428
|
+
const outcomes = await Promise.all(clients.map(async (client) => {
|
|
429
|
+
try {
|
|
430
|
+
const before = await clientSetupState(client);
|
|
431
|
+
return await withClientSetupRollback(client, async (defer) => {
|
|
432
|
+
const result = await removeAgentmuxer(client);
|
|
433
|
+
if (!result.success) throw new Error(result.error ?? "removal failed");
|
|
434
|
+
if (result.removed && before.mcp) defer(async () => {
|
|
435
|
+
const rollback = await installAgentmuxer(client);
|
|
436
|
+
if (!rollback.success) throw new Error(rollback.error ?? "MCP rollback failed");
|
|
437
|
+
});
|
|
438
|
+
if (before.guidance) defer(async () => {
|
|
439
|
+
await installClientGuidance(client);
|
|
440
|
+
});
|
|
441
|
+
const guidanceRemoved = await removeClientGuidance(client);
|
|
442
|
+
return {
|
|
443
|
+
lines: [`${clientLabel(client)} MCP: ${result.removed ? "removed" : "not installed"}`, ...guidanceRemoved ? [`${clientLabel(client)} guidance: removed`] : []],
|
|
444
|
+
failure: void 0
|
|
445
|
+
};
|
|
446
|
+
});
|
|
447
|
+
} catch (error) {
|
|
448
|
+
return {
|
|
449
|
+
lines: [],
|
|
450
|
+
failure: `${clientLabel(client)}: ${error instanceof Error ? error.message : "removal failed"}`
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
}));
|
|
454
|
+
for (const line of outcomes.flatMap((outcome) => outcome.lines ?? [])) process.stdout.write(`${line}\n`);
|
|
455
|
+
const failures = outcomes.flatMap((outcome) => outcome.failure ?? []);
|
|
456
|
+
if (failures.length > 0) fail(failures.join("\n"));
|
|
457
|
+
};
|
|
458
|
+
const runCli = async (args) => {
|
|
459
|
+
const [command, ...rest] = args;
|
|
460
|
+
if (command === "--help" || command === "-h" || command === void 0) {
|
|
461
|
+
process.stdout.write(help);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (command === "--version" || command === "-v") {
|
|
465
|
+
process.stdout.write(`${version}\n`);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (command === "install") return install(rest);
|
|
469
|
+
if (command === "status") return status(rest);
|
|
470
|
+
if (command === "remove") return remove(rest);
|
|
471
|
+
fail(`Unknown command: ${command}\n\n${help}`);
|
|
472
|
+
};
|
|
473
|
+
//#endregion
|
|
474
|
+
//#region src/bin.ts
|
|
475
|
+
runCli(process.argv.slice(2)).catch((error) => {
|
|
476
|
+
process.stderr.write(`AgentMuxer setup: ${error instanceof Error ? error.message : "Unexpected error"}\n`);
|
|
477
|
+
process.exitCode = 1;
|
|
478
|
+
});
|
|
479
|
+
//#endregion
|
|
480
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentmuxer/setup",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Install AgentMuxer's hosted MCP in supported coding agents",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agentmuxer",
|
|
7
|
+
"mcp",
|
|
8
|
+
"model-context-protocol"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://agentmuxer.com",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Amorphic-Labs/agentmuxer/issues"
|
|
13
|
+
},
|
|
14
|
+
"license": "UNLICENSED",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/Amorphic-Labs/agentmuxer.git",
|
|
18
|
+
"directory": "services/installer"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"agentmuxer-setup": "./dist/bin.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"add-mcp": "2.3.0",
|
|
33
|
+
"jsonc-parser": "3.3.1",
|
|
34
|
+
"toml-eslint-parser": "1.0.3"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^24.13.3",
|
|
38
|
+
"typescript": "7.0.2",
|
|
39
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.9",
|
|
40
|
+
"vite-plus": "0.2.9"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=22.18.0"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "vp pack",
|
|
47
|
+
"release:smoke": "node scripts/release-smoke.mjs",
|
|
48
|
+
"test": "vp test run"
|
|
49
|
+
}
|
|
50
|
+
}
|