@agentmuxer/setup 0.1.1 → 0.1.2
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 +57 -6
- package/dist/bin.js +256 -61
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,15 +14,66 @@ automatic detection is unavailable. Repeat `--client` to select more than one.
|
|
|
14
14
|
|
|
15
15
|
The installer registers `https://mcp.agentmuxer.com/mcp` in each selected client's global MCP
|
|
16
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
|
|
18
|
-
AgentMuxer endpoint
|
|
17
|
+
instructions are preserved. Repeat installation updates the block. Removal deletes the marked block and standard
|
|
18
|
+
AgentMuxer endpoint for Codex and Claude Code. OpenCode is disabled with its native `enabled: false`
|
|
19
|
+
setting so its OAuth registration survives alongside its retained credentials; reinstall re-enables it. A nonstandard or different MCP using the same name is reported
|
|
19
20
|
as a conflict and left untouched. The guidance requires a free catalog search before external tooling, then leaves
|
|
20
21
|
the native-versus-specialized choice to fit, simplicity, and cost. Search never requires invocation.
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
The standard install command requests `offline_access` for Claude Code and OpenCode so WorkOS can
|
|
24
|
+
issue refresh tokens. Reinstall upgrades the previous managed URL-only configuration, preserves
|
|
25
|
+
comments and unrelated settings, and leaves custom OAuth configurations untouched. Existing grants
|
|
26
|
+
issued without refresh tokens need one new sign-in after the scope upgrade. Codex retains its native
|
|
27
|
+
OAuth configuration; this change does not fix refresh persistence bugs in older Codex versions.
|
|
28
|
+
|
|
29
|
+
On first installation, setup starts each client's native browser sign-in sequentially. The native
|
|
30
|
+
client owns consent, credential storage, and refresh. Setup runs these commands outside the current
|
|
31
|
+
project so project-specific MCP configurations do not override the global installation. OpenCode
|
|
32
|
+
configuration override environment variables are excluded from these native setup commands; an
|
|
33
|
+
OpenCode session launched with those overrides can still use different configuration.
|
|
34
|
+
Repeat installation preserves existing credentials. Status reports the client's own sign-in status:
|
|
35
|
+
Claude Code checks the connection; Codex and OpenCode report stored credentials, which does not prove
|
|
36
|
+
that a live MCP request will succeed. OpenCode's `expired` status means its stored access token
|
|
37
|
+
has expired; setup preserves the credentials so OpenCode can attempt refresh on its next connection.
|
|
38
|
+
Restart open clients to load the configuration.
|
|
39
|
+
|
|
40
|
+
New OpenCode installations first check native status using a fixed in-memory configuration through
|
|
41
|
+
OpenCode's native `OPENCODE_CONFIG_CONTENT` support, before writing the AgentMuxer entry.
|
|
42
|
+
Retained credentials preserve that registration; only a signed-out new connection configures its
|
|
43
|
+
native `oauth.clientId` with `client_01M1TJJ3DKJR89RPXJ5XVWSKMH`, AgentMuxer's pre-registered public
|
|
44
|
+
WorkOS OAuth application for OpenCode. It uses PKCE without a client secret. OpenCode handles
|
|
45
|
+
browser consent, credential storage and refresh. The fixed ID survives explicit re-login and
|
|
46
|
+
represents the AgentMuxer OpenCode integration, not a device.
|
|
47
|
+
Setup may retry sign-in for this configuration when OpenCode reports that it is signed out.
|
|
48
|
+
|
|
49
|
+
Existing OpenCode DCR configurations keep their original registration and tokens. Setup adds the
|
|
50
|
+
refresh scope without switching their client ID. Explicit DCR re-login in OpenCode 1.18.29 can create
|
|
51
|
+
a new ID and therefore a new AgentMuxer principal. No automatic migration or principal relinking is
|
|
52
|
+
performed. Keep the disabled OpenCode entry when removing setup: manually deleting its configured
|
|
53
|
+
client ID while retaining tokens can require a new sign-in. Codex and Claude retained their CIMD IDs in the tested re-login paths, but setup does not
|
|
54
|
+
infer the registration type from their credential-presence status.
|
|
55
|
+
|
|
56
|
+
An unavailable status or network failure never triggers sign-in. Setup waits for native status
|
|
57
|
+
before writing the AgentMuxer entry, so a failed preflight cannot leave a scope-only registration. For existing connections without
|
|
58
|
+
the configured stable ID, setup prints the native reconnect command. An interrupted first sign-in
|
|
59
|
+
leaves configuration saved. No API-key fallback, local token store, OAuth broker, or Blueprint
|
|
60
|
+
integration is added. Owner/client principal bindings and spending restrictions remain unchanged.
|
|
61
|
+
|
|
62
|
+
The acceptance criterion is silent refresh/restart during healthy operation while authorization
|
|
63
|
+
remains valid. The recorded six-build matrix passed 78 ordinary forced-refresh/restart cycles.
|
|
64
|
+
Credential loss beyond an issuer's replay window is an accepted limitation. Older Codex builds also
|
|
65
|
+
have a reproduced persistence defect when a successful refresh is followed by an MCP 503; this is a
|
|
66
|
+
known client limitation, not something setup repairs. WorkOS Connect's idle and absolute grant
|
|
67
|
+
lifetimes remain unverified; do not promise that users will never need to sign in again. Full
|
|
68
|
+
production idle behavior and desktop onboarding are not certified by setup's packed smoke test.
|
|
69
|
+
|
|
70
|
+
The public WorkOS application must retain its client ID, PKCE requirement, and native redirect URI
|
|
71
|
+
`http://127.0.0.1:19876/mcp/oauth/callback`. OpenCode 1.18.29 passed native login and a live MCP request
|
|
72
|
+
against production with this application, followed by two refresh/restart cycles after actual
|
|
73
|
+
300-second access-token expiration without another login or browser. The built setup CLI also
|
|
74
|
+
passed remove/reinstall and reconnection without sign-in; explicit native re-login and reconnection
|
|
75
|
+
passed separately. These checks do not establish long-idle behavior or desktop onboarding.
|
|
76
|
+
No AgentMuxer-hosted client metadata route is required.
|
|
26
77
|
|
|
27
78
|
Release maintainers must run `vp run @agentmuxer/setup#release:smoke` and publish the exact tested
|
|
28
79
|
tarball it writes to `.context/release`, never publish this directory with npm.
|
package/dist/bin.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { isDeepStrictEqual, parseArgs } from "node:util";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { isDeepStrictEqual, parseArgs, promisify, stripVTControlCharacters } from "node:util";
|
|
3
|
+
import { execFile, spawn } from "node:child_process";
|
|
4
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
5
6
|
import { dirname, join } from "node:path";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { agents, detectGlobalAgents, listInstalledServers, removeServer } from "add-mcp";
|
|
9
|
+
import { applyEdits, modify, parse } from "jsonc-parser";
|
|
8
10
|
import { parseTOML } from "toml-eslint-parser";
|
|
9
11
|
//#region src/management.ts
|
|
10
12
|
const agentmuxerServerName = "agentmuxer";
|
|
11
13
|
const agentmuxerMcpUrl = "https://mcp.agentmuxer.com/mcp";
|
|
14
|
+
const openCodeClientId = "client_01M1TJJ3DKJR89RPXJ5XVWSKMH";
|
|
12
15
|
const agentmuxerConfig = {
|
|
13
16
|
type: "http",
|
|
14
17
|
url: agentmuxerMcpUrl
|
|
@@ -223,15 +226,47 @@ const agentmuxerServer = async (client) => {
|
|
|
223
226
|
};
|
|
224
227
|
};
|
|
225
228
|
const conflictError = (client) => `${clientLabel(client)} already has an ${agentmuxerServerName} configuration that setup cannot safely manage; leave it unchanged or remove it manually`;
|
|
229
|
+
const nativeServerConfig = (client) => {
|
|
230
|
+
const config = agents[client].transformConfig(agentmuxerServerName, agentmuxerConfig);
|
|
231
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) throw new Error("Invalid client configuration transform");
|
|
232
|
+
const base = Object.fromEntries(Object.entries(config).filter(([, value]) => value !== void 0));
|
|
233
|
+
return client === "codex" ? base : {
|
|
234
|
+
...base,
|
|
235
|
+
oauth: client === "claude-code" ? { scopes: "offline_access" } : {
|
|
236
|
+
clientId: openCodeClientId,
|
|
237
|
+
scope: "offline_access"
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
};
|
|
226
241
|
const isManagedServer = (client, server) => {
|
|
227
242
|
const transformed = agents[client].transformConfig(agentmuxerServerName, agentmuxerConfig);
|
|
228
243
|
if (typeof transformed !== "object" || transformed === null || Array.isArray(transformed)) return false;
|
|
229
244
|
const expected = Object.fromEntries(Object.entries(transformed).filter(([, value]) => value !== void 0));
|
|
230
|
-
|
|
245
|
+
const config = client === "opencode" && server.config["enabled"] === false ? {
|
|
246
|
+
...server.config,
|
|
247
|
+
enabled: true
|
|
248
|
+
} : server.config;
|
|
249
|
+
return isDeepStrictEqual(config, expected) || isDeepStrictEqual(config, nativeServerConfig(client)) || client === "opencode" && isDeepStrictEqual(config, {
|
|
250
|
+
...nativeServerConfig(client),
|
|
251
|
+
oauth: { scope: "offline_access" }
|
|
252
|
+
}) || client === "codex" && isDeepStrictEqual(config, { url: "https://mcp.agentmuxer.com/mcp" });
|
|
231
253
|
};
|
|
232
254
|
const isManagedServerAtPath = async (client, server, path) => isManagedServer(client, server) && (client !== "codex" || await readCodexLayout(path) !== void 0);
|
|
233
255
|
const uninspectableConfigError = (client) => `${clientLabel(client)} configuration exists but the client was not detected; refusing to modify it`;
|
|
234
|
-
const
|
|
256
|
+
const writeNativeServerConfig = async (client, path, config) => {
|
|
257
|
+
const original = await readOptionalFile(path) ?? "{}";
|
|
258
|
+
const next = applyEdits(original, modify(original, [client === "claude-code" ? "mcpServers" : "mcp", agentmuxerServerName], config, { formattingOptions: {
|
|
259
|
+
insertSpaces: true,
|
|
260
|
+
tabSize: 2,
|
|
261
|
+
eol: original.includes("\r\n") ? "\r\n" : "\n"
|
|
262
|
+
} }));
|
|
263
|
+
await mkdir(dirname(path), { recursive: true });
|
|
264
|
+
await writeFile(path, next, {
|
|
265
|
+
encoding: "utf8",
|
|
266
|
+
mode: 384
|
|
267
|
+
});
|
|
268
|
+
};
|
|
269
|
+
const installAgentmuxer = async (client, newOpenCodeRegistration = false) => {
|
|
235
270
|
const existing = await agentmuxerServer(client);
|
|
236
271
|
if (!existing.detected && existsSync(existing.configPath)) return {
|
|
237
272
|
success: false,
|
|
@@ -243,26 +278,37 @@ const installAgentmuxer = async (client) => {
|
|
|
243
278
|
path: existing.configPath,
|
|
244
279
|
error: conflictError(client)
|
|
245
280
|
};
|
|
246
|
-
if (existing.server !== void 0) return {
|
|
281
|
+
if (existing.server !== void 0 && (client === "codex" || isDeepStrictEqual(existing.server.config, nativeServerConfig(client)))) return {
|
|
247
282
|
success: true,
|
|
248
|
-
path: existing.configPath
|
|
283
|
+
path: existing.configPath,
|
|
284
|
+
stableClientId: client === "opencode"
|
|
249
285
|
};
|
|
250
286
|
if (client === "codex") {
|
|
251
287
|
await installCodexServer(existing.configPath);
|
|
252
288
|
return {
|
|
253
289
|
success: true,
|
|
254
|
-
path: existing.configPath
|
|
290
|
+
path: existing.configPath,
|
|
291
|
+
created: true
|
|
255
292
|
};
|
|
256
293
|
}
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
294
|
+
const config = client === "opencode" && !(existing.server === void 0 && newOpenCodeRegistration) && !isDeepStrictEqual({
|
|
295
|
+
...existing.server?.config,
|
|
296
|
+
enabled: true
|
|
297
|
+
}, nativeServerConfig(client)) ? {
|
|
298
|
+
...nativeServerConfig(client),
|
|
299
|
+
oauth: { scope: "offline_access" }
|
|
300
|
+
} : nativeServerConfig(client);
|
|
301
|
+
if (existing.server !== void 0 && isDeepStrictEqual(existing.server.config, config)) return {
|
|
302
|
+
success: true,
|
|
303
|
+
path: existing.configPath
|
|
304
|
+
};
|
|
305
|
+
await writeNativeServerConfig(client, existing.configPath, config);
|
|
306
|
+
return {
|
|
307
|
+
success: true,
|
|
308
|
+
path: existing.configPath,
|
|
309
|
+
created: existing.server === void 0,
|
|
310
|
+
stableClientId: client === "opencode" && isDeepStrictEqual(config, nativeServerConfig(client))
|
|
311
|
+
};
|
|
266
312
|
};
|
|
267
313
|
const listAgentmuxerInstallations = async (clients = supportedClients) => {
|
|
268
314
|
configureClientPaths();
|
|
@@ -271,7 +317,7 @@ const listAgentmuxerInstallations = async (clients = supportedClients) => {
|
|
|
271
317
|
agents: [...clients]
|
|
272
318
|
});
|
|
273
319
|
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 ===
|
|
320
|
+
return (await Promise.all(installations.flatMap(({ agentType, displayName, servers }) => isSupportedClient(agentType) ? servers.filter(({ serverName, config }) => serverName === "agentmuxer" && !(agentType === "opencode" && config["enabled"] === false)).map(async (server) => await isManagedServerAtPath(agentType, server, server.configPath) ? {
|
|
275
321
|
client: agentType,
|
|
276
322
|
displayName,
|
|
277
323
|
configPath: server.configPath
|
|
@@ -296,18 +342,51 @@ const removeAgentmuxer = async (client) => {
|
|
|
296
342
|
removed: false,
|
|
297
343
|
error: conflictError(client)
|
|
298
344
|
};
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
345
|
+
try {
|
|
346
|
+
const original = await readFile(existing.configPath, "utf8");
|
|
347
|
+
let result;
|
|
348
|
+
if (client === "codex") {
|
|
349
|
+
const layout = await readCodexLayout(existing.configPath);
|
|
350
|
+
if (layout === void 0) return {
|
|
351
|
+
success: false,
|
|
352
|
+
path: existing.configPath,
|
|
353
|
+
removed: false
|
|
354
|
+
};
|
|
355
|
+
await writeFile(existing.configPath, layout, "utf8");
|
|
356
|
+
result = {
|
|
357
|
+
success: true,
|
|
358
|
+
path: existing.configPath,
|
|
359
|
+
removed: true
|
|
360
|
+
};
|
|
361
|
+
} else if (client === "opencode") {
|
|
362
|
+
if (existing.server.config["enabled"] === false) return {
|
|
363
|
+
success: true,
|
|
364
|
+
path: existing.configPath,
|
|
365
|
+
removed: false,
|
|
366
|
+
disabled: true
|
|
367
|
+
};
|
|
368
|
+
await writeNativeServerConfig(client, existing.configPath, {
|
|
369
|
+
...existing.server.config,
|
|
370
|
+
enabled: false
|
|
371
|
+
});
|
|
372
|
+
result = {
|
|
373
|
+
success: true,
|
|
374
|
+
path: existing.configPath,
|
|
375
|
+
removed: true,
|
|
376
|
+
disabled: true
|
|
377
|
+
};
|
|
378
|
+
} else result = removeServer(client, agentmuxerServerName);
|
|
379
|
+
if (!result.success || !result.removed) return result;
|
|
380
|
+
const next = await readOptionalFile(existing.configPath);
|
|
307
381
|
return {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
382
|
+
...result,
|
|
383
|
+
rollback: async () => {
|
|
384
|
+
if (await readOptionalFile(existing.configPath) !== next) throw new Error("MCP configuration changed during removal; refusing to overwrite it");
|
|
385
|
+
await writeFile(existing.configPath, original, {
|
|
386
|
+
encoding: "utf8",
|
|
387
|
+
mode: 384
|
|
388
|
+
});
|
|
389
|
+
}
|
|
311
390
|
};
|
|
312
391
|
} catch (error) {
|
|
313
392
|
return {
|
|
@@ -317,7 +396,6 @@ const removeAgentmuxer = async (client) => {
|
|
|
317
396
|
error: error instanceof Error ? error.message : "removal failed"
|
|
318
397
|
};
|
|
319
398
|
}
|
|
320
|
-
return removeServer(client, agentmuxerServerName);
|
|
321
399
|
};
|
|
322
400
|
const authenticationCommands = {
|
|
323
401
|
codex: `codex mcp login ${agentmuxerServerName}`,
|
|
@@ -326,14 +404,112 @@ const authenticationCommands = {
|
|
|
326
404
|
};
|
|
327
405
|
const authenticationCommand = (client) => authenticationCommands[client];
|
|
328
406
|
//#endregion
|
|
407
|
+
//#region src/authentication.ts
|
|
408
|
+
const execute = promisify(execFile);
|
|
409
|
+
const statusCommands = {
|
|
410
|
+
codex: [
|
|
411
|
+
"codex",
|
|
412
|
+
"mcp",
|
|
413
|
+
"list",
|
|
414
|
+
"--json"
|
|
415
|
+
],
|
|
416
|
+
"claude-code": [
|
|
417
|
+
"claude",
|
|
418
|
+
"mcp",
|
|
419
|
+
"list"
|
|
420
|
+
],
|
|
421
|
+
opencode: [
|
|
422
|
+
"opencode",
|
|
423
|
+
"mcp",
|
|
424
|
+
"auth",
|
|
425
|
+
"list"
|
|
426
|
+
]
|
|
427
|
+
};
|
|
428
|
+
const nativeEnvironment = (client) => client === "opencode" ? Object.fromEntries(Object.entries(process.env).filter(([name]) => ![
|
|
429
|
+
"OPENCODE_CONFIG",
|
|
430
|
+
"OPENCODE_CONFIG_CONTENT",
|
|
431
|
+
"OPENCODE_CONFIG_DIR"
|
|
432
|
+
].includes(name.toUpperCase()))) : process.env;
|
|
433
|
+
/** Read supported native CLI output, never the client's private credential store. */
|
|
434
|
+
const authenticationState = async (client) => {
|
|
435
|
+
const [command, ...args] = statusCommands[client];
|
|
436
|
+
const cwd = await mkdtemp(join(tmpdir(), "agentmuxer-auth-"));
|
|
437
|
+
try {
|
|
438
|
+
const env = nativeEnvironment(client);
|
|
439
|
+
if (client === "opencode") env["OPENCODE_CONFIG_CONTENT"] = JSON.stringify({ mcp: { agentmuxer: {
|
|
440
|
+
type: "remote",
|
|
441
|
+
url: agentmuxerMcpUrl,
|
|
442
|
+
enabled: true,
|
|
443
|
+
oauth: { scope: "offline_access" }
|
|
444
|
+
} } });
|
|
445
|
+
const { stdout } = await execute(command, args, {
|
|
446
|
+
cwd,
|
|
447
|
+
env,
|
|
448
|
+
shell: process.platform === "win32",
|
|
449
|
+
timeout: 3e4,
|
|
450
|
+
maxBuffer: 1048576
|
|
451
|
+
});
|
|
452
|
+
if (client === "codex") {
|
|
453
|
+
const entries = JSON.parse(stdout);
|
|
454
|
+
if (!Array.isArray(entries)) return "unavailable";
|
|
455
|
+
const entry = entries.find((value) => typeof value === "object" && value !== null && "name" in value && value.name === "agentmuxer");
|
|
456
|
+
if (typeof entry !== "object" || entry === null || !("auth_status" in entry)) return "unavailable";
|
|
457
|
+
return entry.auth_status === "o_auth" ? "credentials-present" : entry.auth_status === "not_logged_in" ? "signed-out" : "unavailable";
|
|
458
|
+
}
|
|
459
|
+
const line = stripVTControlCharacters(stdout).split(/\r?\n/).find((value) => client === "claude-code" ? /^agentmuxer:\s/.test(value.trim()) : /^[\s│●✓✗✘✔✖○⚠]*agentmuxer\s/.test(value));
|
|
460
|
+
if (line === void 0) return "unavailable";
|
|
461
|
+
if (/\b(?:needs authentication|not authenticated)\b/i.test(line)) return "signed-out";
|
|
462
|
+
if (client === "claude-code" && /✓ Connected\s*$/i.test(line)) return "connected";
|
|
463
|
+
return client === "opencode" && /\b(?:authenticated|expired)\s*$/i.test(line) ? "credentials-present" : "unavailable";
|
|
464
|
+
} catch {
|
|
465
|
+
return "unavailable";
|
|
466
|
+
} finally {
|
|
467
|
+
await rm(cwd, {
|
|
468
|
+
recursive: true,
|
|
469
|
+
force: true
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
/** The native client owns browser consent, credential storage and token refresh. */
|
|
474
|
+
const signIn = async (client) => {
|
|
475
|
+
const cwd = await mkdtemp(join(tmpdir(), "agentmuxer-auth-"));
|
|
476
|
+
try {
|
|
477
|
+
await new Promise((resolve, reject) => {
|
|
478
|
+
const [command, ...args] = authenticationCommand(client).split(" ");
|
|
479
|
+
if (command === void 0) {
|
|
480
|
+
reject(/* @__PURE__ */ new Error("Missing native sign-in command"));
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const child = spawn(command, args, {
|
|
484
|
+
cwd,
|
|
485
|
+
env: nativeEnvironment(client),
|
|
486
|
+
stdio: "inherit",
|
|
487
|
+
shell: process.platform === "win32"
|
|
488
|
+
});
|
|
489
|
+
child.once("error", () => {
|
|
490
|
+
reject(/* @__PURE__ */ new Error(`${clientLabel(client)} sign-in could not start. Configuration is saved; run ${authenticationCommand(client)} from a terminal where the client is installed.`));
|
|
491
|
+
});
|
|
492
|
+
child.once("close", (code) => {
|
|
493
|
+
if (code === 0) resolve();
|
|
494
|
+
else reject(/* @__PURE__ */ new Error(`${clientLabel(client)} sign-in did not complete. Configuration is saved; retry ${authenticationCommand(client)}.`));
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
} finally {
|
|
498
|
+
await rm(cwd, {
|
|
499
|
+
recursive: true,
|
|
500
|
+
force: true
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
//#endregion
|
|
329
505
|
//#region src/cli.ts
|
|
330
|
-
const version = "0.1.
|
|
506
|
+
const version = "0.1.2";
|
|
331
507
|
const help = `AgentMuxer setup ${version}
|
|
332
508
|
|
|
333
509
|
Usage:
|
|
334
|
-
agentmuxer
|
|
335
|
-
agentmuxer
|
|
336
|
-
agentmuxer
|
|
510
|
+
npx -y @agentmuxer/setup@latest install Install AgentMuxer globally and start native sign-in
|
|
511
|
+
npx -y @agentmuxer/setup@latest status Check configuration and native sign-in status
|
|
512
|
+
npx -y @agentmuxer/setup@latest remove Remove AgentMuxer-managed configuration
|
|
337
513
|
|
|
338
514
|
Options:
|
|
339
515
|
--client <name> Target codex, claude-code, or opencode; repeatable
|
|
@@ -343,13 +519,6 @@ Options:
|
|
|
343
519
|
const fail = (message) => {
|
|
344
520
|
throw new Error(message);
|
|
345
521
|
};
|
|
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
522
|
const withClientSetupRollback = async (client, operation) => {
|
|
354
523
|
const actions = [];
|
|
355
524
|
try {
|
|
@@ -382,21 +551,25 @@ const install = async (args) => {
|
|
|
382
551
|
const outcomes = await Promise.all(clients.map(async (client) => {
|
|
383
552
|
try {
|
|
384
553
|
return await withClientSetupRollback(client, async (defer) => {
|
|
554
|
+
const state = client === "opencode" ? await authenticationState(client) : void 0;
|
|
555
|
+
if (state === "unavailable") throw new Error("Native sign-in status is unavailable; retry setup. Existing configuration and credentials were preserved.");
|
|
385
556
|
const guidance = await installClientGuidance(client);
|
|
386
557
|
defer(guidance.rollback);
|
|
387
|
-
const result = await installAgentmuxer(client);
|
|
558
|
+
const result = await installAgentmuxer(client, state === "signed-out");
|
|
388
559
|
if (!result.success) throw new Error(result.error ?? "installation failed");
|
|
389
560
|
return {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
],
|
|
561
|
+
client,
|
|
562
|
+
created: result.created === true,
|
|
563
|
+
state,
|
|
564
|
+
stableClientId: result.stableClientId === true,
|
|
565
|
+
lines: [`${clientLabel(client)} MCP: ${result.path}`, `${clientLabel(client)} guidance: ${guidance.path}`],
|
|
395
566
|
failure: void 0
|
|
396
567
|
};
|
|
397
568
|
});
|
|
398
569
|
} catch (error) {
|
|
399
570
|
return {
|
|
571
|
+
client,
|
|
572
|
+
created: false,
|
|
400
573
|
lines: [],
|
|
401
574
|
failure: `${clientLabel(client)}: ${error instanceof Error ? error.message : "installation failed"}`
|
|
402
575
|
};
|
|
@@ -404,8 +577,27 @@ const install = async (args) => {
|
|
|
404
577
|
}));
|
|
405
578
|
for (const line of outcomes.flatMap((outcome) => outcome.lines ?? [])) process.stdout.write(`${line}\n`);
|
|
406
579
|
const failures = outcomes.flatMap((outcome) => outcome.failure ?? []);
|
|
580
|
+
for (const outcome of outcomes) {
|
|
581
|
+
if (outcome.failure !== void 0) continue;
|
|
582
|
+
try {
|
|
583
|
+
const state = outcome.state ?? await authenticationState(outcome.client);
|
|
584
|
+
if (state === "connected" || state === "credentials-present") {
|
|
585
|
+
process.stdout.write(`${clientLabel(outcome.client)}: ${state === "connected" ? "connected" : "native credentials present; live connection not verified"}.\n`);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (state === "unavailable") throw new Error(`${clientLabel(outcome.client)} sign-in status is unavailable. Check that the client is installed and reachable, then retry setup. Existing configuration and credentials were preserved.`);
|
|
589
|
+
if (!outcome.created && !outcome.stableClientId) throw new Error(`${clientLabel(outcome.client)} requires sign-in: ${authenticationCommand(outcome.client)}. Setup preserved the existing registration. Clients using dynamic registration may create a new agent identity on re-login.`);
|
|
590
|
+
process.stdout.write(`${clientLabel(outcome.client)}: starting native sign-in.\n`);
|
|
591
|
+
await signIn(outcome.client);
|
|
592
|
+
const after = await authenticationState(outcome.client);
|
|
593
|
+
if (after !== "connected" && after !== "credentials-present") throw new Error(`${clientLabel(outcome.client)} did not confirm native sign-in.`);
|
|
594
|
+
process.stdout.write(`${clientLabel(outcome.client)}: ${after === "connected" ? "connected" : "native sign-in completed; live connection not verified"}.\n`);
|
|
595
|
+
} catch (error) {
|
|
596
|
+
failures.push(error instanceof Error ? error.message : "Native sign-in failed");
|
|
597
|
+
}
|
|
598
|
+
}
|
|
407
599
|
if (failures.length > 0) fail(failures.join("\n"));
|
|
408
|
-
process.stdout.write("Restart open clients
|
|
600
|
+
process.stdout.write("Restart open clients to load the configuration. Native sign-in does not verify a live MCP request; check the connection in your client.\n");
|
|
409
601
|
};
|
|
410
602
|
const status = async (args) => {
|
|
411
603
|
const clients = parseClients(args) ?? await detectedClients();
|
|
@@ -419,30 +611,33 @@ const status = async (args) => {
|
|
|
419
611
|
process.stdout.write(`${clientLabel(client)} MCP: ${installation?.configPath ?? "not installed"}\n`);
|
|
420
612
|
process.stdout.write(`${clientLabel(client)} guidance: ${guidance.installed ? guidance.path : "not installed"}\n`);
|
|
421
613
|
return installation !== void 0 && guidance.installed;
|
|
422
|
-
}).every(Boolean)) fail("AgentMuxer setup is incomplete or conflicting; run agentmuxer
|
|
614
|
+
}).every(Boolean)) fail("AgentMuxer setup is incomplete or conflicting; run npx -y @agentmuxer/setup@latest install, or remove a custom same-name MCP manually");
|
|
423
615
|
process.stdout.write("AgentMuxer configuration and guidance verified.\n");
|
|
424
|
-
|
|
425
|
-
|
|
616
|
+
const failures = [];
|
|
617
|
+
const authStates = await Promise.all(clients.map(authenticationState));
|
|
618
|
+
for (const [index, client] of clients.entries()) {
|
|
619
|
+
const state = authStates[index] ?? "unavailable";
|
|
620
|
+
process.stdout.write(`${clientLabel(client)}: ${state === "credentials-present" ? "native credentials present; live connection not verified" : state}.\n`);
|
|
621
|
+
if (state === "signed-out" || state === "unavailable") failures.push(clientLabel(client));
|
|
622
|
+
}
|
|
623
|
+
if (failures.length > 0) fail(`Native sign-in could not be confirmed for: ${failures.join(", ")}. Check the connection in your client; existing credentials were preserved.`);
|
|
426
624
|
};
|
|
427
625
|
const remove = async (args) => {
|
|
428
626
|
const clients = parseClients(args) ?? supportedClients;
|
|
429
627
|
await Promise.all(clients.map(validateClientGuidance));
|
|
430
628
|
const outcomes = await Promise.all(clients.map(async (client) => {
|
|
431
629
|
try {
|
|
432
|
-
const before = await
|
|
630
|
+
const before = await clientGuidanceStatus(client);
|
|
433
631
|
return await withClientSetupRollback(client, async (defer) => {
|
|
434
632
|
const result = await removeAgentmuxer(client);
|
|
435
|
-
if (!result.success) throw new Error(result.error
|
|
436
|
-
if (
|
|
437
|
-
|
|
438
|
-
if (!rollback.success) throw new Error(rollback.error ?? "MCP rollback failed");
|
|
439
|
-
});
|
|
440
|
-
if (before.guidance) defer(async () => {
|
|
633
|
+
if (!result.success) throw new Error("error" in result && result.error || "removal failed");
|
|
634
|
+
if ("rollback" in result) defer(result.rollback);
|
|
635
|
+
if (before.installed) defer(async () => {
|
|
441
636
|
await installClientGuidance(client);
|
|
442
637
|
});
|
|
443
638
|
const guidanceRemoved = await removeClientGuidance(client);
|
|
444
639
|
return {
|
|
445
|
-
lines: [`${clientLabel(client)} MCP: ${result.removed ? "removed" : "not installed"}`, ...guidanceRemoved ? [`${clientLabel(client)} guidance: removed`] : []],
|
|
640
|
+
lines: [`${clientLabel(client)} MCP: ${"disabled" in result && result.disabled ? "disabled; registration retained" : result.removed ? "removed" : "not installed"}`, ...guidanceRemoved ? [`${clientLabel(client)} guidance: removed`] : []],
|
|
446
641
|
failure: void 0
|
|
447
642
|
};
|
|
448
643
|
});
|