@topolo/cli 0.13.12 → 0.13.15

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/dist/index.js CHANGED
@@ -269,8 +269,8 @@ function generateRequestId() {
269
269
  }
270
270
  var TopoloSdkError = class extends Error {
271
271
  code;
272
- constructor(code, message2) {
273
- super(message2);
272
+ constructor(code, message3) {
273
+ super(message3);
274
274
  this.name = "TopoloSdkError";
275
275
  this.code = code;
276
276
  }
@@ -391,8 +391,8 @@ var TopoloAuthError = class extends TopoloSdkError {
391
391
  requestId;
392
392
  retryAfter;
393
393
  details;
394
- constructor(message2, code = "auth_error", options = {}) {
395
- super(canonicalTopoloServiceErrorCode(code) ?? "auth_error", message2);
394
+ constructor(message3, code = "auth_error", options = {}) {
395
+ super(canonicalTopoloServiceErrorCode(code) ?? "auth_error", message3);
396
396
  this.name = "TopoloAuthError";
397
397
  this.status = options.status ?? 401;
398
398
  this.body = options.body;
@@ -414,10 +414,10 @@ var TopoloPermissionError = class extends TopoloSdkError {
414
414
  requestId;
415
415
  retryAfter;
416
416
  details;
417
- constructor(message2, required, options = {}) {
417
+ constructor(message3, required, options = {}) {
418
418
  super(
419
419
  canonicalTopoloServiceErrorCode(options.code) ?? serviceErrorCode(options.body, "permission_denied"),
420
- message2
420
+ message3
421
421
  );
422
422
  this.name = "TopoloPermissionError";
423
423
  this.required = canonicalTopoloRequired(required);
@@ -458,8 +458,8 @@ function extractTopoloServiceErrorMessage(body) {
458
458
  candidates.push(record4.message, record4.error_description);
459
459
  if (typeof nested === "string" && !canonicalTopoloServiceErrorCode(nested)) candidates.push(nested);
460
460
  for (const candidate of candidates) {
461
- const message2 = sanitizeTopoloServiceMessage(candidate);
462
- if (message2) return message2;
461
+ const message3 = sanitizeTopoloServiceMessage(candidate);
462
+ if (message3) return message3;
463
463
  }
464
464
  return void 0;
465
465
  }
@@ -533,8 +533,8 @@ function validationIssueDetails(issues) {
533
533
  }
534
534
  return paths.length ? { paths, issues } : { issues };
535
535
  }
536
- function issue(path, message2) {
537
- const text3 = sanitizeTopoloServiceMessage(message2);
536
+ function issue(path, message3) {
537
+ const text3 = sanitizeTopoloServiceMessage(message3);
538
538
  if (!path && !text3) return null;
539
539
  return { ...path ? { path } : {}, ...text3 ? { message: text3 } : {} };
540
540
  }
@@ -554,8 +554,8 @@ function canonicalTopoloValidationIssues(values) {
554
554
  function flattenedValidationIssues(formErrors, fieldErrors) {
555
555
  const issues = [];
556
556
  if (Array.isArray(formErrors)) {
557
- for (const message2 of formErrors) {
558
- const projected = issue(void 0, message2);
557
+ for (const message3 of formErrors) {
558
+ const projected = issue(void 0, message3);
559
559
  if (projected) issues.push(projected);
560
560
  }
561
561
  }
@@ -564,8 +564,8 @@ function flattenedValidationIssues(formErrors, fieldErrors) {
564
564
  const path = canonicalTopoloValidationPath(key);
565
565
  if (!path) continue;
566
566
  const list = Array.isArray(messages) && messages.length ? messages : [void 0];
567
- for (const message2 of list) {
568
- const projected = issue(path, message2);
567
+ for (const message3 of list) {
568
+ const projected = issue(path, message3);
569
569
  if (projected) issues.push(projected);
570
570
  }
571
571
  }
@@ -645,8 +645,8 @@ function topoloErrorHint(error) {
645
645
  return void 0;
646
646
  }
647
647
  var TopoloConfirmationRequiredError = class extends TopoloSdkError {
648
- constructor(message2) {
649
- super("confirmation_required", message2);
648
+ constructor(message3) {
649
+ super("confirmation_required", message3);
650
650
  this.name = "TopoloConfirmationRequiredError";
651
651
  }
652
652
  };
@@ -658,8 +658,8 @@ var TopoloHttpError = class extends TopoloSdkError {
658
658
  requestId;
659
659
  retryAfter;
660
660
  details;
661
- constructor(service, path, status, body, message2, options = {}) {
662
- super(serviceErrorCode(body, "http_error"), message2 ?? `HTTP ${status} from ${service}${path}`);
661
+ constructor(service, path, status, body, message3, options = {}) {
662
+ super(serviceErrorCode(body, "http_error"), message3 ?? `HTTP ${status} from ${service}${path}`);
663
663
  this.name = "TopoloHttpError";
664
664
  this.status = status;
665
665
  this.body = body;
@@ -681,8 +681,8 @@ function topoloRetryAfterSeconds(value, now = Date.now()) {
681
681
  var TopoloRateLimitError = class extends TopoloHttpError {
682
682
  retryable = true;
683
683
  retryAfterSeconds;
684
- constructor(service, path, body, message2, options = {}) {
685
- super(service, path, 429, body, message2, options);
684
+ constructor(service, path, body, message3, options = {}) {
685
+ super(service, path, 429, body, message3, options);
686
686
  this.name = "TopoloRateLimitError";
687
687
  if (this.code === "http_error") this.code = "rate_limited";
688
688
  this.retryAfterSeconds = topoloRetryAfterSeconds(this.retryAfter);
@@ -1262,8 +1262,8 @@ var ASSURANCE_LEVELS = /* @__PURE__ */ new Set(["authenticated", "workload_ident
1262
1262
  var HASH_PATTERN = /^sha256:[a-f0-9]{64}$/;
1263
1263
  var ActorEnvelopeValidationError = class extends Error {
1264
1264
  path;
1265
- constructor(path, message2) {
1266
- super(`${path}: ${message2}`);
1265
+ constructor(path, message3) {
1266
+ super(`${path}: ${message3}`);
1267
1267
  this.name = "ActorEnvelopeValidationError";
1268
1268
  this.path = path;
1269
1269
  }
@@ -1380,8 +1380,8 @@ function parseAccountableSubject(value) {
1380
1380
  }
1381
1381
  async function sha256CanonicalValue(value) {
1382
1382
  const bytes = new TextEncoder().encode(canonicalJson(value));
1383
- const digest = await crypto.subtle.digest("SHA-256", bytes);
1384
- return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
1383
+ const digest2 = await crypto.subtle.digest("SHA-256", bytes);
1384
+ return `sha256:${Array.from(new Uint8Array(digest2), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
1385
1385
  }
1386
1386
  function parseIdentity(value, path) {
1387
1387
  const identity = record2(value, path);
@@ -1460,8 +1460,8 @@ function textArray(value, path) {
1460
1460
  function optionalTextFields(value, path, keys) {
1461
1461
  return Object.fromEntries(keys.flatMap((key) => value[key] === void 0 ? [] : [[key, text(value[key], `${path}.${key}`)]]));
1462
1462
  }
1463
- function fail(path, message2) {
1464
- throw new ActorEnvelopeValidationError(path, message2);
1463
+ function fail(path, message3) {
1464
+ throw new ActorEnvelopeValidationError(path, message3);
1465
1465
  }
1466
1466
  function parseTopoloActionUploadContract(value) {
1467
1467
  if (!isRecord3(value) || value.profile !== "inline-base64" && value.profile !== "url-session") {
@@ -1897,7 +1897,7 @@ var TopoloClient = class _TopoloClient {
1897
1897
  }
1898
1898
  if (!res.ok) {
1899
1899
  const durationMs = Date.now() - startedAt;
1900
- const message2 = describeError(parsed, `HTTP ${res.status}`);
1900
+ const message3 = describeError(parsed, `HTTP ${res.status}`);
1901
1901
  const publicCode = extractTopoloServiceErrorCode(parsed) ?? "http_error";
1902
1902
  this.emit({
1903
1903
  phase: "error",
@@ -1937,7 +1937,7 @@ var TopoloClient = class _TopoloClient {
1937
1937
  resolvedService.serviceKey,
1938
1938
  opts.path,
1939
1939
  parsed,
1940
- message2,
1940
+ message3,
1941
1941
  errorContext
1942
1942
  );
1943
1943
  }
@@ -1946,7 +1946,7 @@ var TopoloClient = class _TopoloClient {
1946
1946
  opts.path,
1947
1947
  res.status,
1948
1948
  parsed,
1949
- message2,
1949
+ message3,
1950
1950
  errorContext
1951
1951
  );
1952
1952
  }
@@ -3902,8 +3902,8 @@ import { homedir as homedir3, platform as hostPlatform } from "os";
3902
3902
  import { dirname as dirname3, join } from "path";
3903
3903
  var TopoloSdkError2 = class extends Error {
3904
3904
  code;
3905
- constructor(code, message2) {
3906
- super(message2);
3905
+ constructor(code, message3) {
3906
+ super(message3);
3907
3907
  this.name = "TopoloSdkError";
3908
3908
  this.code = code;
3909
3909
  }
@@ -4024,8 +4024,8 @@ var TopoloAuthError2 = class extends TopoloSdkError2 {
4024
4024
  requestId;
4025
4025
  retryAfter;
4026
4026
  details;
4027
- constructor(message2, code = "auth_error", options = {}) {
4028
- super(canonicalTopoloServiceErrorCode2(code) ?? "auth_error", message2);
4027
+ constructor(message3, code = "auth_error", options = {}) {
4028
+ super(canonicalTopoloServiceErrorCode2(code) ?? "auth_error", message3);
4029
4029
  this.name = "TopoloAuthError";
4030
4030
  this.status = options.status ?? 401;
4031
4031
  this.body = options.body;
@@ -4123,8 +4123,8 @@ function validationIssueDetails2(issues) {
4123
4123
  }
4124
4124
  return paths.length ? { paths, issues } : { issues };
4125
4125
  }
4126
- function issue2(path, message2) {
4127
- const text2 = sanitizeTopoloServiceMessage2(message2);
4126
+ function issue2(path, message3) {
4127
+ const text2 = sanitizeTopoloServiceMessage2(message3);
4128
4128
  if (!path && !text2) return null;
4129
4129
  return { ...path ? { path } : {}, ...text2 ? { message: text2 } : {} };
4130
4130
  }
@@ -4144,8 +4144,8 @@ function canonicalTopoloValidationIssues2(values) {
4144
4144
  function flattenedValidationIssues2(formErrors, fieldErrors) {
4145
4145
  const issues = [];
4146
4146
  if (Array.isArray(formErrors)) {
4147
- for (const message2 of formErrors) {
4148
- const projected = issue2(void 0, message2);
4147
+ for (const message3 of formErrors) {
4148
+ const projected = issue2(void 0, message3);
4149
4149
  if (projected) issues.push(projected);
4150
4150
  }
4151
4151
  }
@@ -4154,8 +4154,8 @@ function flattenedValidationIssues2(formErrors, fieldErrors) {
4154
4154
  const path = canonicalTopoloValidationPath2(key);
4155
4155
  if (!path) continue;
4156
4156
  const list = Array.isArray(messages) && messages.length ? messages : [void 0];
4157
- for (const message2 of list) {
4158
- const projected = issue2(path, message2);
4157
+ for (const message3 of list) {
4158
+ const projected = issue2(path, message3);
4159
4159
  if (projected) issues.push(projected);
4160
4160
  }
4161
4161
  }
@@ -4189,8 +4189,8 @@ var TopoloHttpError2 = class extends TopoloSdkError2 {
4189
4189
  requestId;
4190
4190
  retryAfter;
4191
4191
  details;
4192
- constructor(service, path, status, body, message2, options = {}) {
4193
- super(serviceErrorCode2(body, "http_error"), message2 ?? `HTTP ${status} from ${service}${path}`);
4192
+ constructor(service, path, status, body, message3, options = {}) {
4193
+ super(serviceErrorCode2(body, "http_error"), message3 ?? `HTTP ${status} from ${service}${path}`);
4194
4194
  this.name = "TopoloHttpError";
4195
4195
  this.status = status;
4196
4196
  this.body = body;
@@ -4212,8 +4212,8 @@ function topoloRetryAfterSeconds2(value, now = Date.now()) {
4212
4212
  var TopoloRateLimitError2 = class extends TopoloHttpError2 {
4213
4213
  retryable = true;
4214
4214
  retryAfterSeconds;
4215
- constructor(service, path, body, message2, options = {}) {
4216
- super(service, path, 429, body, message2, options);
4215
+ constructor(service, path, body, message3, options = {}) {
4216
+ super(service, path, 429, body, message3, options);
4217
4217
  this.name = "TopoloRateLimitError";
4218
4218
  if (this.code === "http_error") this.code = "rate_limited";
4219
4219
  this.retryAfterSeconds = topoloRetryAfterSeconds2(this.retryAfter);
@@ -4221,8 +4221,8 @@ var TopoloRateLimitError2 = class extends TopoloHttpError2 {
4221
4221
  };
4222
4222
  var TopoloReauthRequiredError = class extends TopoloAuthError2 {
4223
4223
  reauthorization;
4224
- constructor(message2, reauthorization, options = {}) {
4225
- super(message2, "reauth_required", { status: 401, service: "auth", path: "/api/developer-oauth/token", ...options });
4224
+ constructor(message3, reauthorization, options = {}) {
4225
+ super(message3, "reauth_required", { status: 401, service: "auth", path: "/api/developer-oauth/token", ...options });
4226
4226
  this.name = "TopoloReauthRequiredError";
4227
4227
  this.reauthorization = Object.freeze({ ...reauthorization });
4228
4228
  }
@@ -5228,12 +5228,12 @@ function renewalFailure(error) {
5228
5228
  if (error instanceof ConfigOwnershipError) return new ConfigOwnershipError();
5229
5229
  if (error instanceof TopoloReauthRequiredError) return error;
5230
5230
  if ((error instanceof TopoloAuthError2 || error instanceof TopoloHttpError2) && error.status === 429) {
5231
- const message2 = "Topolo sign-in renewal is temporarily rate limited by Auth. Retry after the Retry-After interval; the saved sign-in has been retained.";
5231
+ const message3 = "Topolo sign-in renewal is temporarily rate limited by Auth. Retry after the Retry-After interval; the saved sign-in has been retained.";
5232
5232
  return new TopoloRateLimitError2(
5233
5233
  "auth",
5234
5234
  path,
5235
- { code: "rate_limited", retryable: true, message: message2 },
5236
- message2,
5235
+ { code: "rate_limited", retryable: true, message: message3 },
5236
+ message3,
5237
5237
  { requestId: error.requestId, retryAfter: error.retryAfter }
5238
5238
  );
5239
5239
  }
@@ -6520,6 +6520,363 @@ function formatActionPlan(plan) {
6520
6520
  return lines.join("\n");
6521
6521
  }
6522
6522
 
6523
+ // src/lib/credential-custody.ts
6524
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
6525
+ import { constants as constants2 } from "fs";
6526
+ import { chmod as chmod3, lstat as lstat2, mkdir as mkdir3, open, rename as rename2, rm as rm2 } from "fs/promises";
6527
+ import { basename as basename2, isAbsolute, join as join5, normalize, sep } from "path";
6528
+ var CUSTODY_SCHEMA_VERSION = 1;
6529
+ var CUSTODY_API_KEY_FILE = "api-key.json";
6530
+ var CUSTODY_METADATA_FILE = "custody.json";
6531
+ var API_KEYS_LIST_ACTION = "app_topolo_auth.api_keys.list";
6532
+ var CUSTODY_ENVIRONMENT_NAMES = ["production", "staging", "development", "local"];
6533
+ var CustodyError = class extends Error {
6534
+ code;
6535
+ constructor(code, message3) {
6536
+ super(message3);
6537
+ this.name = "CustodyError";
6538
+ this.code = code;
6539
+ }
6540
+ };
6541
+ async function writeCredentialCustody(request, deps) {
6542
+ if (request.confirm !== true) {
6543
+ throw new CustodyError(
6544
+ "custody_confirmation_required",
6545
+ "Writing a credential into custody requires --confirm. The custody file will hold the plaintext key."
6546
+ );
6547
+ }
6548
+ const appId = requiredText(request.appId, "--app");
6549
+ const apiKeyId = requiredText(request.apiKeyId, "--api-key-id");
6550
+ const platform4 = deps.platform ?? process.platform;
6551
+ const uid = deps.uid === void 0 ? currentUid() : deps.uid;
6552
+ if (platform4 === "win32" || uid === null) {
6553
+ throw new CustodyError("custody_platform_unsupported", "Credential custody files require POSIX ownership and permissions.");
6554
+ }
6555
+ const directory = custodyDirectory(request.outDir);
6556
+ const leaf = basename2(directory);
6557
+ if (CUSTODY_ENVIRONMENT_NAMES.includes(leaf) && leaf !== deps.environment) {
6558
+ throw new CustodyError(
6559
+ "custody_environment_mismatch",
6560
+ `--out names the ${leaf} environment but the credential is for ${deps.environment}.`
6561
+ );
6562
+ }
6563
+ await ensureCustodyDirectory(directory, uid);
6564
+ const apiKeyPath = join5(directory, CUSTODY_API_KEY_FILE);
6565
+ const custodyPath = join5(directory, CUSTODY_METADATA_FILE);
6566
+ const existing = [await destinationExists(apiKeyPath), await destinationExists(custodyPath)];
6567
+ if (existing.some(Boolean) && request.replace !== true) {
6568
+ throw new CustodyError(
6569
+ "custody_file_exists",
6570
+ `Custody files already exist in ${directory}. Pass --replace to overwrite them.`
6571
+ );
6572
+ }
6573
+ const identity = await deps.identity();
6574
+ const organizationId = identity.organization?.id;
6575
+ if (!organizationId) {
6576
+ throw new CustodyError(
6577
+ "custody_organization_required",
6578
+ "Credential custody requires an organization context. Pin one with TOPOLO_CONTEXT=org:<organizationId>."
6579
+ );
6580
+ }
6581
+ const key = findListedKey(await deps.listApiKeys(appId), apiKeyId);
6582
+ assertKeyUsable(key, appId, organizationId, (deps.now ?? Date.now)());
6583
+ const secret = await deps.store.load(request.reference);
6584
+ if (typeof secret !== "string" || !secret || /\s/.test(secret)) {
6585
+ throw new CustodyError(
6586
+ "custody_secret_unsupported_type",
6587
+ "The protected reference does not hold a single API key string."
6588
+ );
6589
+ }
6590
+ try {
6591
+ await assertSecretIsKey(deps.introspectKey, secret, key);
6592
+ const custody = {
6593
+ schemaVersion: CUSTODY_SCHEMA_VERSION,
6594
+ appId: key.appId,
6595
+ scopes: [...key.scopes],
6596
+ organizationId: key.organizationId,
6597
+ environment: deps.environment,
6598
+ apiKeyId: key.id,
6599
+ expiresAt: key.expiresAt,
6600
+ keyDigest: `sha256:${createHash3("sha256").update(secret, "utf8").digest("hex")}`,
6601
+ createdAt: key.createdAt
6602
+ };
6603
+ await writeCustodyFiles(directory, [
6604
+ [CUSTODY_API_KEY_FILE, `${JSON.stringify({ schemaVersion: CUSTODY_SCHEMA_VERSION, apiKey: secret })}
6605
+ `],
6606
+ [CUSTODY_METADATA_FILE, `${JSON.stringify(custody, null, 2)}
6607
+ `]
6608
+ ]);
6609
+ try {
6610
+ await deps.store.delete(request.reference);
6611
+ } catch {
6612
+ throw new CustodyError(
6613
+ "custody_reference_release_failed",
6614
+ `Custody files were written to ${directory}, but the local protected reference could not be deleted. Run \`topolo actions release-secret <reference> --confirm\`.`
6615
+ );
6616
+ }
6617
+ return {
6618
+ written: true,
6619
+ replaced: existing.some(Boolean),
6620
+ directory,
6621
+ files: { apiKey: apiKeyPath, custody: custodyPath },
6622
+ custody,
6623
+ reference: { released: true, upstreamRevoked: false }
6624
+ };
6625
+ } catch (error) {
6626
+ throw withheldIfSecretLeaks(error, secret);
6627
+ }
6628
+ }
6629
+ function requiredText(value, flag) {
6630
+ const text2 = value?.trim();
6631
+ if (!text2) throw new CustodyError("custody_invalid_input", `${flag} is required.`);
6632
+ return text2;
6633
+ }
6634
+ function currentUid() {
6635
+ return typeof process.getuid === "function" ? process.getuid() : null;
6636
+ }
6637
+ function custodyDirectory(value) {
6638
+ const raw = value?.trim();
6639
+ if (!raw || !isAbsolute(raw)) {
6640
+ throw new CustodyError("custody_path_not_absolute", "--out must be an absolute directory path.");
6641
+ }
6642
+ const normalized = normalize(raw);
6643
+ const directory = normalized.endsWith(sep) ? normalized.slice(0, -1) : normalized;
6644
+ if (!directory) {
6645
+ throw new CustodyError("custody_dir_insecure", "--out must name a dedicated directory, not the filesystem root.");
6646
+ }
6647
+ return directory;
6648
+ }
6649
+ async function lstatOrNull(path) {
6650
+ try {
6651
+ return await lstat2(path);
6652
+ } catch (error) {
6653
+ if (error.code === "ENOENT") return null;
6654
+ throw error;
6655
+ }
6656
+ }
6657
+ async function ensureCustodyDirectory(directory, uid) {
6658
+ const components = directory.split(sep).filter(Boolean);
6659
+ let current = sep;
6660
+ assertSecureAncestor(current, await lstat2(current), uid);
6661
+ for (let index = 0; index < components.length; index += 1) {
6662
+ current = join5(current, components[index]);
6663
+ const last = index === components.length - 1;
6664
+ let stats = await lstatOrNull(current);
6665
+ if (!stats) {
6666
+ await mkdir3(current, { mode: 448 });
6667
+ await chmod3(current, 448);
6668
+ stats = await lstat2(current);
6669
+ }
6670
+ if (stats.isSymbolicLink()) {
6671
+ throw new CustodyError("custody_path_symlink", `Custody path component ${current} is a symbolic link.`);
6672
+ }
6673
+ if (!stats.isDirectory()) {
6674
+ throw new CustodyError("custody_path_not_directory", `Custody path component ${current} is not a directory.`);
6675
+ }
6676
+ if (last) assertPrivateDirectory(current, stats, uid);
6677
+ else assertSecureAncestor(current, stats, uid);
6678
+ }
6679
+ }
6680
+ function assertPrivateDirectory(path, stats, uid) {
6681
+ if (stats.uid !== uid || (stats.mode & 511) !== 448) {
6682
+ throw new CustodyError(
6683
+ "custody_dir_insecure",
6684
+ `Custody directory ${path} must be owned by the current user with mode 0700.`
6685
+ );
6686
+ }
6687
+ }
6688
+ function assertSecureAncestor(path, stats, uid) {
6689
+ const rootSticky = stats.uid === 0 && (stats.mode & 512) !== 0;
6690
+ if (stats.uid !== 0 && stats.uid !== uid || (stats.mode & 18) !== 0 && !rootSticky) {
6691
+ throw new CustodyError(
6692
+ "custody_parent_insecure",
6693
+ `Custody parent ${path} must be owned by root or the current user and must not be group- or world-writable.`
6694
+ );
6695
+ }
6696
+ }
6697
+ async function destinationExists(path) {
6698
+ const stats = await lstatOrNull(path);
6699
+ if (!stats) return false;
6700
+ if (stats.isSymbolicLink()) {
6701
+ throw new CustodyError("custody_path_symlink", `Custody file ${path} is a symbolic link.`);
6702
+ }
6703
+ if (!stats.isFile()) {
6704
+ throw new CustodyError("custody_path_not_regular", `Custody file ${path} is not a regular file.`);
6705
+ }
6706
+ return true;
6707
+ }
6708
+ function isRecord5(value) {
6709
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6710
+ }
6711
+ function findListedKey(result, apiKeyId) {
6712
+ const data = isRecord5(result) && isRecord5(result.data) ? result.data : result;
6713
+ const keys = isRecord5(data) ? data.keys : void 0;
6714
+ if (!Array.isArray(keys)) {
6715
+ throw new CustodyError("custody_metadata_unavailable", "Auth api_keys.list did not return a key inventory.");
6716
+ }
6717
+ const match = keys.find((key) => isRecord5(key) && key.id === apiKeyId);
6718
+ if (!match) {
6719
+ throw new CustodyError(
6720
+ "custody_key_not_found",
6721
+ `API key ${apiKeyId} is not visible to this credential in api_keys.list.`
6722
+ );
6723
+ }
6724
+ const { id, appId, organizationId, scopes, expiresAt, createdAt, status } = match;
6725
+ if (typeof id !== "string" || typeof appId !== "string" || typeof organizationId !== "string" || !Array.isArray(scopes) || !scopes.every((scope) => typeof scope === "string") || !(expiresAt === null || typeof expiresAt === "number") || typeof createdAt !== "number" || typeof status !== "string") {
6726
+ throw new CustodyError("custody_metadata_unavailable", `Auth returned incomplete metadata for API key ${apiKeyId}.`);
6727
+ }
6728
+ return { id, appId, organizationId, scopes, expiresAt, createdAt, status };
6729
+ }
6730
+ function assertKeyUsable(key, appId, organizationId, nowMs) {
6731
+ if (key.appId !== appId) {
6732
+ throw new CustodyError("custody_app_mismatch", `API key ${key.id} belongs to ${key.appId}, not ${appId}.`);
6733
+ }
6734
+ if (key.organizationId !== organizationId) {
6735
+ throw new CustodyError(
6736
+ "custody_organization_mismatch",
6737
+ `API key ${key.id} belongs to another organization than the verified credential.`
6738
+ );
6739
+ }
6740
+ if (key.status !== "active") {
6741
+ throw new CustodyError("custody_key_inactive", `API key ${key.id} is ${key.status}, not active.`);
6742
+ }
6743
+ if (key.expiresAt !== null && key.expiresAt * 1e3 <= nowMs) {
6744
+ throw new CustodyError("custody_key_expired", `API key ${key.id} has expired.`);
6745
+ }
6746
+ }
6747
+ async function assertSecretIsKey(introspectKey, secret, key) {
6748
+ let identity;
6749
+ try {
6750
+ identity = await introspectKey(secret);
6751
+ } catch (error) {
6752
+ const code = error.code;
6753
+ const status = error.status;
6754
+ const detail = [typeof code === "string" ? code : null, typeof status === "number" ? `HTTP ${status}` : null].filter(Boolean).join(", ");
6755
+ throw new CustodyError(
6756
+ "custody_key_verification_failed",
6757
+ `Auth could not verify the protected key${detail ? ` (${detail})` : ""}. Nothing was written.`
6758
+ );
6759
+ }
6760
+ if (identity.kind !== "api_key" || identity.apiKey?.id !== key.id || identity.apiKey.appId !== key.appId || identity.organization?.id !== key.organizationId) {
6761
+ throw new CustodyError(
6762
+ "custody_key_mismatch",
6763
+ `The protected reference does not hold API key ${key.id} for ${key.appId}. Nothing was written.`
6764
+ );
6765
+ }
6766
+ const introspected = [...identity.apiKey.scopes].sort();
6767
+ const listed = [...key.scopes].sort();
6768
+ if (introspected.length !== listed.length || introspected.some((scope, index) => scope !== listed[index])) {
6769
+ throw new CustodyError(
6770
+ "custody_metadata_mismatch",
6771
+ `Auth introspection and api_keys.list disagree on the scopes of API key ${key.id}. Nothing was written.`
6772
+ );
6773
+ }
6774
+ }
6775
+ async function writeCustodyFiles(directory, files) {
6776
+ const staged = [];
6777
+ try {
6778
+ for (const [name, content] of files) {
6779
+ const temporary = join5(directory, `.${name}.${process.pid}.${randomUUID3()}.tmp`);
6780
+ staged.push([temporary, join5(directory, name)]);
6781
+ const handle = await open(
6782
+ temporary,
6783
+ constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW,
6784
+ 384
6785
+ );
6786
+ try {
6787
+ await handle.chmod(384);
6788
+ await handle.writeFile(content, "utf8");
6789
+ await handle.sync();
6790
+ } finally {
6791
+ await handle.close();
6792
+ }
6793
+ }
6794
+ for (const [temporary, destination] of staged) await rename2(temporary, destination);
6795
+ } finally {
6796
+ await Promise.all(staged.map(([temporary]) => rm2(temporary, { force: true })));
6797
+ }
6798
+ }
6799
+ function withheldIfSecretLeaks(error, secret) {
6800
+ const variants = [secret, encodeURIComponent(secret)];
6801
+ const record3 = error;
6802
+ const parts = [String(error), String(record3?.message), String(record3?.stack)];
6803
+ for (const value of [record3?.body, record3?.details]) {
6804
+ try {
6805
+ parts.push(String(value), JSON.stringify(value ?? null));
6806
+ } catch {
6807
+ }
6808
+ }
6809
+ if (!parts.some((part) => variants.some((variant) => part.includes(variant)))) return error;
6810
+ return new CustodyError(
6811
+ "custody_error_withheld",
6812
+ "Credential custody failed; the underlying error was withheld because it referenced the protected credential."
6813
+ );
6814
+ }
6815
+
6816
+ // src/commands/credentials.ts
6817
+ function registerCredentialsCommand(program2) {
6818
+ const credentials = program2.command("credentials").description("Move protected credentials into operator-owned custody without displaying them");
6819
+ credentials.command("write-custody <reference>").description(
6820
+ "Write the API key behind a protected topolo-secret reference into a 0700 custody directory, then delete the reference"
6821
+ ).option("--out <dir>", "Absolute custody directory (created 0700, or an existing 0700 directory you own)").option("--app <appId>", "Application id the key must belong to").option("--api-key-id <id>", "Auth API key id the reference must hold (data.key.id from api_keys.create)").option("--confirm", "Acknowledge writing the plaintext key to disk").option("--replace", "Atomically replace existing custody files").option("--json", "Output JSON").option("--no-json", "Output human-readable text").addHelpText(
6822
+ "after",
6823
+ '\nThe reference is resolved through the same OS protected store that `actions call` and the stdio MCP use,\nbound to the verified principal, organization, API key identity and environment. Key metadata is read back\nfrom Auth api_keys.list and the key is verified by Auth introspection before anything is written.\nWrites api-key.json ({"schemaVersion":1,"apiKey":...}) and custody.json (metadata plus sha256 keyDigest),\nboth 0600 via temporary file and rename. Output never contains the key. Deleting the local reference does\nnot revoke the upstream key; use app_topolo_auth.api_keys.revoke for that.\nKeep one key per environment with --out <dir>/<environment>; a directory named after an environment must\nmatch the credential environment (topolo --env <environment>).'
6824
+ ).action(async (reference, opts) => {
6825
+ const mode = resolveOutputMode(opts.json);
6826
+ try {
6827
+ const environment = selectedEnvironment();
6828
+ let client;
6829
+ const topolo = () => client ??= buildClient({ environment }).then((built) => {
6830
+ if (!built) throw new Error("No credential configured. Run `topolo auth login`.");
6831
+ return built;
6832
+ });
6833
+ let verified;
6834
+ const identity = () => verified ??= topolo().then((built) => built.client.introspect());
6835
+ const result = await writeCredentialCustody(
6836
+ {
6837
+ reference,
6838
+ outDir: opts.out,
6839
+ appId: opts.app,
6840
+ apiKeyId: opts.apiKeyId,
6841
+ confirm: opts.confirm === true,
6842
+ replace: opts.replace === true
6843
+ },
6844
+ {
6845
+ environment,
6846
+ identity,
6847
+ store: createProtectedActionSecretStore({ environment, identity }),
6848
+ listApiKeys: async (appId) => (await topolo()).client.readAction(API_KEYS_LIST_ACTION, { appId }),
6849
+ introspectKey: async (secret) => {
6850
+ const profile = resolvedProfileConfig(await readConfig(), environment);
6851
+ return createTopolo({
6852
+ credential: { kind: "api_key", apiKey: secret },
6853
+ environment,
6854
+ agent: { clientName: "topolo-cli", clientVersion: CLI_VERSION },
6855
+ serviceUrls: profile.serviceUrls
6856
+ }).client.introspect();
6857
+ }
6858
+ }
6859
+ );
6860
+ emit(result, mode, () => formatCustodyResult(result));
6861
+ } catch (error) {
6862
+ fail2(error, mode);
6863
+ }
6864
+ });
6865
+ }
6866
+ function formatCustodyResult(result) {
6867
+ return [
6868
+ `${result.replaced ? "Replaced" : "Wrote"} credential custody in ${result.directory}`,
6869
+ `API key file: ${result.files.apiKey} (0600)`,
6870
+ `Custody file: ${result.files.custody} (0600)`,
6871
+ `Key: ${result.custody.apiKeyId} (${result.custody.appId}, ${result.custody.environment})`,
6872
+ `Organization: ${result.custody.organizationId}`,
6873
+ `Scopes: ${result.custody.scopes.join(", ") || "-"}`,
6874
+ `Expires at: ${result.custody.expiresAt === null ? "never" : new Date(result.custody.expiresAt * 1e3).toISOString()}`,
6875
+ `Digest: ${result.custody.keyDigest}`,
6876
+ "Local protected reference released. The upstream key is NOT revoked; use app_topolo_auth.api_keys.revoke for that."
6877
+ ].join("\n");
6878
+ }
6879
+
6523
6880
  // src/commands/apps-public.ts
6524
6881
  function publicApplicationRows(catalog) {
6525
6882
  return catalog.filter((entry) => entry.launchable || entry.surfaceKind === "application").map((entry) => ({
@@ -6809,7 +7166,7 @@ function normalizeList(values, fallback = []) {
6809
7166
 
6810
7167
  // src/commands/integrations.ts
6811
7168
  import { readdir as readdir3, readFile as readFile3, stat } from "fs/promises";
6812
- import { join as join5, relative } from "path";
7169
+ import { join as join6, relative } from "path";
6813
7170
  function registerIntegrationsCommand(program2) {
6814
7171
  const integrations = program2.command("integrations").description("Generate and validate Topolo integration handoffs.");
6815
7172
  integrations.command("prompt").description("Print an agent prompt for adding a Topolo integration.").argument("<integration>", "Integration prompt id, currently login-with-topolo").option("--framework <framework>", "auto, authjs, express, or generic", "auto").option("--json", "Emit JSON output").action((integration, options) => {
@@ -6952,7 +7309,7 @@ function detectFramework(packageJson, haystack) {
6952
7309
  }
6953
7310
  async function readPackageJson(repoRoot) {
6954
7311
  try {
6955
- return JSON.parse(await readFile3(join5(repoRoot, "package.json"), "utf8"));
7312
+ return JSON.parse(await readFile3(join6(repoRoot, "package.json"), "utf8"));
6956
7313
  } catch {
6957
7314
  return null;
6958
7315
  }
@@ -6964,7 +7321,7 @@ async function collectFiles(repoRoot) {
6964
7321
  if (result.length > 2e3) return;
6965
7322
  const entries = await readdir3(dir, { withFileTypes: true });
6966
7323
  for (const entry of entries) {
6967
- const fullPath = join5(dir, entry.name);
7324
+ const fullPath = join6(dir, entry.name);
6968
7325
  if (entry.isDirectory()) {
6969
7326
  if ([".git", "node_modules", "dist", "build", ".next", "coverage"].includes(entry.name)) continue;
6970
7327
  await walk(fullPath);
@@ -6991,11 +7348,11 @@ function formatCheckResult(result) {
6991
7348
  }
6992
7349
 
6993
7350
  // src/commands/skills.ts
6994
- import { spawnSync as spawnSync4 } from "child_process";
7351
+ import { spawnSync as spawnSync5 } from "child_process";
6995
7352
  import { existsSync as existsSync6 } from "fs";
6996
- import { readFile as readFile6, rm as rm2 } from "fs/promises";
7353
+ import { readFile as readFile9, rm as rm3 } from "fs/promises";
6997
7354
  import { homedir as homedir5 } from "os";
6998
- import { dirname as dirname6, join as join7, resolve as resolve3 } from "path";
7355
+ import { dirname as dirname7, join as join11, resolve as resolve3 } from "path";
6999
7356
  import { fileURLToPath as fileURLToPath3 } from "url";
7000
7357
  import { createRequire } from "module";
7001
7358
 
@@ -7004,7 +7361,7 @@ import { spawnSync as spawnSync3 } from "child_process";
7004
7361
  import { existsSync as existsSync4 } from "fs";
7005
7362
  import { readFile as readFile4 } from "fs/promises";
7006
7363
  import { homedir as homedir4 } from "os";
7007
- import { isAbsolute, join as join6, resolve as resolve2 } from "path";
7364
+ import { isAbsolute as isAbsolute2, join as join7, resolve as resolve2 } from "path";
7008
7365
  var SUPPORTED_AGENT_IDS = [
7009
7366
  "claude",
7010
7367
  "claude-desktop",
@@ -7047,13 +7404,13 @@ var AGENT_NAME_FOR = {
7047
7404
  function resolveAgentHostTarget(options) {
7048
7405
  const homeDir = resolve2(options.homeDir ?? homedir4());
7049
7406
  const codexHome = options.codexHome ?? (options.homeDir === void 0 ? process.env.CODEX_HOME || void 0 : void 0);
7050
- if (codexHome !== void 0 && (!codexHome.trim() || !isAbsolute(codexHome))) {
7407
+ if (codexHome !== void 0 && (!codexHome.trim() || !isAbsolute2(codexHome))) {
7051
7408
  throw new Error("Codex home must be an absolute nonempty directory.");
7052
7409
  }
7053
7410
  return Object.freeze({
7054
7411
  homeDir,
7055
7412
  ...options.projectDir ? { projectDir: resolve2(options.projectDir) } : {},
7056
- codexHome: codexHome ?? join6(homeDir, ".codex")
7413
+ codexHome: codexHome ?? join7(homeDir, ".codex")
7057
7414
  });
7058
7415
  }
7059
7416
  var RESERVED_OPERATOR_REGISTRATION = ["topolo", "platform"].join("-");
@@ -7062,7 +7419,7 @@ function isPublicRegistrationName(name) {
7062
7419
  }
7063
7420
  function selectStableNodeCommand(whichOutput, fallback = process.execPath) {
7064
7421
  const candidate = whichOutput?.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
7065
- return candidate && isAbsolute(candidate) ? candidate : fallback;
7422
+ return candidate && isAbsolute2(candidate) ? candidate : fallback;
7066
7423
  }
7067
7424
  function stableNodeCommand() {
7068
7425
  const probe = spawnSync3(process.platform === "win32" ? "where" : "which", ["node"], {
@@ -7074,23 +7431,23 @@ function stableNodeCommand() {
7074
7431
  function agentSkillPath(agent, homeDir = homedir4(), projectDir, codexHome) {
7075
7432
  switch (agent) {
7076
7433
  case "claude":
7077
- return projectDir ? join6(projectDir, ".claude", "skills", "topolo", "SKILL.md") : join6(homeDir, ".claude", "skills", "topolo", "SKILL.md");
7434
+ return projectDir ? join7(projectDir, ".claude", "skills", "topolo", "SKILL.md") : join7(homeDir, ".claude", "skills", "topolo", "SKILL.md");
7078
7435
  case "claude-desktop":
7079
- return join6(homeDir, ".claude", "skills", "topolo", "SKILL.md");
7436
+ return join7(homeDir, ".claude", "skills", "topolo", "SKILL.md");
7080
7437
  case "codex":
7081
- return join6(codexHome ?? join6(homeDir, ".codex"), "skills", "topolo", "SKILL.md");
7438
+ return join7(codexHome ?? join7(homeDir, ".codex"), "skills", "topolo", "SKILL.md");
7082
7439
  case "cursor":
7083
- return join6(projectDir ?? homeDir, ".cursor", "rules", "topolo.mdc");
7440
+ return join7(projectDir ?? homeDir, ".cursor", "rules", "topolo.mdc");
7084
7441
  case "grok":
7085
- return join6(projectDir ?? homeDir, ".grok", "skills", "topolo", "SKILL.md");
7442
+ return join7(projectDir ?? homeDir, ".grok", "skills", "topolo", "SKILL.md");
7086
7443
  case "gemini":
7087
- return join6(projectDir ?? homeDir, ".gemini", "skills", "topolo", "SKILL.md");
7444
+ return join7(projectDir ?? homeDir, ".gemini", "skills", "topolo", "SKILL.md");
7088
7445
  case "copilot":
7089
- return projectDir ? join6(projectDir, ".github", "skills", "topolo", "SKILL.md") : join6(homeDir, ".copilot", "skills", "topolo", "SKILL.md");
7446
+ return projectDir ? join7(projectDir, ".github", "skills", "topolo", "SKILL.md") : join7(homeDir, ".copilot", "skills", "topolo", "SKILL.md");
7090
7447
  case "windsurf":
7091
- return projectDir ? join6(projectDir, ".windsurf", "rules", "topolo.md") : void 0;
7448
+ return projectDir ? join7(projectDir, ".windsurf", "rules", "topolo.md") : void 0;
7092
7449
  case "cline":
7093
- return join6(projectDir ?? homeDir, ".cline", "skills", "topolo", "SKILL.md");
7450
+ return join7(projectDir ?? homeDir, ".cline", "skills", "topolo", "SKILL.md");
7094
7451
  case "goose":
7095
7452
  case "amazon-q":
7096
7453
  return void 0;
@@ -7099,7 +7456,7 @@ function agentSkillPath(agent, homeDir = homedir4(), projectDir, codexHome) {
7099
7456
  function agentConfigPaths(agent, homeDir = homedir4(), projectDir, codexHome) {
7100
7457
  switch (agent) {
7101
7458
  case "claude":
7102
- return [{ path: projectDir ? join6(projectDir, ".mcp.json") : join6(homeDir, ".claude.json"), format: "json", rootKey: "mcpServers" }];
7459
+ return [{ path: projectDir ? join7(projectDir, ".mcp.json") : join7(homeDir, ".claude.json"), format: "json", rootKey: "mcpServers" }];
7103
7460
  case "claude-desktop":
7104
7461
  return [{
7105
7462
  path: claudeDesktopConfigPath(homeDir),
@@ -7107,31 +7464,31 @@ function agentConfigPaths(agent, homeDir = homedir4(), projectDir, codexHome) {
7107
7464
  rootKey: "mcpServers"
7108
7465
  }];
7109
7466
  case "codex":
7110
- return [{ path: join6(codexHome ?? join6(homeDir, ".codex"), "config.toml"), format: "toml" }];
7467
+ return [{ path: join7(codexHome ?? join7(homeDir, ".codex"), "config.toml"), format: "toml" }];
7111
7468
  case "cursor":
7112
7469
  return [{
7113
- path: projectDir ? join6(projectDir, ".cursor", "mcp.json") : join6(homeDir, ".cursor", "mcp.json"),
7470
+ path: projectDir ? join7(projectDir, ".cursor", "mcp.json") : join7(homeDir, ".cursor", "mcp.json"),
7114
7471
  format: "json",
7115
7472
  rootKey: "mcpServers"
7116
7473
  }];
7117
7474
  case "grok":
7118
7475
  return [{
7119
- path: projectDir ? join6(projectDir, ".grok", "config.toml") : join6(homeDir, ".grok", "config.toml"),
7476
+ path: projectDir ? join7(projectDir, ".grok", "config.toml") : join7(homeDir, ".grok", "config.toml"),
7120
7477
  format: "toml"
7121
7478
  }];
7122
7479
  case "gemini":
7123
7480
  return [{
7124
- path: projectDir ? join6(projectDir, ".gemini", "settings.json") : join6(homeDir, ".gemini", "settings.json"),
7481
+ path: projectDir ? join7(projectDir, ".gemini", "settings.json") : join7(homeDir, ".gemini", "settings.json"),
7125
7482
  format: "json",
7126
7483
  rootKey: "mcpServers"
7127
7484
  }];
7128
7485
  case "copilot":
7129
7486
  return projectDir ? [
7130
- { path: join6(projectDir, ".mcp.json"), format: "json", rootKey: "mcpServers" },
7131
- { path: join6(projectDir, ".vscode", "mcp.json"), format: "json", rootKey: "servers" }
7487
+ { path: join7(projectDir, ".mcp.json"), format: "json", rootKey: "mcpServers" },
7488
+ { path: join7(projectDir, ".vscode", "mcp.json"), format: "json", rootKey: "servers" }
7132
7489
  ] : [
7133
7490
  {
7134
- path: join6(homeDir, ".copilot", "mcp-config.json"),
7491
+ path: join7(homeDir, ".copilot", "mcp-config.json"),
7135
7492
  format: "json",
7136
7493
  rootKey: "mcpServers"
7137
7494
  },
@@ -7139,24 +7496,24 @@ function agentConfigPaths(agent, homeDir = homedir4(), projectDir, codexHome) {
7139
7496
  ];
7140
7497
  case "windsurf":
7141
7498
  return [{
7142
- path: join6(homeDir, ".codeium", "windsurf", "mcp_config.json"),
7499
+ path: join7(homeDir, ".codeium", "windsurf", "mcp_config.json"),
7143
7500
  format: "json",
7144
7501
  rootKey: "mcpServers"
7145
7502
  }];
7146
7503
  case "cline":
7147
7504
  return [{
7148
- path: projectDir ? join6(projectDir, ".cline", "mcp.json") : join6(homeDir, ".cline", "mcp.json"),
7505
+ path: projectDir ? join7(projectDir, ".cline", "mcp.json") : join7(homeDir, ".cline", "mcp.json"),
7149
7506
  format: "json",
7150
7507
  rootKey: "mcpServers"
7151
7508
  }];
7152
7509
  case "goose":
7153
- return [{ path: join6(homeDir, ".config", "goose", "config.yaml"), format: "yaml" }];
7510
+ return [{ path: join7(homeDir, ".config", "goose", "config.yaml"), format: "yaml" }];
7154
7511
  case "amazon-q":
7155
7512
  if (projectDir) {
7156
- return [{ path: join6(projectDir, ".amazonq", "mcp.json"), format: "json", rootKey: "mcpServers" }];
7513
+ return [{ path: join7(projectDir, ".amazonq", "mcp.json"), format: "json", rootKey: "mcpServers" }];
7157
7514
  }
7158
7515
  return [{
7159
- path: existsSync4(join6(homeDir, ".aws", "amazonq", "mcp.json")) ? join6(homeDir, ".aws", "amazonq", "mcp.json") : join6(homeDir, ".aws", "amazonq", "default.json"),
7516
+ path: existsSync4(join7(homeDir, ".aws", "amazonq", "mcp.json")) ? join7(homeDir, ".aws", "amazonq", "mcp.json") : join7(homeDir, ".aws", "amazonq", "default.json"),
7160
7517
  format: "json",
7161
7518
  rootKey: "mcpServers"
7162
7519
  }];
@@ -7192,7 +7549,7 @@ function agentConfigExists(agent, homeDir = homedir4(), projectDir, codexHome) {
7192
7549
  return agentConfigPaths(agent, homeDir, projectDir, codexHome).some((config) => existsSync4(config.path));
7193
7550
  }
7194
7551
  async function assertClaudeProjectScope(homeDir, projectDir, name) {
7195
- const path = join6(homeDir, ".claude.json");
7552
+ const path = join7(homeDir, ".claude.json");
7196
7553
  let contents;
7197
7554
  try {
7198
7555
  contents = await readFile4(path, "utf8");
@@ -7220,14 +7577,14 @@ async function assertClaudeProjectScope(homeDir, projectDir, name) {
7220
7577
  }
7221
7578
  }
7222
7579
  function claudeDesktopConfigPath(homeDir) {
7223
- if (process.platform === "darwin") return join6(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json");
7224
- if (process.platform === "win32") return join6(process.env.APPDATA ?? join6(homeDir, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
7225
- return join6(process.env.XDG_CONFIG_HOME ?? join6(homeDir, ".config"), "Claude", "claude_desktop_config.json");
7580
+ if (process.platform === "darwin") return join7(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json");
7581
+ if (process.platform === "win32") return join7(process.env.APPDATA ?? join7(homeDir, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
7582
+ return join7(process.env.XDG_CONFIG_HOME ?? join7(homeDir, ".config"), "Claude", "claude_desktop_config.json");
7226
7583
  }
7227
7584
  function vscodeUserConfigPath(homeDir) {
7228
- if (process.platform === "darwin") return join6(homeDir, "Library", "Application Support", "Code", "User", "mcp.json");
7229
- if (process.platform === "win32") return join6(process.env.APPDATA ?? join6(homeDir, "AppData", "Roaming"), "Code", "User", "mcp.json");
7230
- return join6(process.env.XDG_CONFIG_HOME ?? join6(homeDir, ".config"), "Code", "User", "mcp.json");
7585
+ if (process.platform === "darwin") return join7(homeDir, "Library", "Application Support", "Code", "User", "mcp.json");
7586
+ if (process.platform === "win32") return join7(process.env.APPDATA ?? join7(homeDir, "AppData", "Roaming"), "Code", "User", "mcp.json");
7587
+ return join7(process.env.XDG_CONFIG_HOME ?? join7(homeDir, ".config"), "Code", "User", "mcp.json");
7231
7588
  }
7232
7589
  async function readAgentRegistrations(agent, homeDir = homedir4(), projectDir, codexHome) {
7233
7590
  const registrations = [];
@@ -7286,7 +7643,7 @@ async function readTomlRegistrations(path, agent) {
7286
7643
  if (!section || !isPublicRegistrationName(section.name) || !line || line.startsWith("#")) continue;
7287
7644
  const assignment = line.match(/^([A-Za-z0-9_]+)\s*=\s*(.+)$/);
7288
7645
  if (!assignment) continue;
7289
- if (!section.env && assignment[1] !== "command" && assignment[1] !== "args" && assignment[1] !== "env") continue;
7646
+ if (!section.env && !["command", "args", "env", "url", "http_headers"].includes(assignment[1])) continue;
7290
7647
  pending = { key: assignment[1], value: assignment[2] };
7291
7648
  if (isTomlValueComplete(pending.value)) {
7292
7649
  applyTomlAssignment(path, agent, section, pending, registrations);
@@ -7294,7 +7651,7 @@ async function readTomlRegistrations(path, agent) {
7294
7651
  }
7295
7652
  }
7296
7653
  if (pending) throw new Error("Cannot read unsupported MCP value in " + AGENT_LABELS[agent] + " configuration at " + path + ".");
7297
- return [...registrations.values()].filter((registration) => registration.command);
7654
+ return [...registrations.values()].filter((registration) => registration.command || registration.url);
7298
7655
  }
7299
7656
  async function readGooseRegistrations(path) {
7300
7657
  const text2 = await readFile4(path, "utf8");
@@ -7368,11 +7725,18 @@ function applyTomlAssignment(path, agent, section, assignment, registrations) {
7368
7725
  registration.args = value;
7369
7726
  }
7370
7727
  if (!section.env && assignment.key === "env" && value && typeof value === "object" && !Array.isArray(value)) {
7371
- registration.env = Object.fromEntries(Object.entries(value).filter(
7372
- (entry) => typeof entry[1] === "string"
7373
- ));
7728
+ registration.env = stringEntries(value);
7729
+ }
7730
+ if (!section.env && assignment.key === "url" && typeof value === "string") registration.url = value;
7731
+ if (!section.env && assignment.key === "http_headers" && value && typeof value === "object" && !Array.isArray(value)) {
7732
+ registration.headers = stringEntries(value);
7374
7733
  }
7375
7734
  }
7735
+ function stringEntries(value) {
7736
+ return Object.fromEntries(Object.entries(value).filter(
7737
+ (entry) => typeof entry[1] === "string"
7738
+ ));
7739
+ }
7376
7740
  function stripTomlComment(line) {
7377
7741
  let quote;
7378
7742
  let escaped = false;
@@ -7485,9 +7849,13 @@ function splitTomlArray(value) {
7485
7849
  function normalizeRegistration(name, value) {
7486
7850
  if (!value || typeof value !== "object" || Array.isArray(value)) return [];
7487
7851
  const candidate = value;
7852
+ if (typeof candidate.command !== "string" && typeof candidate.url === "string") {
7853
+ const headers = candidate.headers && typeof candidate.headers === "object" && !Array.isArray(candidate.headers) ? stringEntries(candidate.headers) : {};
7854
+ return [{ name, command: "", args: [], env: {}, url: candidate.url, headers }];
7855
+ }
7488
7856
  if (typeof candidate.command !== "string") return [];
7489
7857
  const args = Array.isArray(candidate.args) && candidate.args.every((arg) => typeof arg === "string") ? candidate.args : [];
7490
- const env = candidate.env && typeof candidate.env === "object" && !Array.isArray(candidate.env) ? Object.fromEntries(Object.entries(candidate.env).filter((entry) => typeof entry[1] === "string")) : {};
7858
+ const env = candidate.env && typeof candidate.env === "object" && !Array.isArray(candidate.env) ? stringEntries(candidate.env) : {};
7491
7859
  return [{ name, command: candidate.command, args, env }];
7492
7860
  }
7493
7861
  function message(error) {
@@ -7496,8 +7864,8 @@ function message(error) {
7496
7864
 
7497
7865
  // src/commands/agent-config.ts
7498
7866
  import { existsSync as existsSync5 } from "fs";
7499
- import { lstat as lstat2, mkdir as mkdir3, open, readFile as readFile5, rename as rename2, unlink, writeFile as writeFile2 } from "fs/promises";
7500
- import { randomUUID as randomUUID3 } from "crypto";
7867
+ import { lstat as lstat3, mkdir as mkdir4, open as open2, readFile as readFile5, rename as rename3, unlink, writeFile as writeFile2 } from "fs/promises";
7868
+ import { randomUUID as randomUUID4 } from "crypto";
7501
7869
  import { dirname as dirname5 } from "path";
7502
7870
  async function installAgentSkill(agent, source, homeDir, projectDir, codexHome) {
7503
7871
  const path = agentSkillPath(agent, homeDir, projectDir, codexHome);
@@ -7518,6 +7886,23 @@ async function registerAgentConfig(agent, mcp, homeDir, registration, projectDir
7518
7886
  }
7519
7887
  }
7520
7888
  }
7889
+ var SHARED_HUB_AGENTS = ["claude", "codex"];
7890
+ async function registerAgentHttpConfig(agent, homeDir, registration, codexHome) {
7891
+ const [config] = agentConfigPaths(agent, homeDir, void 0, codexHome);
7892
+ if (agent === "claude") {
7893
+ await writeJsonServer(config, agent, registration.name, {
7894
+ type: "http",
7895
+ url: registration.url,
7896
+ headers: registration.headers
7897
+ });
7898
+ return;
7899
+ }
7900
+ const before = await fileSnapshot(config.path);
7901
+ const text2 = withoutMcpServerSections(before.contents ?? "", registration.name);
7902
+ const headers = Object.entries(registration.headers).map(([key, value]) => JSON.stringify(key) + " = " + JSON.stringify(value)).join(", ");
7903
+ const block = "\n\n[mcp_servers." + registration.name + "]\nurl = " + JSON.stringify(registration.url) + "\nhttp_headers = { " + headers + " }\n";
7904
+ await atomicWrite(config.path, text2 + block, before);
7905
+ }
7521
7906
  async function unregisterAgentConfig(agent, homeDir, name, projectDir, codexHome) {
7522
7907
  for (const config of agentConfigPaths(agent, homeDir, projectDir, codexHome)) {
7523
7908
  if (!existsSync5(config.path)) continue;
@@ -7559,6 +7944,15 @@ function registrationEnvironment(agent, registration) {
7559
7944
  };
7560
7945
  }
7561
7946
  async function registerJson(config, agent, mcp, registration) {
7947
+ const entry = {
7948
+ command: mcp.command,
7949
+ args: mcp.args,
7950
+ env: registrationEnvironment(agent, registration)
7951
+ };
7952
+ if (agent === "claude" || agent === "claude-desktop" || (config.rootKey ?? "mcpServers") === "servers") entry.type = "stdio";
7953
+ await writeJsonServer(config, agent, registration.name, entry);
7954
+ }
7955
+ async function writeJsonServer(config, agent, name, entry) {
7562
7956
  const before = await fileSnapshot(config.path);
7563
7957
  let parsed = {};
7564
7958
  if (before.contents !== void 0) {
@@ -7577,13 +7971,7 @@ async function registerJson(config, agent, mcp, registration) {
7577
7971
  throw new Error("Cannot update malformed " + AGENT_LABELS[agent] + " MCP configuration at " + config.path + ".");
7578
7972
  }
7579
7973
  const servers = existingServers ?? {};
7580
- const entry = {
7581
- command: mcp.command,
7582
- args: mcp.args,
7583
- env: registrationEnvironment(agent, registration)
7584
- };
7585
- if (agent === "claude" || agent === "claude-desktop" || rootKey === "servers") entry.type = "stdio";
7586
- servers[registration.name] = entry;
7974
+ servers[name] = entry;
7587
7975
  parsed[rootKey] = servers;
7588
7976
  await atomicWrite(config.path, JSON.stringify(parsed, null, 2) + "\n", before);
7589
7977
  }
@@ -7684,27 +8072,27 @@ function formatSkillForAgent(agent, source) {
7684
8072
  }
7685
8073
  return "---\ntrigger: model_decision\ndescription: Use Topolo MCP for Topolo platform operations.\n---\n\n" + body;
7686
8074
  }
7687
- function fileIdentity(stat2) {
7688
- return [stat2.dev, stat2.ino, stat2.size, stat2.mtimeMs, stat2.ctimeMs, stat2.mode, stat2.nlink].join(":");
8075
+ function fileIdentity(stat3) {
8076
+ return [stat3.dev, stat3.ino, stat3.size, stat3.mtimeMs, stat3.ctimeMs, stat3.mode, stat3.nlink].join(":");
7689
8077
  }
7690
8078
  async function fileSnapshot(path) {
7691
8079
  const parents = [];
7692
8080
  for (let parent = dirname5(path); ; parent = dirname5(parent)) {
7693
8081
  try {
7694
- const stat2 = await lstat2(parent);
7695
- if (!stat2.isDirectory() || stat2.isSymbolicLink()) throw new Error("Host destination contains a symbolic link or non-directory at " + parent + ".");
7696
- parents.push({ path: parent, identity: [stat2.dev, stat2.ino].join(":") });
8082
+ const stat3 = await lstat3(parent);
8083
+ if (!stat3.isDirectory() || stat3.isSymbolicLink()) throw new Error("Host destination contains a symbolic link or non-directory at " + parent + ".");
8084
+ parents.push({ path: parent, identity: [stat3.dev, stat3.ino].join(":") });
7697
8085
  } catch (error) {
7698
8086
  if (error.code !== "ENOENT") throw error;
7699
8087
  }
7700
8088
  if (dirname5(parent) === parent) break;
7701
8089
  }
7702
8090
  try {
7703
- const stat2 = await lstat2(path);
7704
- if (!stat2.isFile() || stat2.isSymbolicLink() || stat2.nlink !== 1) throw new Error("Host configuration must be a single regular file at " + path + ".");
8091
+ const stat3 = await lstat3(path);
8092
+ if (!stat3.isFile() || stat3.isSymbolicLink() || stat3.nlink !== 1) throw new Error("Host configuration must be a single regular file at " + path + ".");
7705
8093
  const contents = await readFile5(path, "utf8");
7706
- if (fileIdentity(await lstat2(path)) !== fileIdentity(stat2)) throw new Error("Host configuration changed while reading at " + path + ".");
7707
- return { contents, identity: fileIdentity(stat2), parents };
8094
+ if (fileIdentity(await lstat3(path)) !== fileIdentity(stat3)) throw new Error("Host configuration changed while reading at " + path + ".");
8095
+ return { contents, identity: fileIdentity(stat3), parents };
7708
8096
  } catch (error) {
7709
8097
  if (error.code === "ENOENT") return { parents };
7710
8098
  throw error;
@@ -7720,11 +8108,11 @@ async function atomicWrite(path, contents, before) {
7720
8108
  before ??= await fileSnapshot(path);
7721
8109
  await assertSnapshot(path, before);
7722
8110
  if (before.contents === contents) return;
7723
- await mkdir3(dirname5(path), { recursive: true });
8111
+ await mkdir4(dirname5(path), { recursive: true });
7724
8112
  const destination = await fileSnapshot(path);
7725
8113
  await assertSnapshot(path, before);
7726
- const temporaryPath = path + ".topolo-" + randomUUID3() + ".tmp";
7727
- const handle = await open(temporaryPath, "wx", 384);
8114
+ const temporaryPath = path + ".topolo-" + randomUUID4() + ".tmp";
8115
+ const handle = await open2(temporaryPath, "wx", 384);
7728
8116
  let temporaryIdentity;
7729
8117
  let handleClosed = false;
7730
8118
  let temporaryPending = true;
@@ -7736,11 +8124,11 @@ async function atomicWrite(path, contents, before) {
7736
8124
  await handle.close();
7737
8125
  handleClosed = true;
7738
8126
  await assertSnapshot(path, destination);
7739
- const temporary = await lstat2(temporaryPath);
8127
+ const temporary = await lstat3(temporaryPath);
7740
8128
  if (!temporary.isFile() || temporary.isSymbolicLink() || temporary.nlink !== 1 || [temporary.dev, temporary.ino].join(":") !== temporaryIdentity) {
7741
8129
  throw new Error("Host temporary file custody changed before rename at " + path + ".");
7742
8130
  }
7743
- await rename2(temporaryPath, path);
8131
+ await rename3(temporaryPath, path);
7744
8132
  temporaryPending = false;
7745
8133
  const installed = await fileSnapshot(path);
7746
8134
  if (installed.contents !== contents) throw new Error("Host configuration read-back failed at " + path + ".");
@@ -7750,10 +8138,10 @@ async function atomicWrite(path, contents, before) {
7750
8138
  let cleanupOwned = false;
7751
8139
  try {
7752
8140
  const parentsOwned = await Promise.all(destination.parents.map(async (parent) => {
7753
- const current2 = await lstat2(parent.path);
8141
+ const current2 = await lstat3(parent.path);
7754
8142
  return current2.isDirectory() && !current2.isSymbolicLink() && [current2.dev, current2.ino].join(":") === parent.identity;
7755
8143
  }));
7756
- const current = await lstat2(temporaryPath);
8144
+ const current = await lstat3(temporaryPath);
7757
8145
  cleanupOwned = parentsOwned.every(Boolean) && current.isFile() && !current.isSymbolicLink() && current.nlink === 1 && [current.dev, current.ino].join(":") === temporaryIdentity;
7758
8146
  } catch {
7759
8147
  }
@@ -7763,11 +8151,11 @@ async function atomicWrite(path, contents, before) {
7763
8151
  }
7764
8152
  function withoutMcpServerSections(text2, name) {
7765
8153
  const section = "[mcp_servers." + name + "]";
7766
- const environmentSection = "[mcp_servers." + name + ".env]";
8154
+ const subsectionPrefix = "[mcp_servers." + name + ".";
7767
8155
  let skipping = false;
7768
8156
  return text2.split(/\r?\n/).filter((line) => {
7769
8157
  const trimmed = line.trim();
7770
- if (trimmed === section || trimmed === environmentSection) {
8158
+ if (trimmed === section || trimmed.startsWith(subsectionPrefix) && trimmed.endsWith("]")) {
7771
8159
  skipping = true;
7772
8160
  return false;
7773
8161
  }
@@ -7776,14 +8164,505 @@ function withoutMcpServerSections(text2, name) {
7776
8164
  }).join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
7777
8165
  }
7778
8166
 
8167
+ // src/commands/mcp-hub-setup.ts
8168
+ import { realpathSync } from "fs";
8169
+ import { readFile as readFile8 } from "fs/promises";
8170
+ import { join as join10 } from "path";
8171
+
8172
+ // src/lib/mcp-hub-config.ts
8173
+ import { randomBytes } from "crypto";
8174
+ import { lstat as lstat4, mkdir as mkdir5, open as open3, readFile as readFile6, rename as rename4, unlink as unlink2 } from "fs/promises";
8175
+ import { dirname as dirname6, join as join8 } from "path";
8176
+ var MCP_HUB_DEFAULT_PORT = 7337;
8177
+ var MCP_HUB_ENVIRONMENTS = ["production", "staging", "development", "local"];
8178
+ var RESERVED_NAME = ["topolo", "platform"].join("-");
8179
+ var NAME_PATTERN = /^topolo(-[a-z0-9][a-z0-9_-]*)?$/;
8180
+ var TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
8181
+ function mcpHubPaths(credentialStorePath = configPath()) {
8182
+ const directory = dirname6(credentialStorePath);
8183
+ return { config: join8(directory, "mcp-hub.json"), token: join8(directory, "mcp-hub.token") };
8184
+ }
8185
+ function mcpHubUrl(port, name) {
8186
+ return `http://127.0.0.1:${port}/mcp/${name}`;
8187
+ }
8188
+ function emptyMcpHubConfig() {
8189
+ return { port: MCP_HUB_DEFAULT_PORT, identities: [] };
8190
+ }
8191
+ function validateMcpHubPort(value) {
8192
+ const port = typeof value === "string" && /^\d+$/.test(value.trim()) ? Number(value.trim()) : value;
8193
+ if (typeof port !== "number" || !Number.isInteger(port) || port < 1024 || port > 65535) {
8194
+ throw new Error("MCP hub port must be an integer from 1024 to 65535.");
8195
+ }
8196
+ return port;
8197
+ }
8198
+ function validateMcpHubIdentity(value, where = "identity") {
8199
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
8200
+ throw new Error(`MCP hub ${where} must be an object.`);
8201
+ }
8202
+ const record3 = value;
8203
+ const unknown = Object.keys(record3).filter((key) => !["name", "env", "context"].includes(key));
8204
+ if (unknown.length) throw new Error(`MCP hub ${where} has unsupported field(s): ${unknown.join(", ")}.`);
8205
+ const name = record3.name;
8206
+ if (typeof name !== "string" || !NAME_PATTERN.test(name) || name === RESERVED_NAME) {
8207
+ throw new Error(`MCP hub ${where} name must be topolo or topolo-<name> (and not reserved).`);
8208
+ }
8209
+ const environment = record3.env;
8210
+ if (typeof environment !== "string" || !MCP_HUB_ENVIRONMENTS.includes(environment)) {
8211
+ throw new Error(`MCP hub identity ${name} env must be one of ${MCP_HUB_ENVIRONMENTS.join(", ")}.`);
8212
+ }
8213
+ const context = record3.context;
8214
+ if (context !== void 0 && (typeof context !== "string" || !context.trim() || /[\r\n]/.test(context))) {
8215
+ throw new Error(`MCP hub identity ${name} context must be a non-empty single-line selector.`);
8216
+ }
8217
+ return {
8218
+ name,
8219
+ environment,
8220
+ ...context !== void 0 ? { context: context.trim() } : {}
8221
+ };
8222
+ }
8223
+ function parseMcpHubConfig(text2, source) {
8224
+ let parsed;
8225
+ try {
8226
+ parsed = JSON.parse(text2);
8227
+ } catch (error) {
8228
+ throw new Error(`Cannot read malformed MCP hub configuration at ${source}: ${error.message}`);
8229
+ }
8230
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
8231
+ throw new Error(`MCP hub configuration at ${source} must be a JSON object.`);
8232
+ }
8233
+ const record3 = parsed;
8234
+ const unknown = Object.keys(record3).filter((key) => !["version", "port", "identities"].includes(key));
8235
+ if (unknown.length) {
8236
+ throw new Error(`MCP hub configuration at ${source} has unsupported field(s): ${unknown.join(", ")}.`);
8237
+ }
8238
+ if (record3.version !== 1) throw new Error(`MCP hub configuration at ${source} must declare "version": 1.`);
8239
+ const port = record3.port === void 0 ? MCP_HUB_DEFAULT_PORT : validateMcpHubPort(record3.port);
8240
+ if (record3.identities !== void 0 && !Array.isArray(record3.identities)) {
8241
+ throw new Error(`MCP hub configuration at ${source} identities must be an array.`);
8242
+ }
8243
+ const identities = (record3.identities ?? []).map((entry, index) => validateMcpHubIdentity(entry, `identity #${index + 1}`));
8244
+ const seen = /* @__PURE__ */ new Set();
8245
+ for (const identity of identities) {
8246
+ if (seen.has(identity.name)) {
8247
+ throw new Error(`MCP hub configuration at ${source} declares ${identity.name} more than once.`);
8248
+ }
8249
+ seen.add(identity.name);
8250
+ }
8251
+ return { port, identities };
8252
+ }
8253
+ function serializeMcpHubConfig(config) {
8254
+ return JSON.stringify({
8255
+ version: 1,
8256
+ port: config.port,
8257
+ identities: config.identities.map((identity) => ({
8258
+ name: identity.name,
8259
+ env: identity.environment,
8260
+ ...identity.context ? { context: identity.context } : {}
8261
+ }))
8262
+ }, null, 2) + "\n";
8263
+ }
8264
+ async function readMcpHubConfig(path) {
8265
+ let text2;
8266
+ try {
8267
+ text2 = await readFile6(path, "utf8");
8268
+ } catch (error) {
8269
+ if (error.code === "ENOENT") return emptyMcpHubConfig();
8270
+ throw error;
8271
+ }
8272
+ return parseMcpHubConfig(text2, path);
8273
+ }
8274
+ function upsertMcpHubIdentity(config, identity) {
8275
+ const identities = config.identities.filter((entry) => entry.name !== identity.name);
8276
+ identities.push(identity);
8277
+ identities.sort((left, right) => left.name.localeCompare(right.name));
8278
+ return { ...config, identities };
8279
+ }
8280
+ async function writePrivateFile(path, contents) {
8281
+ await mkdir5(dirname6(path), { recursive: true, mode: 448 });
8282
+ const temporary = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
8283
+ const handle = await open3(temporary, "wx", 384);
8284
+ try {
8285
+ await handle.writeFile(contents, "utf8");
8286
+ } finally {
8287
+ await handle.close();
8288
+ }
8289
+ try {
8290
+ await rename4(temporary, path);
8291
+ } catch (error) {
8292
+ await unlink2(temporary).catch(() => void 0);
8293
+ throw error;
8294
+ }
8295
+ }
8296
+ async function writeMcpHubConfig(path, config) {
8297
+ await writePrivateFile(path, serializeMcpHubConfig(config));
8298
+ }
8299
+ async function assertPrivateTokenFile(path) {
8300
+ const stat3 = await lstat4(path);
8301
+ if (!stat3.isFile() || stat3.isSymbolicLink() || stat3.nlink !== 1) {
8302
+ throw new Error(`MCP hub token at ${path} must be a single regular file.`);
8303
+ }
8304
+ if ((stat3.mode & 63) !== 0) {
8305
+ throw new Error(`MCP hub token at ${path} is readable by other users. Delete it and rerun \`topolo setup --shared\` to issue a new one.`);
8306
+ }
8307
+ }
8308
+ async function readMcpHubToken(path) {
8309
+ try {
8310
+ await assertPrivateTokenFile(path);
8311
+ } catch (error) {
8312
+ if (error.code === "ENOENT") {
8313
+ throw new Error(`MCP hub token is missing at ${path}. Run \`topolo setup --shared\`.`);
8314
+ }
8315
+ throw error;
8316
+ }
8317
+ const token = (await readFile6(path, "utf8")).trim();
8318
+ if (!TOKEN_PATTERN.test(token)) {
8319
+ throw new Error(`MCP hub token at ${path} is malformed. Delete it and rerun \`topolo setup --shared\`.`);
8320
+ }
8321
+ return token;
8322
+ }
8323
+ async function ensureMcpHubToken(path) {
8324
+ try {
8325
+ return { token: await readMcpHubToken(path), created: false };
8326
+ } catch (error) {
8327
+ if (!/is missing at/.test(error.message)) throw error;
8328
+ }
8329
+ await mkdir5(dirname6(path), { recursive: true, mode: 448 });
8330
+ const token = randomBytes(32).toString("base64url");
8331
+ const handle = await open3(path, "wx", 384);
8332
+ try {
8333
+ await handle.writeFile(token + "\n", "utf8");
8334
+ } finally {
8335
+ await handle.close();
8336
+ }
8337
+ return { token, created: true };
8338
+ }
8339
+
8340
+ // src/lib/mcp-hub-service.ts
8341
+ import { spawnSync as spawnSync4 } from "child_process";
8342
+ import { readFile as readFile7 } from "fs/promises";
8343
+ import { join as join9 } from "path";
8344
+ var MCP_HUB_LAUNCHD_LABEL = "io.topolo.mcp-hub";
8345
+ var MCP_HUB_SYSTEMD_UNIT = "topolo-mcp-hub.service";
8346
+ var PASSTHROUGH_ENVIRONMENT = ["TOPOLO_CONFIG_PATH", "XDG_CONFIG_HOME"];
8347
+ var runCommand = (command, args) => {
8348
+ const result = spawnSync4(command, args, { encoding: "utf8", stdio: ["ignore", "ignore", "pipe"] });
8349
+ return { status: result.status, stderr: result.stderr ?? "" };
8350
+ };
8351
+ function serviceKindFor(platform4) {
8352
+ if (platform4 === "darwin") return "launchd";
8353
+ if (platform4 === "linux") return "systemd";
8354
+ return void 0;
8355
+ }
8356
+ function mcpHubServicePath(kind, homeDir, xdgConfigHome) {
8357
+ return kind === "launchd" ? join9(homeDir, "Library", "LaunchAgents", `${MCP_HUB_LAUNCHD_LABEL}.plist`) : join9(xdgConfigHome || join9(homeDir, ".config"), "systemd", "user", MCP_HUB_SYSTEMD_UNIT);
8358
+ }
8359
+ function serviceEnvironment(environment) {
8360
+ const selected = {};
8361
+ for (const key of PASSTHROUGH_ENVIRONMENT) {
8362
+ const value = environment[key];
8363
+ if (value) selected[key] = value;
8364
+ }
8365
+ return selected;
8366
+ }
8367
+ function xml(value) {
8368
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
8369
+ }
8370
+ function serviceArguments(spec) {
8371
+ return [spec.nodeCommand, spec.cliEntry, "mcp", "serve", "--port", String(spec.port)];
8372
+ }
8373
+ function renderLaunchdPlist(spec) {
8374
+ const environment = Object.entries(spec.environment);
8375
+ return [
8376
+ '<?xml version="1.0" encoding="UTF-8"?>',
8377
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
8378
+ '<plist version="1.0">',
8379
+ "<dict>",
8380
+ " <key>Label</key>",
8381
+ ` <string>${MCP_HUB_LAUNCHD_LABEL}</string>`,
8382
+ " <key>ProgramArguments</key>",
8383
+ " <array>",
8384
+ ...serviceArguments(spec).map((argument) => ` <string>${xml(argument)}</string>`),
8385
+ " </array>",
8386
+ ...environment.length ? [
8387
+ " <key>EnvironmentVariables</key>",
8388
+ " <dict>",
8389
+ ...environment.flatMap(([key, value]) => [` <key>${xml(key)}</key>`, ` <string>${xml(value)}</string>`]),
8390
+ " </dict>"
8391
+ ] : [],
8392
+ " <key>RunAtLoad</key>",
8393
+ " <true/>",
8394
+ " <key>KeepAlive</key>",
8395
+ " <true/>",
8396
+ " <key>ProcessType</key>",
8397
+ " <string>Background</string>",
8398
+ ...spec.logPath ? [
8399
+ " <key>StandardOutPath</key>",
8400
+ ` <string>${xml(spec.logPath)}</string>`,
8401
+ " <key>StandardErrorPath</key>",
8402
+ ` <string>${xml(spec.logPath)}</string>`
8403
+ ] : [],
8404
+ "</dict>",
8405
+ "</plist>",
8406
+ ""
8407
+ ].join("\n");
8408
+ }
8409
+ function systemdQuote(value) {
8410
+ return '"' + value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%") + '"';
8411
+ }
8412
+ function renderSystemdUnit(spec) {
8413
+ return [
8414
+ "[Unit]",
8415
+ "Description=Topolo shared local MCP hub",
8416
+ "After=network-online.target",
8417
+ "",
8418
+ "[Service]",
8419
+ "Type=simple",
8420
+ `ExecStart=${serviceArguments(spec).map(systemdQuote).join(" ")}`,
8421
+ ...Object.entries(spec.environment).map(([key, value]) => `Environment=${systemdQuote(`${key}=${value}`)}`),
8422
+ "Restart=always",
8423
+ "RestartSec=2",
8424
+ "",
8425
+ "[Install]",
8426
+ "WantedBy=default.target",
8427
+ ""
8428
+ ].join("\n");
8429
+ }
8430
+ async function readExisting(path) {
8431
+ try {
8432
+ return await readFile7(path, "utf8");
8433
+ } catch (error) {
8434
+ if (error.code === "ENOENT") return void 0;
8435
+ throw error;
8436
+ }
8437
+ }
8438
+ function require0(result, what) {
8439
+ if (result.status !== 0) throw new Error(`${what} failed: ${result.stderr.trim() || `exit ${result.status}`}`);
8440
+ }
8441
+ async function installMcpHubService(options) {
8442
+ const kind = serviceKindFor(options.platform);
8443
+ if (!kind) {
8444
+ return {
8445
+ kind: "unsupported",
8446
+ changed: false,
8447
+ activated: false,
8448
+ detail: `No user service manager is supported on ${options.platform}; run \`topolo mcp serve\` yourself.`
8449
+ };
8450
+ }
8451
+ const path = mcpHubServicePath(kind, options.homeDir, options.xdgConfigHome);
8452
+ const spec = kind === "launchd" ? { ...options.spec, logPath: options.spec.logPath ?? join9(options.homeDir, "Library", "Logs", "topolo-mcp-hub.log") } : options.spec;
8453
+ const contents = kind === "launchd" ? renderLaunchdPlist(spec) : renderSystemdUnit(spec);
8454
+ const changed = await readExisting(path) !== contents;
8455
+ if (changed) await writePrivateFile(path, contents);
8456
+ if (!options.activate) {
8457
+ return { kind, path, changed, activated: false, detail: `${kind} unit written; not activated for an alternate agent home.` };
8458
+ }
8459
+ const run = options.run;
8460
+ if (kind === "launchd") {
8461
+ const domain = `gui/${options.uid ?? process.getuid()}`;
8462
+ const target = `${domain}/${MCP_HUB_LAUNCHD_LABEL}`;
8463
+ const loaded = run("launchctl", ["print", target]).status === 0;
8464
+ if (loaded && !changed) {
8465
+ return { kind, path, changed, activated: true, detail: `${MCP_HUB_LAUNCHD_LABEL} already loaded with this definition.` };
8466
+ }
8467
+ if (loaded) run("launchctl", ["bootout", target]);
8468
+ require0(run("launchctl", ["bootstrap", domain, path]), "launchctl bootstrap");
8469
+ return { kind, path, changed, activated: true, detail: `${MCP_HUB_LAUNCHD_LABEL} ${loaded ? "reloaded" : "loaded"} (KeepAlive).` };
8470
+ }
8471
+ require0(run("systemctl", ["--user", "daemon-reload"]), "systemctl --user daemon-reload");
8472
+ require0(run("systemctl", ["--user", "enable", MCP_HUB_SYSTEMD_UNIT]), "systemctl --user enable");
8473
+ require0(run("systemctl", ["--user", changed ? "restart" : "start", MCP_HUB_SYSTEMD_UNIT]), `systemctl --user ${changed ? "restart" : "start"}`);
8474
+ return { kind, path, changed, activated: true, detail: `${MCP_HUB_SYSTEMD_UNIT} enabled and ${changed ? "restarted" : "running"} (Restart=always).` };
8475
+ }
8476
+
8477
+ // src/commands/mcp-hub-setup.ts
8478
+ function resolveCliEntry(argvEntry = process.argv[1]) {
8479
+ if (!argvEntry) throw new Error("Cannot locate the running topolo CLI entry for the hub service.");
8480
+ return realpathSync(argvEntry);
8481
+ }
8482
+ function resolveSharedTargets(options) {
8483
+ const explicit = [...options.agent ?? [], ...options.claude ? ["claude"] : [], ...options.codex ? ["codex"] : []];
8484
+ if (!explicit.length) return [...SHARED_HUB_AGENTS];
8485
+ const unsupported = explicit.filter((agent) => !SHARED_HUB_AGENTS.includes(agent));
8486
+ if (unsupported.length) {
8487
+ throw new Error(
8488
+ `The shared MCP hub supports Claude Code and Codex only (not ${unsupported.join(", ")}). Register other hosts with \`topolo setup\` (stdio).`
8489
+ );
8490
+ }
8491
+ return [...new Set(explicit)];
8492
+ }
8493
+ async function importStdioIdentities(agents, homeDir, codexHome) {
8494
+ const found = /* @__PURE__ */ new Map();
8495
+ const skipped = [];
8496
+ for (const agent of agents) {
8497
+ for (const registration of await readAgentRegistrations(agent, homeDir, void 0, codexHome)) {
8498
+ if (registration.url) continue;
8499
+ if (registration.env.TOPOLO_CONFIG_PATH || registration.env.TOPOLO_AGENT_INSTALLATION_ID) {
8500
+ skipped.push({ agent, name: registration.name, reason: "uses a private config path or agent installation; keep it on stdio" });
8501
+ continue;
8502
+ }
8503
+ let identity;
8504
+ try {
8505
+ identity = validateMcpHubIdentity({
8506
+ name: registration.name,
8507
+ env: registration.env.TOPOLO_ENV || "production",
8508
+ ...registration.env.TOPOLO_CONTEXT ? { context: registration.env.TOPOLO_CONTEXT } : {}
8509
+ });
8510
+ } catch (error) {
8511
+ skipped.push({ agent, name: registration.name, reason: error.message });
8512
+ continue;
8513
+ }
8514
+ const previous = found.get(identity.name);
8515
+ if (previous && JSON.stringify(previous.identity) !== JSON.stringify(identity)) {
8516
+ skipped.push({ agent, name: identity.name, reason: `conflicts with the ${AGENT_LABELS[previous.agent]} registration of the same name` });
8517
+ continue;
8518
+ }
8519
+ found.set(identity.name, { identity, agent });
8520
+ }
8521
+ }
8522
+ return { identities: [...found.values()].map((entry) => entry.identity), skipped };
8523
+ }
8524
+ function sameHeaders(left, right) {
8525
+ return JSON.stringify(Object.entries(left ?? {}).sort()) === JSON.stringify(Object.entries(right).sort());
8526
+ }
8527
+ function hubHeaders(token, agent) {
8528
+ return { Authorization: `Bearer ${token}`, "X-Topolo-Agent": AGENT_NAME_FOR[agent] };
8529
+ }
8530
+ async function installSharedAgentSurfaces(options, dependencies) {
8531
+ if (options.project) {
8532
+ throw new Error("--shared registers user-scope servers only: the hub token must never be written into project files.");
8533
+ }
8534
+ if (options.installationId) {
8535
+ throw new Error("--shared cannot bind an agent installation; keep installation-bound MCPs on stdio.");
8536
+ }
8537
+ const agents = resolveSharedTargets(options);
8538
+ const target = resolveAgentHostTarget({ homeDir: options.homeDir, codexHome: options.codexHome });
8539
+ const paths = dependencies.paths();
8540
+ let config = await readMcpHubConfig(paths.config);
8541
+ if (options.port !== void 0) config = { ...config, port: validateMcpHubPort(options.port) };
8542
+ const skipped = [];
8543
+ if (options.importStdio) {
8544
+ const imported = await importStdioIdentities(agents, target.homeDir, target.codexHome);
8545
+ skipped.push(...imported.skipped);
8546
+ for (const identity of imported.identities) config = upsertMcpHubIdentity(config, identity);
8547
+ }
8548
+ if (!options.importStdio || options.name) {
8549
+ const name = options.name?.trim() || "topolo";
8550
+ const context = options.context?.trim();
8551
+ if (name !== "topolo" && !context) throw new Error("Named registrations require --context <context>.");
8552
+ if (name === "topolo" && context) {
8553
+ throw new Error("Pinned contexts require --name topolo-<name> so the active topolo registration remains independent.");
8554
+ }
8555
+ config = upsertMcpHubIdentity(config, validateMcpHubIdentity({
8556
+ name,
8557
+ env: options.environment?.trim() || dependencies.environment.TOPOLO_ENV || "production",
8558
+ ...context ? { context } : {}
8559
+ }));
8560
+ }
8561
+ await writeMcpHubConfig(paths.config, config);
8562
+ const { token, created } = await ensureMcpHubToken(paths.token);
8563
+ const source = await dependencies.skillSource();
8564
+ const installed = [];
8565
+ for (const agent of agents) {
8566
+ const skill = await installAgentSkill(agent, source, target.homeDir, void 0, target.codexHome);
8567
+ try {
8568
+ const headers = hubHeaders(token, agent);
8569
+ for (const identity of config.identities) {
8570
+ await registerAgentHttpConfig(agent, target.homeDir, {
8571
+ name: identity.name,
8572
+ url: mcpHubUrl(config.port, identity.name),
8573
+ headers
8574
+ }, target.codexHome);
8575
+ }
8576
+ const registrations = await readAgentRegistrations(agent, target.homeDir, void 0, target.codexHome);
8577
+ const missing = config.identities.filter((identity) => {
8578
+ const entry = registrations.find((registration) => registration.name === identity.name);
8579
+ return !entry || entry.url !== mcpHubUrl(config.port, identity.name) || !sameHeaders(entry.headers, headers);
8580
+ });
8581
+ if (missing.length) {
8582
+ throw new Error(`Host configuration did not retain the shared hub registration for ${missing.map((identity) => identity.name).join(", ")}.`);
8583
+ }
8584
+ installed.push({
8585
+ agent,
8586
+ skillPath: skill.path,
8587
+ skillInstalled: skill.installed,
8588
+ mcp: "config-file",
8589
+ registrations: config.identities.map((identity) => identity.name)
8590
+ });
8591
+ } catch (error) {
8592
+ installed.push({
8593
+ agent,
8594
+ skillPath: skill.path,
8595
+ skillInstalled: skill.installed,
8596
+ mcp: "failed",
8597
+ registrations: [],
8598
+ detail: error.message
8599
+ });
8600
+ }
8601
+ }
8602
+ const service = options.service === false ? void 0 : await installMcpHubService({
8603
+ platform: dependencies.platform,
8604
+ homeDir: target.homeDir,
8605
+ xdgConfigHome: target.homeDir === dependencies.realHome ? dependencies.environment.XDG_CONFIG_HOME : void 0,
8606
+ spec: {
8607
+ nodeCommand: dependencies.nodeCommand(),
8608
+ cliEntry: dependencies.cliEntry(),
8609
+ port: config.port,
8610
+ environment: serviceEnvironment(dependencies.environment)
8611
+ },
8612
+ activate: target.homeDir === dependencies.realHome,
8613
+ run: dependencies.run,
8614
+ ...dependencies.uid !== void 0 ? { uid: dependencies.uid } : {}
8615
+ });
8616
+ return {
8617
+ hub: {
8618
+ port: config.port,
8619
+ configPath: paths.config,
8620
+ tokenPath: paths.token,
8621
+ tokenCreated: created,
8622
+ identities: config.identities
8623
+ },
8624
+ installed,
8625
+ skipped,
8626
+ ...service ? { service } : {}
8627
+ };
8628
+ }
8629
+ function defaultSharedSetupDependencies(skillsDir, realHome) {
8630
+ return {
8631
+ paths: () => mcpHubPaths(),
8632
+ skillSource: () => readFile8(join10(skillsDir, "topolo", "SKILL.md"), "utf8"),
8633
+ platform: process.platform,
8634
+ realHome,
8635
+ nodeCommand: stableNodeCommand,
8636
+ cliEntry: () => resolveCliEntry(),
8637
+ environment: process.env,
8638
+ run: runCommand
8639
+ };
8640
+ }
8641
+ function formatSharedSetup(result) {
8642
+ const lines = [
8643
+ `Shared MCP hub: http://127.0.0.1:${result.hub.port}/mcp/<name> (${result.hub.identities.length} identities)`,
8644
+ ` config -> ${result.hub.configPath}`,
8645
+ ` token -> ${result.hub.tokenPath}${result.hub.tokenCreated ? " (new)" : ""}`
8646
+ ];
8647
+ for (const row of result.installed) {
8648
+ lines.push(`${AGENT_LABELS[row.agent]}:`);
8649
+ lines.push(` skill -> ${row.skillPath}`);
8650
+ lines.push(row.mcp === "config-file" ? ` HTTP MCP registered: ${row.registrations.join(", ")}` : ` HTTP MCP registration FAILED: ${row.detail}`);
8651
+ }
8652
+ for (const entry of result.skipped) lines.push(`Skipped ${entry.name} (${AGENT_LABELS[entry.agent]}): ${entry.reason}`);
8653
+ if (result.service) lines.push(`Service (${result.service.kind}): ${result.service.detail}`);
8654
+ lines.push("", "Next: restart the agents, then run `topolo doctor --shared`.");
8655
+ return lines.join("\n");
8656
+ }
8657
+
7779
8658
  // src/commands/skills.ts
7780
8659
  var AGENT_LABELS2 = AGENT_LABELS;
7781
8660
  function skillsSourceDir() {
7782
- const here = dirname6(fileURLToPath3(import.meta.url));
8661
+ const here = dirname7(fileURLToPath3(import.meta.url));
7783
8662
  const distCandidate = resolve3(here, "../skills");
7784
8663
  const srcCandidate = resolve3(here, "../../skills");
7785
8664
  for (const candidate of [distCandidate, srcCandidate]) {
7786
- if (existsSync6(join7(candidate, "topolo", "SKILL.md"))) return candidate;
8665
+ if (existsSync6(join11(candidate, "topolo", "SKILL.md"))) return candidate;
7787
8666
  }
7788
8667
  return distCandidate;
7789
8668
  }
@@ -7793,7 +8672,7 @@ function resolveMcpCommand() {
7793
8672
  }
7794
8673
  function agentBinaryExists(agent) {
7795
8674
  return agentCliNames(agent).some((bin) => {
7796
- const probe = spawnSync4(process.platform === "win32" ? "where" : "which", [bin], {
8675
+ const probe = spawnSync5(process.platform === "win32" ? "where" : "which", [bin], {
7797
8676
  stdio: "ignore"
7798
8677
  });
7799
8678
  return probe.status === 0;
@@ -7841,7 +8720,7 @@ async function installForAgent(agent, sourceDir, mcp, opts) {
7841
8720
  return { agent, skillInstalled: false, mcp: "failed", detail: err instanceof Error ? err.message : String(err) };
7842
8721
  }
7843
8722
  }
7844
- const source = await readFile6(join7(sourceDir, "topolo", "SKILL.md"), "utf8");
8723
+ const source = await readFile9(join11(sourceDir, "topolo", "SKILL.md"), "utf8");
7845
8724
  const skill = await installAgentSkill(agent, source, homeDir, projectDir, codexHome);
7846
8725
  if (!opts.withMcp) {
7847
8726
  return { agent, skillPath: skill.path, skillInstalled: skill.installed, mcp: "skipped" };
@@ -7938,7 +8817,7 @@ function registerSkillsCommand(program2) {
7938
8817
  }
7939
8818
  async function installAgentSurfaces(opts) {
7940
8819
  const sourceDir = skillsSourceDir();
7941
- if (!existsSync6(join7(sourceDir, "topolo", "SKILL.md"))) {
8820
+ if (!existsSync6(join11(sourceDir, "topolo", "SKILL.md"))) {
7942
8821
  throw new Error("Bundled skill file not found. Reinstall the CLI: npm install -g @topolo/cli");
7943
8822
  }
7944
8823
  const mcp = resolveMcpCommand();
@@ -7956,9 +8835,22 @@ async function installAgentSurfaces(opts) {
7956
8835
  return { results, mcp };
7957
8836
  }
7958
8837
  function registerSetupCommand(program2) {
7959
- program2.command("setup").description("Install the Topolo skill and deterministic local MCP server for supported agent hosts").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--codex-home <dir>", "Use an explicit native Codex home instead of <agent-home>/.codex").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
8838
+ program2.command("setup").description("Install the Topolo skill and deterministic local MCP server for supported agent hosts").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--codex-home <dir>", "Use an explicit native Codex home instead of <agent-home>/.codex").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").option("--shared", "Opt in: register Claude Code and Codex against the shared local MCP hub (one process) instead of stdio").option("--port <port>", "With --shared: hub port on 127.0.0.1 (default 7337)").option("--import-stdio", "With --shared: also move every existing stdio topolo/topolo-* registration onto the hub").option("--no-service", "With --shared: do not install the launchd/systemd keep-alive unit").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
7960
8839
  const mode = resolveOutputMode(opts.json);
7961
8840
  try {
8841
+ if (opts.shared) {
8842
+ const shared = await installSharedAgentSurfaces({
8843
+ ...opts,
8844
+ installationId: opts.installation,
8845
+ homeDir: opts.agentHome
8846
+ }, defaultSharedSetupDependencies(skillsSourceDir(), homedir5()));
8847
+ assertSharedSetupSucceeded(shared);
8848
+ emit(shared, mode, () => formatSharedSetup(shared));
8849
+ return;
8850
+ }
8851
+ if (opts.port !== void 0 || opts.importStdio || opts.service === false) {
8852
+ throw new Error("--port, --import-stdio and --no-service apply only with --shared.");
8853
+ }
7962
8854
  const { results, mcp } = await installAgentSurfaces({
7963
8855
  ...opts,
7964
8856
  installationId: opts.installation,
@@ -7971,6 +8863,12 @@ function registerSetupCommand(program2) {
7971
8863
  }
7972
8864
  });
7973
8865
  }
8866
+ function assertSharedSetupSucceeded(result) {
8867
+ const failed = result.installed.filter((row) => row.mcp === "failed");
8868
+ if (failed.length) {
8869
+ throw new Error(failed.map((row) => `${AGENT_LABELS2[row.agent]}: ${row.detail}`).join("; "));
8870
+ }
8871
+ }
7974
8872
  function assertAgentSetupSucceeded(results) {
7975
8873
  const failed = results.filter((result) => result.mcp === "failed");
7976
8874
  if (failed.length) {
@@ -7980,7 +8878,7 @@ function assertAgentSetupSucceeded(results) {
7980
8878
  async function agentSurfaceStatus(options = {}) {
7981
8879
  const target = resolveAgentHostTarget({ homeDir: options.homeDir, projectDir: options.project, codexHome: options.codexHome });
7982
8880
  const { homeDir, projectDir, codexHome } = target;
7983
- const sourceSkill = await readFile6(join7(skillsSourceDir(), "topolo", "SKILL.md"), "utf8");
8881
+ const sourceSkill = await readFile9(join11(skillsSourceDir(), "topolo", "SKILL.md"), "utf8");
7984
8882
  const agents = resolveTargets({
7985
8883
  agent: options.agent,
7986
8884
  all: options.all,
@@ -7990,7 +8888,7 @@ async function agentSurfaceStatus(options = {}) {
7990
8888
  });
7991
8889
  return Promise.all(agents.map(async (agent) => {
7992
8890
  const skillPath = agentSkillPath(agent, homeDir, projectDir, codexHome);
7993
- const installedSkill = skillPath && existsSync6(skillPath) ? await readFile6(skillPath, "utf8") : void 0;
8891
+ const installedSkill = skillPath && existsSync6(skillPath) ? await readFile9(skillPath, "utf8") : void 0;
7994
8892
  const registrations = (await readAgentRegistrations(agent, homeDir, projectDir, codexHome)).filter((registration) => !options.name || registration.name === options.name);
7995
8893
  return {
7996
8894
  agent,
@@ -8016,7 +8914,7 @@ async function uninstallAgentSurfaces(options) {
8016
8914
  const remaining = await readAgentRegistrations(agent, homeDir, projectDir, codexHome);
8017
8915
  const skillPath = agentSkillPath(agent, homeDir, projectDir, codexHome);
8018
8916
  const skillRemoved = remaining.length === 0 && Boolean(skillPath && existsSync6(skillPath));
8019
- if (skillRemoved && skillPath) await rm2(dirname6(skillPath), { recursive: true, force: true });
8917
+ if (skillRemoved && skillPath) await rm3(dirname7(skillPath), { recursive: true, force: true });
8020
8918
  removed.push({ agent, name, registrationRemoved, skillRemoved });
8021
8919
  }
8022
8920
  return removed;
@@ -8045,14 +8943,128 @@ function formatStatus(rows) {
8045
8943
  }
8046
8944
 
8047
8945
  // src/commands/doctor.ts
8048
- import { access } from "fs/promises";
8946
+ import { access as access2 } from "fs/promises";
8049
8947
  import { homedir as homedir6 } from "os";
8050
8948
  import { resolve as resolve4 } from "path";
8051
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
8949
+ import { Client as Client2 } from "@modelcontextprotocol/sdk/client/index.js";
8052
8950
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
8951
+
8952
+ // src/commands/mcp-hub-doctor.ts
8953
+ import { access } from "fs/promises";
8954
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
8955
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
8956
+ async function verifyHubIdentity(url, headers, fetchImplementation = fetch) {
8957
+ const client = new Client({ name: "topolo-cli-doctor", version: CLI_VERSION });
8958
+ const transport = new StreamableHTTPClientTransport(new URL(url), {
8959
+ requestInit: { headers },
8960
+ fetch: fetchImplementation
8961
+ });
8962
+ try {
8963
+ await client.connect(transport);
8964
+ const tools = await client.listTools();
8965
+ if (!tools.tools.some((tool) => tool.name === "topolo_whoami")) {
8966
+ throw new Error("hub identity connected but did not advertise topolo_whoami.");
8967
+ }
8968
+ const identity = await client.callTool({ name: "topolo_whoami", arguments: {} });
8969
+ if (identity.isError) throw new Error("topolo_whoami returned an error through the hub.");
8970
+ return `handshake and topolo_whoami succeeded; ${tools.tools.length} tools advertised`;
8971
+ } finally {
8972
+ await client.close();
8973
+ }
8974
+ }
8975
+ var defaultSharedDoctorDependencies = () => ({
8976
+ paths: () => mcpHubPaths(),
8977
+ platform: process.platform,
8978
+ fetch,
8979
+ verifyIdentity: (url, headers) => verifyHubIdentity(url, headers),
8980
+ fileExists: async (path) => access(path).then(() => true, () => false),
8981
+ environment: process.env
8982
+ });
8983
+ function message2(error) {
8984
+ return error instanceof Error ? error.message : String(error);
8985
+ }
8986
+ async function runSharedDoctor(options, dependencies) {
8987
+ const checks = [];
8988
+ const done = () => ({ ok: checks.every((check) => check.ok), cliVersion: CLI_VERSION, checks });
8989
+ const paths = dependencies.paths();
8990
+ let config;
8991
+ try {
8992
+ config = await readMcpHubConfig(paths.config);
8993
+ const selected = options.name ? config.identities.filter((identity) => identity.name === options.name) : config.identities;
8994
+ if (!selected.length) {
8995
+ throw new Error(options.name ? `${options.name} is not a hub identity in ${paths.config}.` : `No identities are configured in ${paths.config}. Run \`topolo setup --shared\`.`);
8996
+ }
8997
+ config = { ...config, identities: selected };
8998
+ checks.push({ name: "hub-config", ok: true, detail: `${paths.config}: port ${config.port}, ${selected.length} identities` });
8999
+ } catch (error) {
9000
+ checks.push({ name: "hub-config", ok: false, detail: message2(error) });
9001
+ return done();
9002
+ }
9003
+ let token;
9004
+ try {
9005
+ token = await readMcpHubToken(paths.token);
9006
+ checks.push({ name: "hub-token", ok: true, detail: `${paths.token} is private (0600)` });
9007
+ } catch (error) {
9008
+ checks.push({ name: "hub-token", ok: false, detail: message2(error) });
9009
+ return done();
9010
+ }
9011
+ const target = resolveAgentHostTarget({ homeDir: options.agentHome, codexHome: options.codexHome });
9012
+ const kind = serviceKindFor(dependencies.platform);
9013
+ if (kind) {
9014
+ const path = mcpHubServicePath(kind, target.homeDir, options.agentHome ? void 0 : dependencies.environment.XDG_CONFIG_HOME);
9015
+ const present = await dependencies.fileExists(path);
9016
+ checks.push({ name: "service", ok: present, detail: present ? `${kind} unit at ${path}` : `${kind} unit missing at ${path}` });
9017
+ }
9018
+ try {
9019
+ const response = await dependencies.fetch(`http://127.0.0.1:${config.port}/health`, {
9020
+ headers: { authorization: `Bearer ${token}` }
9021
+ });
9022
+ if (response.status !== 200) throw new Error(`hub /health answered ${response.status}`);
9023
+ checks.push({ name: "hub", ok: true, detail: `listening on 127.0.0.1:${config.port}` });
9024
+ } catch (error) {
9025
+ checks.push({ name: "hub", ok: false, detail: `hub is not reachable on 127.0.0.1:${config.port}: ${message2(error)}` });
9026
+ return done();
9027
+ }
9028
+ const explicit = [...options.agent ?? [], ...options.claude ? ["claude"] : [], ...options.codex ? ["codex"] : []];
9029
+ const agents = (explicit.length ? explicit : [...SHARED_HUB_AGENTS]).filter((agent) => SHARED_HUB_AGENTS.includes(agent));
9030
+ for (const agent of agents) {
9031
+ const registrations = await readAgentRegistrations(agent, target.homeDir, void 0, target.codexHome).catch((error) => {
9032
+ checks.push({ name: "registration", ok: false, detail: `${agent}: ${message2(error)}` });
9033
+ return void 0;
9034
+ });
9035
+ if (!registrations) continue;
9036
+ const headers = hubHeaders(token, agent);
9037
+ for (const identity of config.identities) {
9038
+ const entry = registrations.find((registration) => registration.name === identity.name);
9039
+ const exact = entry?.url === mcpHubUrl(config.port, identity.name) && entry.headers?.Authorization === headers.Authorization;
9040
+ checks.push({
9041
+ name: "registration",
9042
+ ok: exact,
9043
+ detail: `${agent}: ${identity.name} ${exact ? "points at the hub with the current token" : "is missing, stdio, or uses a stale hub URL/token"}`
9044
+ });
9045
+ }
9046
+ }
9047
+ for (const identity of config.identities) {
9048
+ try {
9049
+ const detail = await dependencies.verifyIdentity(
9050
+ mcpHubUrl(config.port, identity.name),
9051
+ { Authorization: `Bearer ${token}`, "X-Topolo-Agent": "topolo-doctor" }
9052
+ );
9053
+ checks.push({ name: "mcp", ok: true, detail: `${identity.name}: ${detail}` });
9054
+ } catch (error) {
9055
+ checks.push({ name: "mcp", ok: false, detail: `${identity.name}: ${message2(error)}` });
9056
+ }
9057
+ }
9058
+ return done();
9059
+ }
9060
+ function formatSharedDoctor(result) {
9061
+ return result.checks.map((check) => `${check.ok ? "PASS" : "FAIL"} ${check.name}: ${check.detail}`).join("\n");
9062
+ }
9063
+
9064
+ // src/commands/doctor.ts
8053
9065
  var defaultDependencies = {
8054
9066
  resolveMcp: resolveMcpCommand,
8055
- verifyFile: (path) => access(path),
9067
+ verifyFile: (path) => access2(path),
8056
9068
  verifyAuthentication: async (context, environment) => {
8057
9069
  const topolo = await buildClient({ context, environment });
8058
9070
  if (!topolo) throw new Error(context ? `No credential configured for ${context}. Run \`topolo auth login\` for that context.` : "No credential configured. Run `topolo auth login`.");
@@ -8092,7 +9104,7 @@ async function runDoctor(dependencies = defaultDependencies, target) {
8092
9104
  return { ok: checks.every((check) => check.ok), cliVersion: CLI_VERSION, checks };
8093
9105
  }
8094
9106
  async function verifyMcpProtocol(command, args, env = definedEnvironment(process.env)) {
8095
- const client = new Client({ name: "topolo-cli-doctor", version: CLI_VERSION });
9107
+ const client = new Client2({ name: "topolo-cli-doctor", version: CLI_VERSION });
8096
9108
  const transport = new StdioClientTransport({ command, args, stderr: "pipe", env });
8097
9109
  try {
8098
9110
  await client.connect(transport);
@@ -8132,7 +9144,7 @@ async function resolveInstalledDoctorTarget(options) {
8132
9144
  preflightChecks.push({
8133
9145
  name: "registration",
8134
9146
  ok: exact,
8135
- detail: `${row.agent}: ${exact ? `${name} uses the bundled launcher` : `${name} is missing or does not use the bundled launcher`}`
9147
+ detail: `${row.agent}: ${exact ? `${name} uses the bundled launcher` : registration?.url ? `${name} is registered with the shared MCP hub; run \`topolo doctor --shared\`` : `${name} is missing or does not use the bundled launcher`}`
8136
9148
  });
8137
9149
  }
8138
9150
  const context = options.context !== void 0 ? normalizeDoctorContext(options.context) : selected?.env.TOPOLO_CONTEXT;
@@ -8150,9 +9162,15 @@ function definedEnvironment(environment) {
8150
9162
  return Object.fromEntries(Object.entries(environment).filter((entry) => entry[1] !== void 0));
8151
9163
  }
8152
9164
  function registerDoctorCommand(program2) {
8153
- program2.command("doctor").description("Verify installed agent surfaces, exact credential, and MCP protocol end to end").option("--name <name>", "Verify one installed MCP registration (defaults to topolo)").option("--context <context>", "Verify one exact personal or organization context").option("--environment <env>", "Override the context environment").option("--claude", "Verify Claude Code only").option("--codex", "Verify Codex only").option("--agent <host...>", "Verify one or more hosts by id").option("--all", "Verify every supported local host").option("--project <dir>", "Inspect project-scoped host files under <dir>").option("--agent-home <dir>", "Inspect an alternate agent home").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
9165
+ program2.command("doctor").description("Verify installed agent surfaces, exact credential, and MCP protocol end to end").option("--name <name>", "Verify one installed MCP registration (defaults to topolo)").option("--context <context>", "Verify one exact personal or organization context").option("--environment <env>", "Override the context environment").option("--claude", "Verify Claude Code only").option("--codex", "Verify Codex only").option("--agent <host...>", "Verify one or more hosts by id").option("--all", "Verify every supported local host").option("--project <dir>", "Inspect project-scoped host files under <dir>").option("--agent-home <dir>", "Inspect an alternate agent home").option("--shared", "Verify the shared local MCP hub end to end for every hub identity (or --name)").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
8154
9166
  const mode = resolveOutputMode(opts.json);
8155
9167
  try {
9168
+ if (opts.shared) {
9169
+ const shared = await runSharedDoctor(opts, defaultSharedDoctorDependencies());
9170
+ if (!shared.ok) throw new Error(formatSharedDoctor(shared));
9171
+ emit(shared, mode, () => formatSharedDoctor(shared));
9172
+ return;
9173
+ }
8156
9174
  const target = await resolveInstalledDoctorTarget(opts);
8157
9175
  const result = await runDoctor(defaultDependencies, target);
8158
9176
  if (!result.ok) throw new Error(formatDoctor(result));
@@ -8749,6 +9767,248 @@ function registerInstallationsCommand(program2) {
8749
9767
  });
8750
9768
  }
8751
9769
 
9770
+ // src/commands/mcp.ts
9771
+ import { createTopoloMcpLocalHandler } from "@topolo/mcp/local";
9772
+
9773
+ // src/lib/mcp-hub-server.ts
9774
+ import { createHash as createHash4, timingSafeEqual } from "crypto";
9775
+ import { stat as stat2 } from "fs/promises";
9776
+ import { createServer } from "http";
9777
+ var MCP_HUB_HOST = "127.0.0.1";
9778
+ var MCP_HUB_MAX_BODY_BYTES = 4 * 1024 * 1024;
9779
+ var DEFAULT_AGENT_LABEL = "topolo-mcp-hub";
9780
+ var AGENT_LABELS3 = /* @__PURE__ */ new Set([...Object.values(AGENT_NAME_FOR), "topolo-doctor"]);
9781
+ var STRIPPED_HEADERS = /* @__PURE__ */ new Set(["authorization", "x-api-key", "cookie", "host", "origin"]);
9782
+ function json(status, body, headers = {}) {
9783
+ return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json", ...headers } });
9784
+ }
9785
+ function refusal(status, message3, headers = {}) {
9786
+ return json(status, { jsonrpc: "2.0", error: { code: -32e3, message: message3 }, id: null }, headers);
9787
+ }
9788
+ function digest(value) {
9789
+ return createHash4("sha256").update(value, "utf8").digest();
9790
+ }
9791
+ function bearerTokenMatches(header, token) {
9792
+ const match = /^Bearer[ \t]+(\S+)[ \t]*$/i.exec(header ?? "");
9793
+ if (!match) return false;
9794
+ return timingSafeEqual(digest(match[1]), digest(token));
9795
+ }
9796
+ function loopbackHosts(port) {
9797
+ return /* @__PURE__ */ new Set([`127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`]);
9798
+ }
9799
+ function loopbackOrigins(port) {
9800
+ return new Set([...loopbackHosts(port)].map((host) => `http://${host}`));
9801
+ }
9802
+ function agentLabel(value) {
9803
+ const label = value?.trim().toLowerCase() ?? "";
9804
+ return AGENT_LABELS3.has(label) ? label : DEFAULT_AGENT_LABEL;
9805
+ }
9806
+ function createMcpHubHandler(options) {
9807
+ const hosts = loopbackHosts(options.port);
9808
+ const origins = loopbackOrigins(options.port);
9809
+ const now = options.now ?? Date.now;
9810
+ const log = options.log ?? (() => void 0);
9811
+ const handlers = /* @__PURE__ */ new Map();
9812
+ function handlerFor(identity, agentName) {
9813
+ const key = JSON.stringify([identity.name, identity.environment, identity.context ?? null, agentName]);
9814
+ let handler = handlers.get(key);
9815
+ if (!handler) {
9816
+ handler = options.createIdentityHandler(identity, agentName);
9817
+ handlers.set(key, handler);
9818
+ }
9819
+ return handler;
9820
+ }
9821
+ async function forward(request, url, identity) {
9822
+ const headers = new Headers();
9823
+ request.headers.forEach((value, name) => {
9824
+ if (!STRIPPED_HEADERS.has(name.toLowerCase())) headers.set(name, value);
9825
+ });
9826
+ const body = request.method === "GET" || request.method === "HEAD" ? void 0 : await request.arrayBuffer();
9827
+ const forwarded = new Request(`http://${MCP_HUB_HOST}:${options.port}${url.pathname}`, {
9828
+ method: request.method,
9829
+ headers,
9830
+ ...body ? { body } : {}
9831
+ });
9832
+ return handlerFor(identity, agentLabel(request.headers.get("x-topolo-agent"))).fetch(forwarded);
9833
+ }
9834
+ async function route(request) {
9835
+ const host = request.headers.get("host")?.toLowerCase();
9836
+ if (!host || !hosts.has(host)) return refusal(403, "Host is not a loopback address for this hub.");
9837
+ const origin = request.headers.get("origin");
9838
+ if (origin !== null && !origins.has(origin.toLowerCase())) {
9839
+ return refusal(403, "Cross-origin requests to the Topolo MCP hub are refused.");
9840
+ }
9841
+ if (!bearerTokenMatches(request.headers.get("authorization"), options.token)) {
9842
+ return refusal(401, "Missing or invalid Topolo MCP hub token.", { "www-authenticate": 'Bearer realm="topolo-mcp-hub"' });
9843
+ }
9844
+ const url = new URL(request.url);
9845
+ let config;
9846
+ try {
9847
+ config = await options.loadConfig();
9848
+ } catch {
9849
+ return refusal(500, "The Topolo MCP hub configuration is invalid. Run `topolo doctor --shared`.");
9850
+ }
9851
+ if (url.pathname === "/health") {
9852
+ if (request.method !== "GET") return refusal(405, "Use GET for /health.", { allow: "GET" });
9853
+ return json(200, { ok: true, identities: config.identities.map((identity2) => identity2.name) });
9854
+ }
9855
+ const match = /^\/mcp\/([^/]+)$/.exec(url.pathname);
9856
+ const identity = match ? config.identities.find((entry) => entry.name === match[1]) : void 0;
9857
+ if (!identity) return refusal(404, "No Topolo MCP identity is mounted at this path.");
9858
+ try {
9859
+ return await forward(request, url, identity);
9860
+ } catch {
9861
+ return refusal(500, "The Topolo MCP identity failed unexpectedly.");
9862
+ }
9863
+ }
9864
+ return async (request) => {
9865
+ const started = now();
9866
+ const response = await route(request);
9867
+ log(`${request.method} ${new URL(request.url).pathname} ${response.status} ${now() - started}ms`);
9868
+ return response;
9869
+ };
9870
+ }
9871
+ function createMcpHubConfigLoader(path) {
9872
+ let cached;
9873
+ return async () => {
9874
+ let signature = "missing";
9875
+ try {
9876
+ const info = await stat2(path);
9877
+ signature = [info.ino, info.size, info.mtimeMs].join(":");
9878
+ } catch (error) {
9879
+ if (error.code !== "ENOENT") throw error;
9880
+ }
9881
+ if (cached?.signature === signature) return cached.config;
9882
+ const config = await readMcpHubConfig(path);
9883
+ cached = { signature, config };
9884
+ return config;
9885
+ };
9886
+ }
9887
+ async function readBody(request, limit) {
9888
+ const chunks = [];
9889
+ let size = 0;
9890
+ for await (const chunk of request) {
9891
+ size += chunk.length;
9892
+ if (size > limit) throw new Error("Request body exceeds the hub limit.");
9893
+ chunks.push(chunk);
9894
+ }
9895
+ return Buffer.concat(chunks);
9896
+ }
9897
+ function toWebRequest(request, body, port) {
9898
+ const headers = new Headers();
9899
+ for (const [name, value] of Object.entries(request.headers)) {
9900
+ if (value === void 0) continue;
9901
+ headers.set(name, Array.isArray(value) ? value.join(", ") : value);
9902
+ }
9903
+ return new Request(`http://${MCP_HUB_HOST}:${port}${request.url ?? "/"}`, {
9904
+ method: request.method ?? "GET",
9905
+ headers,
9906
+ ...body && body.length ? { body } : {}
9907
+ });
9908
+ }
9909
+ async function writeWebResponse(response, target) {
9910
+ const headers = {};
9911
+ response.headers.forEach((value, name) => {
9912
+ headers[name] = value;
9913
+ });
9914
+ const body = Buffer.from(await response.arrayBuffer());
9915
+ target.writeHead(response.status, headers);
9916
+ target.end(body);
9917
+ }
9918
+ async function startMcpHub(options) {
9919
+ const server = createServer();
9920
+ await new Promise((resolve5, reject) => {
9921
+ server.once("error", reject);
9922
+ server.listen(options.port, MCP_HUB_HOST, () => {
9923
+ server.off("error", reject);
9924
+ resolve5();
9925
+ });
9926
+ });
9927
+ const port = server.address().port;
9928
+ const handle = createMcpHubHandler({ ...options, port });
9929
+ const limit = options.maxBodyBytes ?? MCP_HUB_MAX_BODY_BYTES;
9930
+ server.on("request", (request, response) => {
9931
+ void (async () => {
9932
+ let body;
9933
+ try {
9934
+ body = request.method === "GET" || request.method === "HEAD" ? void 0 : await readBody(request, limit);
9935
+ } catch {
9936
+ await writeWebResponse(refusal(413, "Request body is too large for the Topolo MCP hub."), response);
9937
+ request.resume();
9938
+ return;
9939
+ }
9940
+ let webRequest;
9941
+ try {
9942
+ webRequest = toWebRequest(request, body, port);
9943
+ } catch {
9944
+ await writeWebResponse(refusal(400, "Malformed request."), response);
9945
+ return;
9946
+ }
9947
+ await writeWebResponse(await handle(webRequest), response);
9948
+ })().catch(() => {
9949
+ response.destroy();
9950
+ });
9951
+ });
9952
+ return {
9953
+ port,
9954
+ server,
9955
+ close: () => new Promise((resolve5) => {
9956
+ server.close(() => resolve5());
9957
+ server.closeAllConnections();
9958
+ })
9959
+ };
9960
+ }
9961
+
9962
+ // src/commands/mcp.ts
9963
+ var createLocalIdentityHandler = (identity, agentName) => createTopoloMcpLocalHandler({
9964
+ identity: { environment: identity.environment, context: identity.context ?? null },
9965
+ agentName
9966
+ });
9967
+ var defaultServeDependencies = () => ({
9968
+ paths: () => mcpHubPaths(),
9969
+ start: startMcpHub,
9970
+ createIdentityHandler: createLocalIdentityHandler,
9971
+ log: (line) => process.stderr.write(`${(/* @__PURE__ */ new Date()).toISOString()} topolo mcp hub: ${line}
9972
+ `),
9973
+ onSignal: (signal, listener) => {
9974
+ process.once(signal, listener);
9975
+ },
9976
+ exit: (code) => process.exit(code)
9977
+ });
9978
+ async function serveMcpHub(options, dependencies) {
9979
+ const paths = dependencies.paths();
9980
+ const config = await readMcpHubConfig(paths.config);
9981
+ const port = options.port !== void 0 ? validateMcpHubPort(options.port) : config.port;
9982
+ const { token } = await ensureMcpHubToken(paths.token);
9983
+ const hub = await dependencies.start({
9984
+ port,
9985
+ token,
9986
+ loadConfig: createMcpHubConfigLoader(paths.config),
9987
+ createIdentityHandler: dependencies.createIdentityHandler,
9988
+ log: dependencies.log
9989
+ });
9990
+ dependencies.log(
9991
+ `listening on http://127.0.0.1:${hub.port}/mcp/<name> with ${config.identities.length} identities (${config.identities.map((identity) => identity.name).join(", ") || "none yet"}); config ${paths.config}`
9992
+ );
9993
+ const stop = () => {
9994
+ dependencies.log("stopping");
9995
+ void hub.close().then(() => dependencies.exit(0));
9996
+ };
9997
+ dependencies.onSignal("SIGINT", stop);
9998
+ dependencies.onSignal("SIGTERM", stop);
9999
+ return hub;
10000
+ }
10001
+ function registerMcpCommand(program2, dependencies = defaultServeDependencies) {
10002
+ const mcp = program2.command("mcp").description("Run the shared local Topolo MCP hub (one process for every agent thread and identity)");
10003
+ mcp.command("serve").description("Serve every identity in mcp-hub.json at http://127.0.0.1:<port>/mcp/<name> (loopback only, bearer token required)").option("--port <port>", "Port on 127.0.0.1 (defaults to the port in mcp-hub.json, 7337)").action(async (opts) => {
10004
+ try {
10005
+ await serveMcpHub(opts, dependencies());
10006
+ } catch (error) {
10007
+ fail2(error, resolveOutputMode(false));
10008
+ }
10009
+ });
10010
+ }
10011
+
8752
10012
  // src/index.ts
8753
10013
  import { Agent, setGlobalDispatcher } from "undici";
8754
10014
  setGlobalDispatcher(new Agent({ connect: { timeout: 3e4 } }));
@@ -8774,6 +10034,7 @@ program.hook("preAction", async (thisCommand, actionCommand) => {
8774
10034
  if (opts.env || environmentWasStated) return;
8775
10035
  for (let node = actionCommand; node; node = node.parent) {
8776
10036
  if (node.name() === "agent-packs") return;
10037
+ if (node.name() === "mcp" && node.parent === program) return;
8777
10038
  }
8778
10039
  const authenticated = await authenticatedEnvironments();
8779
10040
  if (authenticated.length < 2) return;
@@ -8796,6 +10057,7 @@ Or set it once for the whole session:
8796
10057
  registerAuthCommands(program);
8797
10058
  registerWhoamiCommand(program);
8798
10059
  registerActionsCommand(program);
10060
+ registerCredentialsCommand(program);
8799
10061
  registerApiCommand(program);
8800
10062
  registerAppsCommand(program);
8801
10063
  registerOAuthCommands(program);
@@ -8809,6 +10071,7 @@ registerResourcesCommand(program);
8809
10071
  registerWorkspacesCommand(program);
8810
10072
  registerInstallationsCommand(program);
8811
10073
  registerAgentPacksCommand(program);
10074
+ registerMcpCommand(program);
8812
10075
  var helpCommands = [program];
8813
10076
  for (const command of helpCommands) {
8814
10077
  command.configureHelp({