@topolo/cli 0.13.26 → 0.13.28

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.
Files changed (2) hide show
  1. package/dist/index.js +841 -525
  2. package/package.json +5 -4
package/dist/index.js CHANGED
@@ -308,7 +308,7 @@ import { Command, Help } from "commander";
308
308
  import { createInterface } from "readline/promises";
309
309
  import { stdin as input } from "process";
310
310
 
311
- // node_modules/@topolo/sdk/dist/index.js
311
+ // node_modules/.pnpm/@topolo+sdk@0.24.1/node_modules/@topolo/sdk/dist/index.js
312
312
  function record(value) {
313
313
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
314
314
  }
@@ -326,78 +326,10 @@ function validTopoloActionAccessPolicy(value) {
326
326
  }
327
327
  return true;
328
328
  }
329
- function record2(value) {
330
- return !!value && typeof value === "object" && !Array.isArray(value);
331
- }
332
- function text(value, limit = 512) {
333
- return typeof value === "string" && value.trim() ? value.trim().slice(0, limit) : null;
334
- }
335
- function strings2(value) {
336
- return Array.isArray(value) ? value.slice(0, 8).flatMap((item) => {
337
- const result = text(item);
338
- return result ? [result] : [];
339
- }) : [];
340
- }
341
- function pairs(value, first, second) {
342
- return Array.isArray(value) ? value.slice(0, 8).flatMap((item) => {
343
- if (!record2(item)) return [];
344
- const a = text(item[first], 160);
345
- const b = text(item[second]);
346
- return a && b ? [{ [first]: a, [second]: b }] : [];
347
- }) : [];
348
- }
349
- function docs(value) {
350
- const result = text(value, 2048);
351
- if (!result) return null;
352
- try {
353
- const url = new URL(result);
354
- return url.protocol === "https:" && !url.username && !url.password ? url.href : null;
355
- } catch {
356
- return null;
357
- }
358
- }
359
- function normalizeApplicationGuide(value) {
360
- if (!record2(value) || value.schemaVersion !== 1 || value.source !== "published_application_manifest") return null;
361
- const purpose = text(value.purpose, 2048);
362
- const gettingStarted = strings2(value.gettingStarted);
363
- const docsUrl = docs(value.docsUrl);
364
- return {
365
- schemaVersion: 1,
366
- source: "published_application_manifest",
367
- publishedAt: text(value.publishedAt, 160),
368
- purpose,
369
- problemStatement: text(value.problemStatement, 2048),
370
- solutionStatement: text(value.solutionStatement, 2048),
371
- benefits: strings2(value.benefits),
372
- features: pairs(value.features, "title", "description"),
373
- gettingStarted,
374
- worksWellWith: pairs(value.worksWellWith, "appId", "reason"),
375
- docsUrl,
376
- missingFields: [...!purpose ? ["purpose"] : [], ...!docsUrl ? ["docsUrl"] : [], ...!gettingStarted.length ? ["gettingStarted"] : []]
377
- };
378
- }
379
- function applyAuthHeaders(headers, credential) {
380
- if (credential.kind === "api_key") {
381
- headers.set("X-Api-Key", credential.apiKey);
382
- return;
383
- }
384
- headers.set("Authorization", `${credential.kind === "dpop_access_token" ? "DPoP" : "Bearer"} ${credential.accessToken}`);
385
- }
386
- function applyAuditHeaders(headers, agent, requestId) {
387
- headers.set("X-Topolo-Client", `${agent.clientName}/${agent.clientVersion}`);
388
- if (agent.agentName) headers.set("X-Topolo-Agent", agent.agentName);
389
- headers.set("X-Topolo-Request-Id", requestId);
390
- }
391
- function generateRequestId() {
392
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
393
- return crypto.randomUUID();
394
- }
395
- return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
396
- }
397
329
  var TopoloSdkError = class extends Error {
398
330
  code;
399
- constructor(code, message3) {
400
- super(message3);
331
+ constructor(code, message2) {
332
+ super(message2);
401
333
  this.name = "TopoloSdkError";
402
334
  this.code = code;
403
335
  }
@@ -518,8 +450,8 @@ var TopoloAuthError = class extends TopoloSdkError {
518
450
  requestId;
519
451
  retryAfter;
520
452
  details;
521
- constructor(message3, code = "auth_error", options = {}) {
522
- super(canonicalTopoloServiceErrorCode(code) ?? "auth_error", message3);
453
+ constructor(message2, code = "auth_error", options = {}) {
454
+ super(canonicalTopoloServiceErrorCode(code) ?? "auth_error", message2);
523
455
  this.name = "TopoloAuthError";
524
456
  this.status = options.status ?? 401;
525
457
  this.body = options.body;
@@ -541,10 +473,10 @@ var TopoloPermissionError = class extends TopoloSdkError {
541
473
  requestId;
542
474
  retryAfter;
543
475
  details;
544
- constructor(message3, required, options = {}) {
476
+ constructor(message2, required, options = {}) {
545
477
  super(
546
478
  canonicalTopoloServiceErrorCode(options.code) ?? serviceErrorCode(options.body, "permission_denied"),
547
- message3
479
+ message2
548
480
  );
549
481
  this.name = "TopoloPermissionError";
550
482
  this.required = canonicalTopoloRequired(required);
@@ -585,8 +517,8 @@ function extractTopoloServiceErrorMessage(body) {
585
517
  candidates.push(record5.message, record5.error_description);
586
518
  if (typeof nested === "string" && !canonicalTopoloServiceErrorCode(nested)) candidates.push(nested);
587
519
  for (const candidate of candidates) {
588
- const message3 = sanitizeTopoloServiceMessage(candidate);
589
- if (message3) return message3;
520
+ const message2 = sanitizeTopoloServiceMessage(candidate);
521
+ if (message2) return message2;
590
522
  }
591
523
  return void 0;
592
524
  }
@@ -660,8 +592,8 @@ function validationIssueDetails(issues) {
660
592
  }
661
593
  return paths.length ? { paths, issues } : { issues };
662
594
  }
663
- function issue(path, message3) {
664
- const text4 = sanitizeTopoloServiceMessage(message3);
595
+ function issue(path, message2) {
596
+ const text4 = sanitizeTopoloServiceMessage(message2);
665
597
  if (!path && !text4) return null;
666
598
  return { ...path ? { path } : {}, ...text4 ? { message: text4 } : {} };
667
599
  }
@@ -681,8 +613,8 @@ function canonicalTopoloValidationIssues(values) {
681
613
  function flattenedValidationIssues(formErrors, fieldErrors) {
682
614
  const issues = [];
683
615
  if (Array.isArray(formErrors)) {
684
- for (const message3 of formErrors) {
685
- const projected = issue(void 0, message3);
616
+ for (const message2 of formErrors) {
617
+ const projected = issue(void 0, message2);
686
618
  if (projected) issues.push(projected);
687
619
  }
688
620
  }
@@ -691,8 +623,8 @@ function flattenedValidationIssues(formErrors, fieldErrors) {
691
623
  const path = canonicalTopoloValidationPath(key);
692
624
  if (!path) continue;
693
625
  const list = Array.isArray(messages) && messages.length ? messages : [void 0];
694
- for (const message3 of list) {
695
- const projected = issue(path, message3);
626
+ for (const message2 of list) {
627
+ const projected = issue(path, message2);
696
628
  if (projected) issues.push(projected);
697
629
  }
698
630
  }
@@ -772,8 +704,8 @@ function topoloErrorHint(error) {
772
704
  return void 0;
773
705
  }
774
706
  var TopoloConfirmationRequiredError = class extends TopoloSdkError {
775
- constructor(message3) {
776
- super("confirmation_required", message3);
707
+ constructor(message2) {
708
+ super("confirmation_required", message2);
777
709
  this.name = "TopoloConfirmationRequiredError";
778
710
  }
779
711
  };
@@ -785,8 +717,8 @@ var TopoloHttpError = class extends TopoloSdkError {
785
717
  requestId;
786
718
  retryAfter;
787
719
  details;
788
- constructor(service, path, status, body, message3, options = {}) {
789
- super(serviceErrorCode(body, "http_error"), message3 ?? `HTTP ${status} from ${service}${path}`);
720
+ constructor(service, path, status, body, message2, options = {}) {
721
+ super(serviceErrorCode(body, "http_error"), message2 ?? `HTTP ${status} from ${service}${path}`);
790
722
  this.name = "TopoloHttpError";
791
723
  this.status = status;
792
724
  this.body = body;
@@ -808,8 +740,8 @@ function topoloRetryAfterSeconds(value, now = Date.now()) {
808
740
  var TopoloRateLimitError = class extends TopoloHttpError {
809
741
  retryable = true;
810
742
  retryAfterSeconds;
811
- constructor(service, path, body, message3, options = {}) {
812
- super(service, path, 429, body, message3, options);
743
+ constructor(service, path, body, message2, options = {}) {
744
+ super(service, path, 429, body, message2, options);
813
745
  this.name = "TopoloRateLimitError";
814
746
  if (this.code === "http_error") this.code = "rate_limited";
815
747
  this.retryAfterSeconds = topoloRetryAfterSeconds(this.retryAfter);
@@ -817,158 +749,12 @@ var TopoloRateLimitError = class extends TopoloHttpError {
817
749
  };
818
750
  var TopoloReauthRequiredError = class extends TopoloAuthError {
819
751
  reauthorization;
820
- constructor(message3, reauthorization, options = {}) {
821
- super(message3, "reauth_required", { status: 401, service: "auth", path: "/api/developer-oauth/token", ...options });
752
+ constructor(message2, reauthorization, options = {}) {
753
+ super(message2, "reauth_required", { status: 401, service: "auth", path: "/api/developer-oauth/token", ...options });
822
754
  this.name = "TopoloReauthRequiredError";
823
755
  this.reauthorization = Object.freeze({ ...reauthorization });
824
756
  }
825
757
  };
826
- var ACTION_SECRET_PREFIX = "topolo-secret:";
827
- function isActionSecretReference(value) {
828
- return typeof value === "string" && value.startsWith(ACTION_SECRET_PREFIX);
829
- }
830
- function isRecord(value) {
831
- return typeof value === "object" && value !== null && !Array.isArray(value);
832
- }
833
- function sensitive(key, value) {
834
- const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
835
- if (normalized === "capability") {
836
- if (typeof value !== "string") return value !== null && value !== void 0 && typeof value !== "boolean" && typeof value !== "number";
837
- const text4 = value.trim();
838
- return Boolean(text4) && !/^[a-z][a-z0-9_-]{0,31}$/.test(text4);
839
- }
840
- return ["authorization", "cookie", "setcookie", "proxyauthorization"].includes(normalized) || /(token|secret|password|privatekey|apikey|backupcodes?|recoverycodes?)$/.test(normalized);
841
- }
842
- function maskActionSecrets(value, secrets) {
843
- let masked = value;
844
- for (const [secret, reference] of secrets) {
845
- const encoded = encodeURIComponent(secret);
846
- const variants = [.../* @__PURE__ */ new Set([secret, encoded])].map((item) => item.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
847
- masked = masked.replace(new RegExp(variants.join("|"), "g"), (match) => match === secret ? reference : encodeURIComponent(reference));
848
- }
849
- return masked;
850
- }
851
- async function resolveActionSecrets(value, store, secrets) {
852
- if (isActionSecretReference(value)) {
853
- if (!store) throw new TopoloSdkError("action_secret_store_required", "Protected action references require a secret store.");
854
- const resolved = await store.load(value);
855
- if (typeof resolved !== "string" || !resolved) throw new TopoloSdkError("action_secret_unsupported_type", "Protected action handoff requires a nonempty string credential.");
856
- secrets.set(resolved, value);
857
- return resolved;
858
- }
859
- if (Array.isArray(value)) return Promise.all(value.map((item) => resolveActionSecrets(item, store, secrets)));
860
- if (!isRecord(value)) return value;
861
- return Object.fromEntries(await Promise.all(Object.entries(value).map(async ([key, item]) => [key, await resolveActionSecrets(item, store, secrets)])));
862
- }
863
- async function protectActionResult(value, store, options = {}) {
864
- const secrets = /* @__PURE__ */ new Map();
865
- function documentation(item, key) {
866
- if (options.preserveActionSchemas === true && ["inputSchema", "outputSchema", "input_schema", "output_schema"].includes(key) && isRecord(item)) return true;
867
- if (options.preserveActionContracts === true && ["agentContract", "agent_contract"].includes(key) && isRecord(item)) return true;
868
- return options.preserveAgentGuidance === true && key === "agentGuidance" && isRecord(item) && item.schemaVersion === 1 && typeof item.kind === "string" && ["action-guide", "product-guide", "concept"].includes(item.kind);
869
- }
870
- function localizationMap(path) {
871
- const location = path.join("/");
872
- if (options.localizationAction === "sources.verify") return location === "data/hashes";
873
- if (options.localizationAction === "values.bulk_upsert") return location === "data/sourceHashes";
874
- return ["translation.release.prepare", "translation.release.import", "translation.release.get"].includes(options.localizationAction ?? "") && /^data\/bundle\/catalogs\/\*\/(sources|(?:packs|provenance)\/[^/]+)$/.test(location);
875
- }
876
- async function protect(item, key = "", path = []) {
877
- if (documentation(item, key)) return item;
878
- if (isActionSecretReference(item)) return item;
879
- if (Array.isArray(item) && /(?:backup|recovery)codes$/.test(key.toLowerCase().replace(/[^a-z0-9]/g, ""))) {
880
- return Promise.all(item.map((child) => protect(child, key, [...path, "*"])));
881
- }
882
- if (sensitive(key, item)) {
883
- const reference = store ? await store.save(item) : "[REDACTED]";
884
- if (typeof item === "string" && item) secrets.set(item, reference);
885
- return reference;
886
- }
887
- if (Array.isArray(item)) return Promise.all(item.map((child) => protect(child, "", [...path, "*"])));
888
- if (!isRecord(item)) return item;
889
- return Object.fromEntries(await Promise.all(Object.entries(item).map(async ([name, child]) => [name, await protect(child, localizationMap(path) ? "" : name, [...path, name])])));
890
- }
891
- const protectedValue = await protect(value);
892
- async function protectEmbedded(item, key = "") {
893
- if (documentation(item, key)) return item;
894
- if (typeof item === "string" && !isActionSecretReference(item) && maskActionSecrets(item, secrets) !== item) {
895
- return store ? store.save(item) : maskActionSecrets(item, secrets);
896
- }
897
- if (Array.isArray(item)) return Promise.all(item.map((child) => protectEmbedded(child)));
898
- if (!isRecord(item)) return item;
899
- return Object.fromEntries(await Promise.all(Object.entries(item).map(async ([key2, child]) => [key2, await protectEmbedded(child, key2)])));
900
- }
901
- return protectEmbedded(protectedValue);
902
- }
903
- function maskActionSecretValue(value, secrets) {
904
- if (value instanceof Error) return maskActionSecretError(value, secrets);
905
- if (typeof value === "string") return maskActionSecrets(value, secrets);
906
- if (Array.isArray(value)) return value.map((item) => maskActionSecretValue(item, secrets));
907
- if (!isRecord(value)) return value;
908
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, maskActionSecretValue(item, secrets)]));
909
- }
910
- function maskActionSecretError(error, secrets) {
911
- if (!(error instanceof Error)) return maskActionSecretValue(error, secrets);
912
- const masked = Object.create(Object.getPrototypeOf(error));
913
- for (const key of Object.getOwnPropertyNames(error)) {
914
- const descriptor = Object.getOwnPropertyDescriptor(error, key);
915
- if ("value" in descriptor) descriptor.value = maskActionSecretValue(descriptor.value, secrets);
916
- Object.defineProperty(masked, key, descriptor);
917
- }
918
- return masked;
919
- }
920
- var encoder = new TextEncoder();
921
- function encodeBase64Url(value) {
922
- const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
923
- let binary = "";
924
- for (const byte of bytes) binary += String.fromCharCode(byte);
925
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
926
- }
927
- function encodeJson(value) {
928
- return encodeBase64Url(encoder.encode(JSON.stringify(value)));
929
- }
930
- async function sha256Base64Url(value) {
931
- return encodeBase64Url(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
932
- }
933
- function publicJwk(privateJwk) {
934
- const { crv, kty, x, y } = privateJwk;
935
- if (kty !== "EC" || crv !== "P-256" || !x || !y || !privateJwk.d) {
936
- throw new TypeError("A private P-256 JSON Web Key is required for DPoP signing.");
937
- }
938
- return { crv, kty, x, y };
939
- }
940
- function canonicalTargetUrl(value) {
941
- const url = new URL(value);
942
- return `${url.origin}${url.pathname}`;
943
- }
944
- async function calculateDpopJkt(privateJwk) {
945
- const jwk = publicJwk(privateJwk);
946
- return sha256Base64Url(JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y }));
947
- }
948
- async function createDpopProof(input2) {
949
- const jwk = publicJwk(input2.privateJwk);
950
- const key = await crypto.subtle.importKey(
951
- "jwk",
952
- input2.privateJwk,
953
- { name: "ECDSA", namedCurve: "P-256" },
954
- false,
955
- ["sign"]
956
- );
957
- const header = encodeJson({ alg: "ES256", typ: "dpop+jwt", jwk });
958
- const claims = encodeJson({
959
- jti: crypto.randomUUID(),
960
- iat: Math.floor(Date.now() / 1e3),
961
- htm: input2.method.toUpperCase(),
962
- htu: canonicalTargetUrl(input2.targetUrl),
963
- ath: await sha256Base64Url(input2.accessToken)
964
- });
965
- const signature = await crypto.subtle.sign(
966
- { name: "ECDSA", hash: "SHA-256" },
967
- key,
968
- encoder.encode(`${header}.${claims}`)
969
- );
970
- return `${header}.${claims}.${encodeBase64Url(signature)}`;
971
- }
972
758
  var TOPOLO_FIRST_PARTY_ORIGINS = Object.freeze({
973
759
  production: Object.freeze([
974
760
  "https://admin.topolo.app",
@@ -1191,83 +977,347 @@ var LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::
1191
977
  function isRuntimeEnvironment(value) {
1192
978
  return typeof value === "string" && TOPOLO_RUNTIME_ENVIRONMENTS.includes(value);
1193
979
  }
1194
- function originIsFirstParty(url, environment) {
1195
- if (environment === "local") {
1196
- return (url.protocol === "http:" || url.protocol === "https:") && LOOPBACK_HOSTNAMES.has(url.hostname);
1197
- }
1198
- if (url.protocol !== "https:") return false;
1199
- return FIRST_PARTY_ORIGIN_SETS[environment].has(url.origin);
980
+ function originIsFirstParty(url, environment) {
981
+ if (environment === "local") {
982
+ return (url.protocol === "http:" || url.protocol === "https:") && LOOPBACK_HOSTNAMES.has(url.hostname);
983
+ }
984
+ if (url.protocol !== "https:") return false;
985
+ return FIRST_PARTY_ORIGIN_SETS[environment].has(url.origin);
986
+ }
987
+ function requireTopoloRuntimeEnvironment(environment) {
988
+ if (!isRuntimeEnvironment(environment)) {
989
+ throw new TopoloSdkError(
990
+ "environment_required",
991
+ 'Refusing to send a Topolo credential: the runtime environment is not resolved. Pass environment: "production" | "staging" | "development" | "local".'
992
+ );
993
+ }
994
+ return environment;
995
+ }
996
+ function assertTopoloServiceUrlEnvironment(service, value, environment) {
997
+ const resolved = requireTopoloRuntimeEnvironment(environment);
998
+ let url;
999
+ try {
1000
+ url = new URL(value);
1001
+ } catch {
1002
+ throw new TopoloSdkError("invalid_service_url", `Invalid URL configured for Topolo service "${service}".`);
1003
+ }
1004
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !originIsFirstParty(url, resolved)) {
1005
+ throw new TopoloSdkError(
1006
+ "environment_mismatch",
1007
+ `Refusing Topolo service "${service}": ${url.origin} is not a first-party ${resolved} service origin.`
1008
+ );
1009
+ }
1010
+ return url.origin;
1011
+ }
1012
+ function topoloServiceOrigins(serviceUrls, environment) {
1013
+ return Object.fromEntries(
1014
+ Object.entries(serviceUrls ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([service, value]) => [
1015
+ service,
1016
+ assertTopoloServiceUrlEnvironment(service, value, environment)
1017
+ ])
1018
+ );
1019
+ }
1020
+ var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
1021
+ async function fetchWithCredentialBoundary(fetchImpl, environment, service, target, init) {
1022
+ const url = new URL(String(target));
1023
+ const origin = assertTopoloServiceUrlEnvironment(service, url.toString(), environment);
1024
+ const response = await fetchImpl(url.toString(), { ...init, redirect: "manual" });
1025
+ let responseOrigin = origin;
1026
+ try {
1027
+ if (response.url) responseOrigin = new URL(response.url).origin;
1028
+ } catch {
1029
+ responseOrigin = "";
1030
+ }
1031
+ if (response.redirected === true || responseOrigin !== origin) {
1032
+ await response.body?.cancel().catch(() => void 0);
1033
+ throw new TopoloSdkError(
1034
+ "credential_redirect_refused",
1035
+ `Refusing a redirected response from Topolo service "${service}"; credentials are never forwarded.`
1036
+ );
1037
+ }
1038
+ if (REDIRECT_STATUSES.has(response.status)) {
1039
+ const location = response.headers.get("Location");
1040
+ let destination = null;
1041
+ if (location !== null) {
1042
+ try {
1043
+ destination = new URL(location, url).origin;
1044
+ } catch {
1045
+ destination = null;
1046
+ }
1047
+ }
1048
+ if (location !== null && destination !== origin) {
1049
+ await response.body?.cancel().catch(() => void 0);
1050
+ throw new TopoloSdkError(
1051
+ "credential_redirect_refused",
1052
+ `Refusing a redirect from Topolo service "${service}" to another origin; credentials are never forwarded.`
1053
+ );
1054
+ }
1055
+ }
1056
+ return response;
1057
+ }
1058
+ var MARKETING_ORIGINS = Object.freeze({
1059
+ development: "https://topolo.dev",
1060
+ staging: "https://stg.topolo.us",
1061
+ production: "https://topolo.io"
1062
+ });
1063
+ function isOnePublicMarketingAction(entry) {
1064
+ return entry.appId === "app_topolo_one" && entry.target === "marketing" && entry.authorizationMode === "public";
1065
+ }
1066
+ function publicMarketingOrigin(entry, environment) {
1067
+ const selected = requireTopoloRuntimeEnvironment(environment);
1068
+ const origin = selected === "local" ? void 0 : MARKETING_ORIGINS[selected];
1069
+ if (!isOnePublicMarketingAction(entry) || !origin || entry.apiBaseUrl !== origin && entry.apiBaseUrl !== origin + "/") {
1070
+ throw new TopoloSdkError("environment_mismatch", "Public action target does not match the registered application, target and environment.");
1071
+ }
1072
+ return origin;
1073
+ }
1074
+ async function fetchPublicMarketingAction(fetchImpl, entry, environment, target, init) {
1075
+ const origin = publicMarketingOrigin(entry, environment);
1076
+ if (target.origin !== origin || target.protocol !== "https:" || target.username || target.password || target.port) {
1077
+ throw new TopoloSdkError("service_path_origin_mismatch", "Public action path changes the registered target origin.");
1078
+ }
1079
+ init.signal?.throwIfAborted();
1080
+ const headers = new Headers({
1081
+ Accept: "application/json",
1082
+ "X-Topolo-Action-Id": entry.actionId,
1083
+ "X-Topolo-Action-Name": entry.name
1084
+ });
1085
+ const contentType = new Headers(init.headers).get("Content-Type");
1086
+ if (contentType) headers.set("Content-Type", contentType);
1087
+ const response = await fetchImpl(target.toString(), {
1088
+ method: init.method,
1089
+ headers,
1090
+ body: init.body,
1091
+ signal: init.signal,
1092
+ credentials: "omit",
1093
+ redirect: "manual",
1094
+ referrerPolicy: "no-referrer"
1095
+ });
1096
+ let responseOrigin = origin;
1097
+ try {
1098
+ if (response.url) responseOrigin = new URL(response.url).origin;
1099
+ } catch {
1100
+ responseOrigin = "";
1101
+ }
1102
+ if (response.redirected || responseOrigin !== origin || response.status >= 300 && response.status < 400) {
1103
+ await response.body?.cancel().catch(() => void 0);
1104
+ throw new TopoloSdkError("public_action_redirect_refused", "Public action redirects are refused.");
1105
+ }
1106
+ return response;
1107
+ }
1108
+ function record2(value) {
1109
+ return !!value && typeof value === "object" && !Array.isArray(value);
1110
+ }
1111
+ function text(value, limit = 512) {
1112
+ return typeof value === "string" && value.trim() ? value.trim().slice(0, limit) : null;
1113
+ }
1114
+ function strings2(value) {
1115
+ return Array.isArray(value) ? value.slice(0, 8).flatMap((item) => {
1116
+ const result = text(item);
1117
+ return result ? [result] : [];
1118
+ }) : [];
1119
+ }
1120
+ function pairs(value, first, second) {
1121
+ return Array.isArray(value) ? value.slice(0, 8).flatMap((item) => {
1122
+ if (!record2(item)) return [];
1123
+ const a = text(item[first], 160);
1124
+ const b = text(item[second]);
1125
+ return a && b ? [{ [first]: a, [second]: b }] : [];
1126
+ }) : [];
1127
+ }
1128
+ function docs(value) {
1129
+ const result = text(value, 2048);
1130
+ if (!result) return null;
1131
+ try {
1132
+ const url = new URL(result);
1133
+ return url.protocol === "https:" && !url.username && !url.password ? url.href : null;
1134
+ } catch {
1135
+ return null;
1136
+ }
1137
+ }
1138
+ function normalizeApplicationGuide(value) {
1139
+ if (!record2(value) || value.schemaVersion !== 1 || value.source !== "published_application_manifest") return null;
1140
+ const purpose = text(value.purpose, 2048);
1141
+ const gettingStarted = strings2(value.gettingStarted);
1142
+ const docsUrl = docs(value.docsUrl);
1143
+ return {
1144
+ schemaVersion: 1,
1145
+ source: "published_application_manifest",
1146
+ publishedAt: text(value.publishedAt, 160),
1147
+ purpose,
1148
+ problemStatement: text(value.problemStatement, 2048),
1149
+ solutionStatement: text(value.solutionStatement, 2048),
1150
+ benefits: strings2(value.benefits),
1151
+ features: pairs(value.features, "title", "description"),
1152
+ gettingStarted,
1153
+ worksWellWith: pairs(value.worksWellWith, "appId", "reason"),
1154
+ docsUrl,
1155
+ missingFields: [...!purpose ? ["purpose"] : [], ...!docsUrl ? ["docsUrl"] : [], ...!gettingStarted.length ? ["gettingStarted"] : []]
1156
+ };
1157
+ }
1158
+ function applyAuthHeaders(headers, credential) {
1159
+ if (credential.kind === "api_key") {
1160
+ headers.set("X-Api-Key", credential.apiKey);
1161
+ return;
1162
+ }
1163
+ headers.set("Authorization", `${credential.kind === "dpop_access_token" ? "DPoP" : "Bearer"} ${credential.accessToken}`);
1164
+ }
1165
+ function applyAuditHeaders(headers, agent, requestId) {
1166
+ headers.set("X-Topolo-Client", `${agent.clientName}/${agent.clientVersion}`);
1167
+ if (agent.agentName) headers.set("X-Topolo-Agent", agent.agentName);
1168
+ headers.set("X-Topolo-Request-Id", requestId);
1169
+ }
1170
+ function generateRequestId() {
1171
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1172
+ return crypto.randomUUID();
1173
+ }
1174
+ return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
1175
+ }
1176
+ var ACTION_SECRET_PREFIX = "topolo-secret:";
1177
+ function isActionSecretReference(value) {
1178
+ return typeof value === "string" && value.startsWith(ACTION_SECRET_PREFIX);
1179
+ }
1180
+ function isRecord(value) {
1181
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1182
+ }
1183
+ function sensitive(key, value) {
1184
+ const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
1185
+ if (normalized === "capability") {
1186
+ if (typeof value !== "string") return value !== null && value !== void 0 && typeof value !== "boolean" && typeof value !== "number";
1187
+ const text4 = value.trim();
1188
+ return Boolean(text4) && !/^[a-z][a-z0-9_-]{0,31}$/.test(text4);
1189
+ }
1190
+ return ["authorization", "cookie", "setcookie", "proxyauthorization"].includes(normalized) || /(token|secret|password|privatekey|apikey|backupcodes?|recoverycodes?)$/.test(normalized);
1191
+ }
1192
+ function maskActionSecrets(value, secrets) {
1193
+ let masked = value;
1194
+ for (const [secret, reference] of secrets) {
1195
+ const encoded = encodeURIComponent(secret);
1196
+ const variants = [.../* @__PURE__ */ new Set([secret, encoded])].map((item) => item.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
1197
+ masked = masked.replace(new RegExp(variants.join("|"), "g"), (match) => match === secret ? reference : encodeURIComponent(reference));
1198
+ }
1199
+ return masked;
1200
+ }
1201
+ async function resolveActionSecrets(value, store, secrets) {
1202
+ if (isActionSecretReference(value)) {
1203
+ if (!store) throw new TopoloSdkError("action_secret_store_required", "Protected action references require a secret store.");
1204
+ const resolved = await store.load(value);
1205
+ if (typeof resolved !== "string" || !resolved) throw new TopoloSdkError("action_secret_unsupported_type", "Protected action handoff requires a nonempty string credential.");
1206
+ secrets.set(resolved, value);
1207
+ return resolved;
1208
+ }
1209
+ if (Array.isArray(value)) return Promise.all(value.map((item) => resolveActionSecrets(item, store, secrets)));
1210
+ if (!isRecord(value)) return value;
1211
+ return Object.fromEntries(await Promise.all(Object.entries(value).map(async ([key, item]) => [key, await resolveActionSecrets(item, store, secrets)])));
1212
+ }
1213
+ async function protectActionResult(value, store, options = {}) {
1214
+ const secrets = /* @__PURE__ */ new Map();
1215
+ function documentation(item, key) {
1216
+ if (options.preserveActionSchemas === true && ["inputSchema", "outputSchema", "input_schema", "output_schema"].includes(key) && isRecord(item)) return true;
1217
+ if (options.preserveActionContracts === true && ["agentContract", "agent_contract"].includes(key) && isRecord(item)) return true;
1218
+ return options.preserveAgentGuidance === true && key === "agentGuidance" && isRecord(item) && item.schemaVersion === 1 && typeof item.kind === "string" && ["action-guide", "product-guide", "concept"].includes(item.kind);
1219
+ }
1220
+ function localizationMap(path) {
1221
+ const location = path.join("/");
1222
+ if (options.localizationAction === "sources.verify") return location === "data/hashes";
1223
+ if (options.localizationAction === "values.bulk_upsert") return location === "data/sourceHashes";
1224
+ return ["translation.release.prepare", "translation.release.import", "translation.release.get"].includes(options.localizationAction ?? "") && /^data\/bundle\/catalogs\/\*\/(sources|(?:packs|provenance)\/[^/]+)$/.test(location);
1225
+ }
1226
+ async function protect(item, key = "", path = []) {
1227
+ if (documentation(item, key)) return item;
1228
+ if (isActionSecretReference(item)) return item;
1229
+ if (Array.isArray(item) && /(?:backup|recovery)codes$/.test(key.toLowerCase().replace(/[^a-z0-9]/g, ""))) {
1230
+ return Promise.all(item.map((child) => protect(child, key, [...path, "*"])));
1231
+ }
1232
+ if (sensitive(key, item)) {
1233
+ const reference = store ? await store.save(item) : "[REDACTED]";
1234
+ if (typeof item === "string" && item) secrets.set(item, reference);
1235
+ return reference;
1236
+ }
1237
+ if (Array.isArray(item)) return Promise.all(item.map((child) => protect(child, "", [...path, "*"])));
1238
+ if (!isRecord(item)) return item;
1239
+ return Object.fromEntries(await Promise.all(Object.entries(item).map(async ([name, child]) => [name, await protect(child, localizationMap(path) ? "" : name, [...path, name])])));
1240
+ }
1241
+ const protectedValue = await protect(value);
1242
+ async function protectEmbedded(item, key = "") {
1243
+ if (documentation(item, key)) return item;
1244
+ if (typeof item === "string" && !isActionSecretReference(item) && maskActionSecrets(item, secrets) !== item) {
1245
+ return store ? store.save(item) : maskActionSecrets(item, secrets);
1246
+ }
1247
+ if (Array.isArray(item)) return Promise.all(item.map((child) => protectEmbedded(child)));
1248
+ if (!isRecord(item)) return item;
1249
+ return Object.fromEntries(await Promise.all(Object.entries(item).map(async ([key2, child]) => [key2, await protectEmbedded(child, key2)])));
1250
+ }
1251
+ return protectEmbedded(protectedValue);
1252
+ }
1253
+ function maskActionSecretValue(value, secrets) {
1254
+ if (value instanceof Error) return maskActionSecretError(value, secrets);
1255
+ if (typeof value === "string") return maskActionSecrets(value, secrets);
1256
+ if (Array.isArray(value)) return value.map((item) => maskActionSecretValue(item, secrets));
1257
+ if (!isRecord(value)) return value;
1258
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, maskActionSecretValue(item, secrets)]));
1259
+ }
1260
+ function maskActionSecretError(error, secrets) {
1261
+ if (!(error instanceof Error)) return maskActionSecretValue(error, secrets);
1262
+ const masked = Object.create(Object.getPrototypeOf(error));
1263
+ for (const key of Object.getOwnPropertyNames(error)) {
1264
+ const descriptor = Object.getOwnPropertyDescriptor(error, key);
1265
+ if ("value" in descriptor) descriptor.value = maskActionSecretValue(descriptor.value, secrets);
1266
+ Object.defineProperty(masked, key, descriptor);
1267
+ }
1268
+ return masked;
1269
+ }
1270
+ var encoder = new TextEncoder();
1271
+ function encodeBase64Url(value) {
1272
+ const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
1273
+ let binary = "";
1274
+ for (const byte of bytes) binary += String.fromCharCode(byte);
1275
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
1276
+ }
1277
+ function encodeJson(value) {
1278
+ return encodeBase64Url(encoder.encode(JSON.stringify(value)));
1200
1279
  }
1201
- function requireTopoloRuntimeEnvironment(environment) {
1202
- if (!isRuntimeEnvironment(environment)) {
1203
- throw new TopoloSdkError(
1204
- "environment_required",
1205
- 'Refusing to send a Topolo credential: the runtime environment is not resolved. Pass environment: "production" | "staging" | "development" | "local".'
1206
- );
1207
- }
1208
- return environment;
1280
+ async function sha256Base64Url(value) {
1281
+ return encodeBase64Url(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
1209
1282
  }
1210
- function assertTopoloServiceUrlEnvironment(service, value, environment) {
1211
- const resolved = requireTopoloRuntimeEnvironment(environment);
1212
- let url;
1213
- try {
1214
- url = new URL(value);
1215
- } catch {
1216
- throw new TopoloSdkError("invalid_service_url", `Invalid URL configured for Topolo service "${service}".`);
1217
- }
1218
- if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !originIsFirstParty(url, resolved)) {
1219
- throw new TopoloSdkError(
1220
- "environment_mismatch",
1221
- `Refusing Topolo service "${service}": ${url.origin} is not a first-party ${resolved} service origin.`
1222
- );
1283
+ function publicJwk(privateJwk) {
1284
+ const { crv, kty, x, y } = privateJwk;
1285
+ if (kty !== "EC" || crv !== "P-256" || !x || !y || !privateJwk.d) {
1286
+ throw new TypeError("A private P-256 JSON Web Key is required for DPoP signing.");
1223
1287
  }
1224
- return url.origin;
1288
+ return { crv, kty, x, y };
1225
1289
  }
1226
- function topoloServiceOrigins(serviceUrls, environment) {
1227
- return Object.fromEntries(
1228
- Object.entries(serviceUrls ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([service, value]) => [
1229
- service,
1230
- assertTopoloServiceUrlEnvironment(service, value, environment)
1231
- ])
1232
- );
1290
+ function canonicalTargetUrl(value) {
1291
+ const url = new URL(value);
1292
+ return `${url.origin}${url.pathname}`;
1233
1293
  }
1234
- var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
1235
- async function fetchWithCredentialBoundary(fetchImpl, environment, service, target, init) {
1236
- const url = new URL(String(target));
1237
- const origin = assertTopoloServiceUrlEnvironment(service, url.toString(), environment);
1238
- const response = await fetchImpl(url.toString(), { ...init, redirect: "manual" });
1239
- let responseOrigin = origin;
1240
- try {
1241
- if (response.url) responseOrigin = new URL(response.url).origin;
1242
- } catch {
1243
- responseOrigin = "";
1244
- }
1245
- if (response.redirected === true || responseOrigin !== origin) {
1246
- await response.body?.cancel().catch(() => void 0);
1247
- throw new TopoloSdkError(
1248
- "credential_redirect_refused",
1249
- `Refusing a redirected response from Topolo service "${service}"; credentials are never forwarded.`
1250
- );
1251
- }
1252
- if (REDIRECT_STATUSES.has(response.status)) {
1253
- const location = response.headers.get("Location");
1254
- let destination = null;
1255
- if (location !== null) {
1256
- try {
1257
- destination = new URL(location, url).origin;
1258
- } catch {
1259
- destination = null;
1260
- }
1261
- }
1262
- if (location !== null && destination !== origin) {
1263
- await response.body?.cancel().catch(() => void 0);
1264
- throw new TopoloSdkError(
1265
- "credential_redirect_refused",
1266
- `Refusing a redirect from Topolo service "${service}" to another origin; credentials are never forwarded.`
1267
- );
1268
- }
1269
- }
1270
- return response;
1294
+ async function calculateDpopJkt(privateJwk) {
1295
+ const jwk = publicJwk(privateJwk);
1296
+ return sha256Base64Url(JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y }));
1297
+ }
1298
+ async function createDpopProof(input2) {
1299
+ const jwk = publicJwk(input2.privateJwk);
1300
+ const key = await crypto.subtle.importKey(
1301
+ "jwk",
1302
+ input2.privateJwk,
1303
+ { name: "ECDSA", namedCurve: "P-256" },
1304
+ false,
1305
+ ["sign"]
1306
+ );
1307
+ const header = encodeJson({ alg: "ES256", typ: "dpop+jwt", jwk });
1308
+ const claims = encodeJson({
1309
+ jti: crypto.randomUUID(),
1310
+ iat: Math.floor(Date.now() / 1e3),
1311
+ htm: input2.method.toUpperCase(),
1312
+ htu: canonicalTargetUrl(input2.targetUrl),
1313
+ ath: await sha256Base64Url(input2.accessToken)
1314
+ });
1315
+ const signature = await crypto.subtle.sign(
1316
+ { name: "ECDSA", hash: "SHA-256" },
1317
+ key,
1318
+ encoder.encode(`${header}.${claims}`)
1319
+ );
1320
+ return `${header}.${claims}.${encodeBase64Url(signature)}`;
1271
1321
  }
1272
1322
  var APP_API_URLS = {
1273
1323
  auth: "https://auth.topolo.app",
@@ -1668,8 +1718,8 @@ var ASSURANCE_LEVELS = /* @__PURE__ */ new Set(["authenticated", "workload_ident
1668
1718
  var HASH_PATTERN = /^sha256:[a-f0-9]{64}$/;
1669
1719
  var ActorEnvelopeValidationError = class extends Error {
1670
1720
  path;
1671
- constructor(path, message3) {
1672
- super(`${path}: ${message3}`);
1721
+ constructor(path, message2) {
1722
+ super(`${path}: ${message2}`);
1673
1723
  this.name = "ActorEnvelopeValidationError";
1674
1724
  this.path = path;
1675
1725
  }
@@ -1866,8 +1916,8 @@ function textArray(value, path) {
1866
1916
  function optionalTextFields(value, path, keys) {
1867
1917
  return Object.fromEntries(keys.flatMap((key) => value[key] === void 0 ? [] : [[key, text2(value[key], `${path}.${key}`)]]));
1868
1918
  }
1869
- function fail(path, message3) {
1870
- throw new ActorEnvelopeValidationError(path, message3);
1919
+ function fail(path, message2) {
1920
+ throw new ActorEnvelopeValidationError(path, message2);
1871
1921
  }
1872
1922
  function parseTopoloActionUploadContract(value) {
1873
1923
  if (!isRecord3(value) || value.profile !== "inline-base64" && value.profile !== "url-session") {
@@ -2140,7 +2190,17 @@ var TopoloClient = class _TopoloClient {
2140
2190
  * use and for the generic `topolo api` / MCP passthrough tool.
2141
2191
  */
2142
2192
  async request(opts) {
2193
+ return this.requestWithTarget(opts);
2194
+ }
2195
+ async requestWithTarget(opts, publicAction) {
2143
2196
  requireTopoloRuntimeEnvironment(this.environment);
2197
+ const publicOrigin = publicAction ? publicMarketingOrigin(publicAction, this.environment) : void 0;
2198
+ if (publicOrigin && (opts.actorEnvelope !== void 0 || opts.resource !== void 0)) {
2199
+ throw new TopoloSdkError("public_action_context_not_supported", "Public marketing actions do not accept actor or resource authority.");
2200
+ }
2201
+ if (publicOrigin && (!opts.path.startsWith("/") || opts.path.startsWith("//") || opts.path.includes("\\"))) {
2202
+ throw new TopoloSdkError("service_path_origin_mismatch", "Public action paths must be origin-relative paths.");
2203
+ }
2144
2204
  const method = opts.method ?? "GET";
2145
2205
  if (this.requireConfirmForWrites && WRITE_METHODS.has(method) && !opts.confirm) {
2146
2206
  throw new TopoloConfirmationRequiredError(
@@ -2149,7 +2209,7 @@ var TopoloClient = class _TopoloClient {
2149
2209
  }
2150
2210
  const actionId = requestActionId(opts.headers);
2151
2211
  const taskId = opts.actorEnvelope?.delegation?.taskId ?? this.actorContext?.taskId;
2152
- let requestCredential = await this.loadCurrentCredential({
2212
+ let requestCredential = publicOrigin ? void 0 : await this.loadCurrentCredential({
2153
2213
  service: normalizeLookupKey(opts.service),
2154
2214
  method,
2155
2215
  path: opts.path,
@@ -2157,7 +2217,7 @@ var TopoloClient = class _TopoloClient {
2157
2217
  ...taskId ? { taskId } : {},
2158
2218
  ...opts.resource ? { resource: opts.resource } : {}
2159
2219
  });
2160
- const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: this.validatedServiceOrigin(opts.service, opts.baseUrl) } : await this.resolveRequestService(opts.service);
2220
+ const resolvedService = publicOrigin ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: publicOrigin } : opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: this.validatedServiceOrigin(opts.service, opts.baseUrl) } : await this.resolveRequestService(opts.service);
2161
2221
  const baseUrl = resolvedService.baseUrl;
2162
2222
  const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
2163
2223
  if (url.origin !== new URL(baseUrl).origin) {
@@ -2178,17 +2238,21 @@ var TopoloClient = class _TopoloClient {
2178
2238
  headers.set("Accept", "application/json");
2179
2239
  if (opts.body !== void 0) headers.set("Content-Type", "application/json");
2180
2240
  const platformAppId = resolveConfiguredAppId(resolvedService.serviceKey, this.appIds);
2181
- if (platformAppId) headers.set("X-App-ID", platformAppId);
2182
- const actorEnvelope = opts.actorEnvelope === void 0 ? void 0 : parseActorEnvelope(opts.actorEnvelope);
2241
+ if (platformAppId && !publicOrigin) headers.set("X-App-ID", platformAppId);
2242
+ const actorEnvelope = publicOrigin || opts.actorEnvelope === void 0 ? void 0 : parseActorEnvelope(opts.actorEnvelope);
2183
2243
  const requestId = actorEnvelope?.action.requestId ?? generateRequestId();
2184
- applyAuditHeaders(headers, this.agent, requestId);
2185
- if (opts.headers) {
2244
+ if (!publicOrigin) applyAuditHeaders(headers, this.agent, requestId);
2245
+ if (publicAction) {
2246
+ headers.set(TOPOLO_ACTION_ID_HEADER, publicAction.actionId);
2247
+ headers.set(TOPOLO_ACTION_NAME_HEADER, publicAction.name);
2248
+ }
2249
+ if (opts.headers && !publicOrigin) {
2186
2250
  for (const [k, v] of Object.entries(opts.headers)) headers.set(k, v);
2187
2251
  }
2188
2252
  if (actorEnvelope) {
2189
2253
  headers.set(TOPOLO_ACTOR_ENVELOPE_HEADER, encodeActorEnvelopeHeader(actorEnvelope));
2190
2254
  }
2191
- if (opts.resource) {
2255
+ if (opts.resource && !publicOrigin) {
2192
2256
  const resourceType = opts.resource.resourceType.trim();
2193
2257
  const resourceId = opts.resource.resourceId.trim();
2194
2258
  if (!resourceType || !resourceId) {
@@ -2213,8 +2277,8 @@ var TopoloClient = class _TopoloClient {
2213
2277
  const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
2214
2278
  try {
2215
2279
  const requestHeaders = new Headers(headers);
2216
- applyAuthHeaders(requestHeaders, requestCredential);
2217
- if (requestCredential.kind === "dpop_access_token") {
2280
+ if (requestCredential) applyAuthHeaders(requestHeaders, requestCredential);
2281
+ if (requestCredential?.kind === "dpop_access_token") {
2218
2282
  requestHeaders.set("DPoP", await createDpopProof({
2219
2283
  accessToken: requestCredential.accessToken,
2220
2284
  method,
@@ -2222,12 +2286,13 @@ var TopoloClient = class _TopoloClient {
2222
2286
  privateJwk: requestCredential.privateJwk
2223
2287
  }));
2224
2288
  }
2225
- return await this.credentialedFetch(resolvedService.serviceKey, urlStr, {
2289
+ const init = {
2226
2290
  method,
2227
2291
  headers: requestHeaders,
2228
2292
  body: opts.rawBody ?? (opts.body !== void 0 ? JSON.stringify(opts.body) : null),
2229
2293
  signal
2230
- });
2294
+ };
2295
+ return publicOrigin ? await fetchPublicMarketingAction((input2, requestInit) => this.fetchImpl(input2, requestInit), publicAction, this.environment, url, init) : await this.credentialedFetch(resolvedService.serviceKey, urlStr, init);
2231
2296
  } catch (error) {
2232
2297
  if (timedOut) {
2233
2298
  throw new TopoloSdkError(
@@ -2235,6 +2300,9 @@ var TopoloClient = class _TopoloClient {
2235
2300
  `Topolo request timed out after ${this.timeoutMs}ms. Verify current state before retrying a mutation.`
2236
2301
  );
2237
2302
  }
2303
+ if (publicOrigin && opts.signal?.aborted) {
2304
+ throw new TopoloSdkError("request_cancelled", "Public action request was cancelled.");
2305
+ }
2238
2306
  throw error;
2239
2307
  } finally {
2240
2308
  clearTimeout(timeoutId);
@@ -2257,7 +2325,7 @@ var TopoloClient = class _TopoloClient {
2257
2325
  try {
2258
2326
  const attemptedCredential = requestCredential;
2259
2327
  res = await fetchWithReadRetry();
2260
- if (res.status === 401 && attemptedCredential.kind === "access_token" && this.refreshCredential) {
2328
+ if (res.status === 401 && attemptedCredential?.kind === "access_token" && this.refreshCredential) {
2261
2329
  await res.body?.cancel().catch(() => void 0);
2262
2330
  const refreshed = await this.refreshRejectedAccessToken(attemptedCredential.accessToken);
2263
2331
  if (refreshed) {
@@ -2304,7 +2372,7 @@ var TopoloClient = class _TopoloClient {
2304
2372
  }
2305
2373
  if (!res.ok) {
2306
2374
  const durationMs = Date.now() - startedAt;
2307
- const message3 = describeError(parsed, `HTTP ${res.status}`);
2375
+ const message2 = describeError(parsed, `HTTP ${res.status}`);
2308
2376
  const publicCode = extractTopoloServiceErrorCode(parsed) ?? "http_error";
2309
2377
  this.emit({
2310
2378
  phase: "error",
@@ -2344,7 +2412,7 @@ var TopoloClient = class _TopoloClient {
2344
2412
  resolvedService.serviceKey,
2345
2413
  opts.path,
2346
2414
  parsed,
2347
- message3,
2415
+ message2,
2348
2416
  errorContext
2349
2417
  );
2350
2418
  }
@@ -2353,7 +2421,7 @@ var TopoloClient = class _TopoloClient {
2353
2421
  opts.path,
2354
2422
  res.status,
2355
2423
  parsed,
2356
- message3,
2424
+ message2,
2357
2425
  errorContext
2358
2426
  );
2359
2427
  }
@@ -2411,6 +2479,9 @@ var TopoloClient = class _TopoloClient {
2411
2479
  return fetchWithCredentialBoundary((input2, requestInit) => this.fetchImpl(input2, requestInit), this.environment, service, target, init);
2412
2480
  }
2413
2481
  validatedActionEntry(entry) {
2482
+ if (isOnePublicMarketingAction(entry)) {
2483
+ return { ...entry, apiBaseUrl: publicMarketingOrigin(entry, this.environment) };
2484
+ }
2414
2485
  if (!entry.apiBaseUrl) return entry;
2415
2486
  return {
2416
2487
  ...entry,
@@ -2652,7 +2723,7 @@ var TopoloClient = class _TopoloClient {
2652
2723
  "TopoloAuth did not return an action catalog for the current credential."
2653
2724
  );
2654
2725
  }
2655
- const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry)).map((entry) => this.validatedActionEntry(entry));
2726
+ const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
2656
2727
  return {
2657
2728
  actions: entries,
2658
2729
  page: normalizeCatalogPage(
@@ -2717,11 +2788,18 @@ var TopoloClient = class _TopoloClient {
2717
2788
  const raw = res.data?.action ?? res.action;
2718
2789
  const match = normalizeActionCatalogEntry(raw);
2719
2790
  if (!match) throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
2791
+ if (isOnePublicMarketingAction(match) && match.actionId !== actionId) {
2792
+ throw new TopoloSdkError("action_contract_mismatch", "Public action catalog response does not match the requested action ID.");
2793
+ }
2720
2794
  return this.validatedActionEntry(match);
2721
2795
  }
2722
2796
  async callAction(action, input2 = {}, options = {}) {
2723
- const entry = await this.getAction(action);
2724
- return this.executeResolvedAction(entry, input2, options);
2797
+ const entry = Object.freeze({ ...await this.getAction(action) });
2798
+ const publicAction = isOnePublicMarketingAction(entry);
2799
+ if (publicAction && entry.actionId !== action.trim()) {
2800
+ throw new TopoloSdkError("action_contract_mismatch", "Public action catalog response does not match the requested action ID.");
2801
+ }
2802
+ return this.executeResolvedAction(entry, input2, options, false, publicAction);
2725
2803
  }
2726
2804
  async issueExternalAgentCredential(input2) {
2727
2805
  const actionId = "app_topolo_auth.principal_credentials.issue";
@@ -2865,7 +2943,15 @@ var TopoloClient = class _TopoloClient {
2865
2943
  }
2866
2944
  return this.executeResolvedAction(entry, input2, options);
2867
2945
  }
2868
- async executeResolvedAction(entry, input2, options, preserveSensitiveResult = false) {
2946
+ async executeResolvedAction(entry, input2, options, preserveSensitiveResult = false, allowPublicMarketing = false) {
2947
+ if (isOnePublicMarketingAction(entry)) {
2948
+ if (!allowPublicMarketing) {
2949
+ throw new TopoloSdkError("public_action_entrypoint_required", "Public marketing actions require callAction.");
2950
+ }
2951
+ if (options.actorEnvelope !== void 0 || options.resource !== void 0) {
2952
+ throw new TopoloSdkError("public_action_context_not_supported", "Public marketing actions do not accept actor or resource authority.");
2953
+ }
2954
+ }
2869
2955
  const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
2870
2956
  if (agentAccess === "off") {
2871
2957
  throw new TopoloPermissionError(
@@ -2894,7 +2980,8 @@ var TopoloClient = class _TopoloClient {
2894
2980
  );
2895
2981
  }
2896
2982
  const mapped = mapActionInput(entry, input2);
2897
- const resolvedActorEnvelope = options.actorEnvelope ?? await this.buildVerifiedAgentActorEnvelope(entry.actionId, input2, options.resource);
2983
+ const publicAction = isOnePublicMarketingAction(entry);
2984
+ const resolvedActorEnvelope = publicAction ? void 0 : options.actorEnvelope ?? await this.buildVerifiedAgentActorEnvelope(entry.actionId, input2, options.resource);
2898
2985
  if (resolvedActorEnvelope) {
2899
2986
  const actorEnvelope = parseActorEnvelope(resolvedActorEnvelope);
2900
2987
  if (actorEnvelope.action.actionId !== entry.actionId) {
@@ -2911,8 +2998,9 @@ var TopoloClient = class _TopoloClient {
2911
2998
  const service = entry.appSlug || entry.appId;
2912
2999
  let result;
2913
3000
  try {
2914
- result = await this.request({
3001
+ result = await this.requestWithTarget({
2915
3002
  service,
3003
+ signal: options.signal,
2916
3004
  ...entry.apiBaseUrl ? { baseUrl: entry.apiBaseUrl } : {},
2917
3005
  method: entry.method,
2918
3006
  path: mapped.path,
@@ -2925,7 +3013,7 @@ var TopoloClient = class _TopoloClient {
2925
3013
  [TOPOLO_ACTION_NAME_HEADER]: entry.name
2926
3014
  },
2927
3015
  confirm: options.confirm === true || entry.readOnly && !entry.destructive && !entry.requiresConfirmation
2928
- });
3016
+ }, publicAction ? entry : void 0);
2929
3017
  } catch (error) {
2930
3018
  throw maskActionSecretError(error, this.actionSecrets);
2931
3019
  }
@@ -4321,7 +4409,7 @@ async function pollUntilApproved(params) {
4321
4409
  // src/commands/auth-lock.ts
4322
4410
  import { createHash as createHash3 } from "crypto";
4323
4411
 
4324
- // node_modules/@topolo/sdk/dist/node/credential-store.js
4412
+ // node_modules/.pnpm/@topolo+sdk@0.24.1/node_modules/@topolo/sdk/dist/node/credential-store.js
4325
4413
  import { mkdir as mkdir2, readFile as readFile2, chmod as chmod2, lstat, readdir as readdir2 } from "fs/promises";
4326
4414
  import { existsSync as existsSync2, mkdirSync, lstatSync, readdirSync, readFileSync as readFileSync2, openSync, writeFileSync, fsyncSync, closeSync, renameSync, unlinkSync, rmdirSync, linkSync } from "fs";
4327
4415
  import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
@@ -4338,8 +4426,8 @@ import { homedir as homedir3, platform as hostPlatform } from "os";
4338
4426
  import { dirname as dirname3, join } from "path";
4339
4427
  var TopoloSdkError2 = class extends Error {
4340
4428
  code;
4341
- constructor(code, message3) {
4342
- super(message3);
4429
+ constructor(code, message2) {
4430
+ super(message2);
4343
4431
  this.name = "TopoloSdkError";
4344
4432
  this.code = code;
4345
4433
  }
@@ -4460,8 +4548,8 @@ var TopoloAuthError2 = class extends TopoloSdkError2 {
4460
4548
  requestId;
4461
4549
  retryAfter;
4462
4550
  details;
4463
- constructor(message3, code = "auth_error", options = {}) {
4464
- super(canonicalTopoloServiceErrorCode2(code) ?? "auth_error", message3);
4551
+ constructor(message2, code = "auth_error", options = {}) {
4552
+ super(canonicalTopoloServiceErrorCode2(code) ?? "auth_error", message2);
4465
4553
  this.name = "TopoloAuthError";
4466
4554
  this.status = options.status ?? 401;
4467
4555
  this.body = options.body;
@@ -4559,8 +4647,8 @@ function validationIssueDetails2(issues) {
4559
4647
  }
4560
4648
  return paths.length ? { paths, issues } : { issues };
4561
4649
  }
4562
- function issue2(path, message3) {
4563
- const text3 = sanitizeTopoloServiceMessage2(message3);
4650
+ function issue2(path, message2) {
4651
+ const text3 = sanitizeTopoloServiceMessage2(message2);
4564
4652
  if (!path && !text3) return null;
4565
4653
  return { ...path ? { path } : {}, ...text3 ? { message: text3 } : {} };
4566
4654
  }
@@ -4580,8 +4668,8 @@ function canonicalTopoloValidationIssues2(values) {
4580
4668
  function flattenedValidationIssues2(formErrors, fieldErrors) {
4581
4669
  const issues = [];
4582
4670
  if (Array.isArray(formErrors)) {
4583
- for (const message3 of formErrors) {
4584
- const projected = issue2(void 0, message3);
4671
+ for (const message2 of formErrors) {
4672
+ const projected = issue2(void 0, message2);
4585
4673
  if (projected) issues.push(projected);
4586
4674
  }
4587
4675
  }
@@ -4590,8 +4678,8 @@ function flattenedValidationIssues2(formErrors, fieldErrors) {
4590
4678
  const path = canonicalTopoloValidationPath2(key);
4591
4679
  if (!path) continue;
4592
4680
  const list = Array.isArray(messages) && messages.length ? messages : [void 0];
4593
- for (const message3 of list) {
4594
- const projected = issue2(path, message3);
4681
+ for (const message2 of list) {
4682
+ const projected = issue2(path, message2);
4595
4683
  if (projected) issues.push(projected);
4596
4684
  }
4597
4685
  }
@@ -4625,8 +4713,8 @@ var TopoloHttpError2 = class extends TopoloSdkError2 {
4625
4713
  requestId;
4626
4714
  retryAfter;
4627
4715
  details;
4628
- constructor(service, path, status, body, message3, options = {}) {
4629
- super(serviceErrorCode2(body, "http_error"), message3 ?? `HTTP ${status} from ${service}${path}`);
4716
+ constructor(service, path, status, body, message2, options = {}) {
4717
+ super(serviceErrorCode2(body, "http_error"), message2 ?? `HTTP ${status} from ${service}${path}`);
4630
4718
  this.name = "TopoloHttpError";
4631
4719
  this.status = status;
4632
4720
  this.body = body;
@@ -4648,8 +4736,8 @@ function topoloRetryAfterSeconds2(value, now = Date.now()) {
4648
4736
  var TopoloRateLimitError2 = class extends TopoloHttpError2 {
4649
4737
  retryable = true;
4650
4738
  retryAfterSeconds;
4651
- constructor(service, path, body, message3, options = {}) {
4652
- super(service, path, 429, body, message3, options);
4739
+ constructor(service, path, body, message2, options = {}) {
4740
+ super(service, path, 429, body, message2, options);
4653
4741
  this.name = "TopoloRateLimitError";
4654
4742
  if (this.code === "http_error") this.code = "rate_limited";
4655
4743
  this.retryAfterSeconds = topoloRetryAfterSeconds2(this.retryAfter);
@@ -4657,8 +4745,8 @@ var TopoloRateLimitError2 = class extends TopoloHttpError2 {
4657
4745
  };
4658
4746
  var TopoloReauthRequiredError2 = class extends TopoloAuthError2 {
4659
4747
  reauthorization;
4660
- constructor(message3, reauthorization, options = {}) {
4661
- super(message3, "reauth_required", { status: 401, service: "auth", path: "/api/developer-oauth/token", ...options });
4748
+ constructor(message2, reauthorization, options = {}) {
4749
+ super(message2, "reauth_required", { status: 401, service: "auth", path: "/api/developer-oauth/token", ...options });
4662
4750
  this.name = "TopoloReauthRequiredError";
4663
4751
  this.reauthorization = Object.freeze({ ...reauthorization });
4664
4752
  }
@@ -6199,12 +6287,12 @@ function renewalFailure(error) {
6199
6287
  if (error instanceof TopoloReauthRequiredError2) return error;
6200
6288
  if (error instanceof TopoloSdkError2 && error.code === "config_unreadable") return error;
6201
6289
  if ((error instanceof TopoloAuthError2 || error instanceof TopoloHttpError2) && error.status === 429) {
6202
- 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.";
6290
+ 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.";
6203
6291
  return new TopoloRateLimitError2(
6204
6292
  "auth",
6205
6293
  path,
6206
- { code: "rate_limited", retryable: true, message: message3 },
6207
- message3,
6294
+ { code: "rate_limited", retryable: true, message: message2 },
6295
+ message2,
6208
6296
  { requestId: error.requestId, retryAfter: error.retryAfter }
6209
6297
  );
6210
6298
  }
@@ -7977,8 +8065,8 @@ var API_KEYS_LIST_ACTION = "app_topolo_auth.api_keys.list";
7977
8065
  var CUSTODY_ENVIRONMENT_NAMES = ["production", "staging", "development", "local"];
7978
8066
  var CustodyError = class extends Error {
7979
8067
  code;
7980
- constructor(code, message3) {
7981
- super(message3);
8068
+ constructor(code, message2) {
8069
+ super(message2);
7982
8070
  this.name = "CustodyError";
7983
8071
  this.code = code;
7984
8072
  }
@@ -8798,7 +8886,7 @@ import { spawnSync as spawnSync5 } from "child_process";
8798
8886
  import { existsSync as existsSync6 } from "fs";
8799
8887
  import { readFile as readFile9, rm as rm3 } from "fs/promises";
8800
8888
  import { homedir as homedir5 } from "os";
8801
- import { dirname as dirname7, join as join11, resolve as resolve4 } from "path";
8889
+ import { dirname as dirname7, join as join12, resolve as resolve4 } from "path";
8802
8890
  import { fileURLToPath as fileURLToPath3 } from "url";
8803
8891
  import { createRequire } from "module";
8804
8892
 
@@ -8813,7 +8901,231 @@ import { spawnSync as spawnSync3 } from "child_process";
8813
8901
  import { existsSync as existsSync4 } from "fs";
8814
8902
  import { readFile as readFile4 } from "fs/promises";
8815
8903
  import { homedir as homedir4 } from "os";
8816
- import { isAbsolute as isAbsolute2, join as join7, resolve as resolve3 } from "path";
8904
+ import { isAbsolute as isAbsolute2, join as join8, resolve as resolve3 } from "path";
8905
+
8906
+ // src/commands/host-json.ts
8907
+ import { createHash as createHash5 } from "crypto";
8908
+ import { chmod as chmod4, lstat as lstat3, mkdir as mkdir4, open as open2, readdir as readdir4, unlink } from "fs/promises";
8909
+ import { basename as basename3, join as join7 } from "path";
8910
+ import {
8911
+ applyEdits,
8912
+ modify,
8913
+ parse as parseJsonc,
8914
+ printParseErrorCode,
8915
+ visit
8916
+ } from "jsonc-parser";
8917
+ var BOM = "\uFEFF";
8918
+ var RESERVED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
8919
+ var JSONC_OPTIONS = { allowTrailingComma: true, disallowComments: false, allowEmptyContent: true };
8920
+ var HOST_JSON_BACKUP_RETENTION = 3;
8921
+ function printable(text3) {
8922
+ return text3.replace(
8923
+ /[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g,
8924
+ (character) => "\\u" + character.charCodeAt(0).toString(16).padStart(4, "0")
8925
+ );
8926
+ }
8927
+ function printableLines(text3) {
8928
+ return text3.split("\n").map((line) => line.split(" ").map(printable).join(" ")).join("\n");
8929
+ }
8930
+ function quotedKey(key) {
8931
+ return '"' + printable(key.replace(/["\\]/g, "\\$&")) + '"';
8932
+ }
8933
+ function malformedHostConfigError(verb, target, reason) {
8934
+ return new Error(
8935
+ "Cannot " + verb + " malformed " + target.label + " configuration at " + printable(target.path) + ": " + reason + " Topolo did not change this file. Fix it in an editor, or move it aside (for example rename it to " + printable(target.path) + ".bak) and run the command again."
8936
+ );
8937
+ }
8938
+ function lineColumn(text3, offset) {
8939
+ const before = text3.slice(0, offset).split("\n");
8940
+ return "line " + before.length + ", column " + (before[before.length - 1].length + 1);
8941
+ }
8942
+ function objectOrThrow(value, verb, target) {
8943
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
8944
+ const found = value === null ? "null" : Array.isArray(value) ? "an array" : typeof value;
8945
+ throw malformedHostConfigError(verb, target, "the top level must be a JSON object ({ ... }), but it is " + found + ".");
8946
+ }
8947
+ return value;
8948
+ }
8949
+ function assertSafeKeys(body, verb, target) {
8950
+ const objects = [];
8951
+ let problem;
8952
+ visit(body, {
8953
+ onObjectBegin: () => {
8954
+ objects.push(/* @__PURE__ */ new Set());
8955
+ },
8956
+ onObjectEnd: () => {
8957
+ objects.pop();
8958
+ },
8959
+ onObjectProperty: (property, offset) => {
8960
+ if (problem) return;
8961
+ const keys = objects[objects.length - 1];
8962
+ if (RESERVED_KEYS.has(property)) {
8963
+ problem = "it contains the reserved key " + quotedKey(property) + " at " + lineColumn(body, offset) + ". Remove or rename that key.";
8964
+ } else if (keys.has(property)) {
8965
+ problem = "it contains the duplicate key " + quotedKey(property) + " (again at " + lineColumn(body, offset) + "). Keep one " + quotedKey(property) + " entry, merging what you need from the other, and remove the duplicate.";
8966
+ }
8967
+ keys.add(property);
8968
+ }
8969
+ }, JSONC_OPTIONS);
8970
+ if (problem) throw malformedHostConfigError(verb, target, problem);
8971
+ }
8972
+ function parseHostJson(text3, target, verb = "update") {
8973
+ const bom = text3?.startsWith(BOM) ? BOM : "";
8974
+ const body = (text3 ?? "").slice(bom.length);
8975
+ if (!body.trim()) return { kind: "empty", value: {}, bom, body };
8976
+ let value;
8977
+ let kind = "strict";
8978
+ try {
8979
+ value = JSON.parse(body);
8980
+ } catch (error) {
8981
+ if (!(error instanceof SyntaxError)) throw error;
8982
+ const errors = [];
8983
+ value = parseJsonc(body, errors, JSONC_OPTIONS);
8984
+ if (errors.length) {
8985
+ const first = errors[0];
8986
+ throw malformedHostConfigError(
8987
+ verb,
8988
+ target,
8989
+ "invalid JSON at " + lineColumn(body, first.offset) + " (" + printParseErrorCode(first.error) + ")."
8990
+ );
8991
+ }
8992
+ if (target.syntax !== "jsonc") {
8993
+ throw malformedHostConfigError(
8994
+ verb,
8995
+ target,
8996
+ "it is not strict JSON: it contains comments or trailing commas, which this file must not have."
8997
+ );
8998
+ }
8999
+ kind = value === void 0 ? "empty" : "jsonc";
9000
+ }
9001
+ assertSafeKeys(body, verb, target);
9002
+ if (kind === "empty") return { kind, value: {}, bom, body };
9003
+ return { kind, value: objectOrThrow(value, verb, target), bom, body };
9004
+ }
9005
+ function hostServers(document, rootKey, target, verb = "update") {
9006
+ const servers = document.value[rootKey];
9007
+ if (servers === void 0) return void 0;
9008
+ if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
9009
+ throw malformedHostConfigError(verb, target, 'the "' + rootKey + '" entry must be a JSON object of MCP servers.');
9010
+ }
9011
+ return servers;
9012
+ }
9013
+ function editsInPlace(document, target) {
9014
+ return target.syntax === "jsonc" && document.body.trim() !== "";
9015
+ }
9016
+ function stableJson(value) {
9017
+ return JSON.stringify(value, (_key, item) => item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b))) : item);
9018
+ }
9019
+ function editHostServer(document, rootKey, name, entry, target) {
9020
+ const servers = hostServers(document, rootKey, target) ?? {};
9021
+ const nextServers = { ...servers };
9022
+ if (entry === void 0) delete nextServers[name];
9023
+ else nextServers[name] = entry;
9024
+ const serialized = JSON.stringify({ ...document.value, [rootKey]: nextServers }, null, 2) + "\n";
9025
+ let text3;
9026
+ if (!editsInPlace(document, target)) {
9027
+ text3 = serialized;
9028
+ } else if (document.kind === "empty") {
9029
+ text3 = document.bom + document.body.trimEnd() + "\n" + serialized;
9030
+ } else {
9031
+ const edits = modify(document.body, [rootKey, name], entry, {
9032
+ formattingOptions: { insertSpaces: true, tabSize: 2, eol: "\n" }
9033
+ });
9034
+ text3 = document.bom + applyEdits(document.body, edits);
9035
+ }
9036
+ const after = parseHostJson(text3, target);
9037
+ const afterServers = Object(hostServers(after, rootKey, target));
9038
+ const preserved = Object.keys(document.value).every((key) => key === rootKey || stableJson(after.value[key]) === stableJson(document.value[key])) && Object.keys(after.value).every((key) => key === rootKey || Object.hasOwn(document.value, key)) && Object.keys(servers).every((key) => key === name || stableJson(afterServers[key]) === stableJson(servers[key])) && Object.keys(afterServers).every((key) => key === name || Object.hasOwn(servers, key)) && (entry === void 0 ? !Object.hasOwn(afterServers, name) : stableJson(afterServers[name]) === stableJson(entry));
9039
+ if (!preserved) {
9040
+ throw malformedHostConfigError("update", target, "Topolo could not edit it without changing other entries.");
9041
+ }
9042
+ return text3;
9043
+ }
9044
+ function backupTimestamp(now = /* @__PURE__ */ new Date()) {
9045
+ return now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
9046
+ }
9047
+ function hostJsonBackupLocation(homeDir, host, configPath2) {
9048
+ const hash = createHash5("sha256").update(configPath2).digest("hex").slice(0, 12);
9049
+ return {
9050
+ directory: join7(homeDir, ".topolo", "backups", host),
9051
+ prefix: basename3(configPath2) + "." + hash + ".backup-"
9052
+ };
9053
+ }
9054
+ var HOST_JSON_BACKUP_MAX_SEQUENCE = 999999999;
9055
+ var HOST_JSON_BACKUP_MIN_AGE_MS = 3e4;
9056
+ function backupSequence(name, prefix) {
9057
+ const match = name.startsWith(prefix) ? /^(\d{6,9})-\d{8}T\d{6}Z$/.exec(name.slice(prefix.length)) : null;
9058
+ return match ? Number(match[1]) : void 0;
9059
+ }
9060
+ function privateFolderError() {
9061
+ return Object.assign(new Error("not a private directory"), { code: "ENOTPRIVATE" });
9062
+ }
9063
+ async function privateBackupFolder(homeDir, directory) {
9064
+ const levels = [
9065
+ [join7(homeDir, ".topolo"), 18],
9066
+ [join7(homeDir, ".topolo", "backups"), 63],
9067
+ [directory, 63]
9068
+ ];
9069
+ await mkdir4(homeDir, { recursive: true, mode: 448 });
9070
+ for (const [level, loose] of levels) {
9071
+ try {
9072
+ await mkdir4(level, { mode: 448 });
9073
+ } catch (error) {
9074
+ if (error.code !== "EEXIST") throw error;
9075
+ }
9076
+ const folder = await lstat3(level);
9077
+ if (!folder.isDirectory() || folder.uid !== (process.getuid?.() ?? folder.uid)) throw privateFolderError();
9078
+ if (folder.mode & loose) await chmod4(level, folder.mode & 511 & ~loose);
9079
+ }
9080
+ }
9081
+ async function removeOwnedBackup(file) {
9082
+ try {
9083
+ const info = await lstat3(file);
9084
+ if (info.isFile() && Date.now() - info.mtimeMs >= HOST_JSON_BACKUP_MIN_AGE_MS) await unlink(file);
9085
+ } catch (error) {
9086
+ if (error.code !== "ENOENT") throw error;
9087
+ }
9088
+ }
9089
+ async function writeHostJsonBackup(target, contents, homeDir, host, now = /* @__PURE__ */ new Date()) {
9090
+ const { directory, prefix } = hostJsonBackupLocation(homeDir, host, target.path);
9091
+ try {
9092
+ await privateBackupFolder(homeDir, directory);
9093
+ const sequences = () => readdir4(directory).then((names) => names.map((name) => ({ name, sequence: backupSequence(name, prefix) })).filter((item) => item.sequence !== void 0));
9094
+ let sequence = Math.max(0, ...(await sequences()).map((item) => item.sequence)) + 1;
9095
+ let written;
9096
+ for (let attempt = 1; !written; attempt += 1) {
9097
+ if (sequence > HOST_JSON_BACKUP_MAX_SEQUENCE) {
9098
+ throw new Error("backup numbers are used up; remove the old backups in that folder");
9099
+ }
9100
+ const candidate = join7(directory, prefix + String(sequence).padStart(6, "0") + "-" + backupTimestamp(now));
9101
+ try {
9102
+ const handle = await open2(candidate, "wx", 384);
9103
+ try {
9104
+ await handle.writeFile(contents, "utf8");
9105
+ } finally {
9106
+ await handle.close();
9107
+ }
9108
+ written = candidate;
9109
+ } catch (error) {
9110
+ if (error.code !== "EEXIST" || attempt >= 100) throw error;
9111
+ sequence += 1;
9112
+ }
9113
+ }
9114
+ const older = (await sequences()).sort((left, right) => left.sequence - right.sequence);
9115
+ older.splice(Math.max(0, older.length - HOST_JSON_BACKUP_RETENTION));
9116
+ for (const item of older) {
9117
+ if (join7(directory, item.name) !== written) await removeOwnedBackup(join7(directory, item.name));
9118
+ }
9119
+ return written;
9120
+ } catch (error) {
9121
+ const code = error.code;
9122
+ throw new Error(
9123
+ "Cannot back up " + target.label + " configuration at " + printable(target.path) + " before editing it: the private backup folder " + printable(directory) + " could not be used (" + (code ?? printable(error.message)) + "). Topolo did not change this file. Make sure that folder and its parent folders are directories you own (not links), or move the configuration aside, and run the command again."
9124
+ );
9125
+ }
9126
+ }
9127
+
9128
+ // src/commands/agent-host.ts
8817
9129
  var SUPPORTED_AGENT_IDS = [
8818
9130
  "claude",
8819
9131
  "claude-desktop",
@@ -8862,7 +9174,7 @@ function resolveAgentHostTarget(options) {
8862
9174
  return Object.freeze({
8863
9175
  homeDir,
8864
9176
  ...options.projectDir ? { projectDir: resolve3(options.projectDir) } : {},
8865
- codexHome: codexHome ?? join7(homeDir, ".codex")
9177
+ codexHome: codexHome ?? join8(homeDir, ".codex")
8866
9178
  });
8867
9179
  }
8868
9180
  var RESERVED_OPERATOR_REGISTRATION = ["topolo", "platform"].join("-");
@@ -8883,23 +9195,23 @@ function stableNodeCommand() {
8883
9195
  function agentSkillPath(agent, homeDir = homedir4(), projectDir, codexHome) {
8884
9196
  switch (agent) {
8885
9197
  case "claude":
8886
- return projectDir ? join7(projectDir, ".claude", "skills", "topolo", "SKILL.md") : join7(homeDir, ".claude", "skills", "topolo", "SKILL.md");
9198
+ return projectDir ? join8(projectDir, ".claude", "skills", "topolo", "SKILL.md") : join8(homeDir, ".claude", "skills", "topolo", "SKILL.md");
8887
9199
  case "claude-desktop":
8888
- return join7(homeDir, ".claude", "skills", "topolo", "SKILL.md");
9200
+ return join8(homeDir, ".claude", "skills", "topolo", "SKILL.md");
8889
9201
  case "codex":
8890
- return join7(codexHome ?? join7(homeDir, ".codex"), "skills", "topolo", "SKILL.md");
9202
+ return join8(codexHome ?? join8(homeDir, ".codex"), "skills", "topolo", "SKILL.md");
8891
9203
  case "cursor":
8892
- return join7(projectDir ?? homeDir, ".cursor", "rules", "topolo.mdc");
9204
+ return join8(projectDir ?? homeDir, ".cursor", "rules", "topolo.mdc");
8893
9205
  case "grok":
8894
- return join7(projectDir ?? homeDir, ".grok", "skills", "topolo", "SKILL.md");
9206
+ return join8(projectDir ?? homeDir, ".grok", "skills", "topolo", "SKILL.md");
8895
9207
  case "gemini":
8896
- return join7(projectDir ?? homeDir, ".gemini", "skills", "topolo", "SKILL.md");
9208
+ return join8(projectDir ?? homeDir, ".gemini", "skills", "topolo", "SKILL.md");
8897
9209
  case "copilot":
8898
- return projectDir ? join7(projectDir, ".github", "skills", "topolo", "SKILL.md") : join7(homeDir, ".copilot", "skills", "topolo", "SKILL.md");
9210
+ return projectDir ? join8(projectDir, ".github", "skills", "topolo", "SKILL.md") : join8(homeDir, ".copilot", "skills", "topolo", "SKILL.md");
8899
9211
  case "windsurf":
8900
- return projectDir ? join7(projectDir, ".windsurf", "rules", "topolo.md") : void 0;
9212
+ return projectDir ? join8(projectDir, ".windsurf", "rules", "topolo.md") : void 0;
8901
9213
  case "cline":
8902
- return join7(projectDir ?? homeDir, ".cline", "skills", "topolo", "SKILL.md");
9214
+ return join8(projectDir ?? homeDir, ".cline", "skills", "topolo", "SKILL.md");
8903
9215
  case "goose":
8904
9216
  case "amazon-q":
8905
9217
  return void 0;
@@ -8908,7 +9220,7 @@ function agentSkillPath(agent, homeDir = homedir4(), projectDir, codexHome) {
8908
9220
  function agentConfigPaths(agent, homeDir = homedir4(), projectDir, codexHome) {
8909
9221
  switch (agent) {
8910
9222
  case "claude":
8911
- return [{ path: projectDir ? join7(projectDir, ".mcp.json") : join7(homeDir, ".claude.json"), format: "json", rootKey: "mcpServers" }];
9223
+ return [{ path: projectDir ? join8(projectDir, ".mcp.json") : join8(homeDir, ".claude.json"), format: "json", rootKey: "mcpServers" }];
8912
9224
  case "claude-desktop":
8913
9225
  return [{
8914
9226
  path: claudeDesktopConfigPath(homeDir),
@@ -8916,56 +9228,56 @@ function agentConfigPaths(agent, homeDir = homedir4(), projectDir, codexHome) {
8916
9228
  rootKey: "mcpServers"
8917
9229
  }];
8918
9230
  case "codex":
8919
- return [{ path: join7(codexHome ?? join7(homeDir, ".codex"), "config.toml"), format: "toml" }];
9231
+ return [{ path: join8(codexHome ?? join8(homeDir, ".codex"), "config.toml"), format: "toml" }];
8920
9232
  case "cursor":
8921
9233
  return [{
8922
- path: projectDir ? join7(projectDir, ".cursor", "mcp.json") : join7(homeDir, ".cursor", "mcp.json"),
9234
+ path: projectDir ? join8(projectDir, ".cursor", "mcp.json") : join8(homeDir, ".cursor", "mcp.json"),
8923
9235
  format: "json",
8924
9236
  rootKey: "mcpServers"
8925
9237
  }];
8926
9238
  case "grok":
8927
9239
  return [{
8928
- path: projectDir ? join7(projectDir, ".grok", "config.toml") : join7(homeDir, ".grok", "config.toml"),
9240
+ path: projectDir ? join8(projectDir, ".grok", "config.toml") : join8(homeDir, ".grok", "config.toml"),
8929
9241
  format: "toml"
8930
9242
  }];
8931
9243
  case "gemini":
8932
9244
  return [{
8933
- path: projectDir ? join7(projectDir, ".gemini", "settings.json") : join7(homeDir, ".gemini", "settings.json"),
9245
+ path: projectDir ? join8(projectDir, ".gemini", "settings.json") : join8(homeDir, ".gemini", "settings.json"),
8934
9246
  format: "json",
8935
9247
  rootKey: "mcpServers"
8936
9248
  }];
8937
9249
  case "copilot":
8938
9250
  return projectDir ? [
8939
- { path: join7(projectDir, ".mcp.json"), format: "json", rootKey: "mcpServers" },
8940
- { path: join7(projectDir, ".vscode", "mcp.json"), format: "json", rootKey: "servers" }
9251
+ { path: join8(projectDir, ".mcp.json"), format: "json", rootKey: "mcpServers" },
9252
+ { path: join8(projectDir, ".vscode", "mcp.json"), format: "json", rootKey: "servers", syntax: "jsonc" }
8941
9253
  ] : [
8942
9254
  {
8943
- path: join7(homeDir, ".copilot", "mcp-config.json"),
9255
+ path: join8(homeDir, ".copilot", "mcp-config.json"),
8944
9256
  format: "json",
8945
9257
  rootKey: "mcpServers"
8946
9258
  },
8947
- { path: vscodeUserConfigPath(homeDir), format: "json", rootKey: "servers" }
9259
+ { path: vscodeUserConfigPath(homeDir), format: "json", rootKey: "servers", syntax: "jsonc" }
8948
9260
  ];
8949
9261
  case "windsurf":
8950
9262
  return [{
8951
- path: join7(homeDir, ".codeium", "windsurf", "mcp_config.json"),
9263
+ path: join8(homeDir, ".codeium", "windsurf", "mcp_config.json"),
8952
9264
  format: "json",
8953
9265
  rootKey: "mcpServers"
8954
9266
  }];
8955
9267
  case "cline":
8956
9268
  return [{
8957
- path: projectDir ? join7(projectDir, ".cline", "mcp.json") : join7(homeDir, ".cline", "mcp.json"),
9269
+ path: projectDir ? join8(projectDir, ".cline", "mcp.json") : join8(homeDir, ".cline", "mcp.json"),
8958
9270
  format: "json",
8959
9271
  rootKey: "mcpServers"
8960
9272
  }];
8961
9273
  case "goose":
8962
- return [{ path: join7(homeDir, ".config", "goose", "config.yaml"), format: "yaml" }];
9274
+ return [{ path: join8(homeDir, ".config", "goose", "config.yaml"), format: "yaml" }];
8963
9275
  case "amazon-q":
8964
9276
  if (projectDir) {
8965
- return [{ path: join7(projectDir, ".amazonq", "mcp.json"), format: "json", rootKey: "mcpServers" }];
9277
+ return [{ path: join8(projectDir, ".amazonq", "mcp.json"), format: "json", rootKey: "mcpServers" }];
8966
9278
  }
8967
9279
  return [{
8968
- path: existsSync4(join7(homeDir, ".aws", "amazonq", "mcp.json")) ? join7(homeDir, ".aws", "amazonq", "mcp.json") : join7(homeDir, ".aws", "amazonq", "default.json"),
9280
+ path: existsSync4(join8(homeDir, ".aws", "amazonq", "mcp.json")) ? join8(homeDir, ".aws", "amazonq", "mcp.json") : join8(homeDir, ".aws", "amazonq", "default.json"),
8969
9281
  format: "json",
8970
9282
  rootKey: "mcpServers"
8971
9283
  }];
@@ -9001,7 +9313,7 @@ function agentConfigExists(agent, homeDir = homedir4(), projectDir, codexHome) {
9001
9313
  return agentConfigPaths(agent, homeDir, projectDir, codexHome).some((config) => existsSync4(config.path));
9002
9314
  }
9003
9315
  async function assertClaudeProjectScope(homeDir, projectDir, name) {
9004
- const path = join7(homeDir, ".claude.json");
9316
+ const path = join8(homeDir, ".claude.json");
9005
9317
  let contents;
9006
9318
  try {
9007
9319
  contents = await readFile4(path, "utf8");
@@ -9011,10 +9323,9 @@ async function assertClaudeProjectScope(homeDir, projectDir, name) {
9011
9323
  }
9012
9324
  let config;
9013
9325
  try {
9014
- config = JSON.parse(contents);
9015
- if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error();
9326
+ config = parseHostJson(contents, { path, label: "Claude", syntax: "json" }).value;
9016
9327
  } catch {
9017
- throw new Error("Cannot verify malformed Claude scope configuration at " + path + ".");
9328
+ throw new Error("Cannot verify malformed Claude scope configuration at " + path + ". Fix it, or move it aside, and run the command again.");
9018
9329
  }
9019
9330
  const object = (value) => {
9020
9331
  if (value === void 0) return void 0;
@@ -9029,21 +9340,21 @@ async function assertClaudeProjectScope(homeDir, projectDir, name) {
9029
9340
  }
9030
9341
  }
9031
9342
  function claudeDesktopConfigPath(homeDir) {
9032
- if (process.platform === "darwin") return join7(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json");
9033
- if (process.platform === "win32") return join7(process.env.APPDATA ?? join7(homeDir, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
9034
- return join7(process.env.XDG_CONFIG_HOME ?? join7(homeDir, ".config"), "Claude", "claude_desktop_config.json");
9343
+ if (process.platform === "darwin") return join8(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json");
9344
+ if (process.platform === "win32") return join8(process.env.APPDATA ?? join8(homeDir, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
9345
+ return join8(process.env.XDG_CONFIG_HOME ?? join8(homeDir, ".config"), "Claude", "claude_desktop_config.json");
9035
9346
  }
9036
9347
  function vscodeUserConfigPath(homeDir) {
9037
- if (process.platform === "darwin") return join7(homeDir, "Library", "Application Support", "Code", "User", "mcp.json");
9038
- if (process.platform === "win32") return join7(process.env.APPDATA ?? join7(homeDir, "AppData", "Roaming"), "Code", "User", "mcp.json");
9039
- return join7(process.env.XDG_CONFIG_HOME ?? join7(homeDir, ".config"), "Code", "User", "mcp.json");
9348
+ if (process.platform === "darwin") return join8(homeDir, "Library", "Application Support", "Code", "User", "mcp.json");
9349
+ if (process.platform === "win32") return join8(process.env.APPDATA ?? join8(homeDir, "AppData", "Roaming"), "Code", "User", "mcp.json");
9350
+ return join8(process.env.XDG_CONFIG_HOME ?? join8(homeDir, ".config"), "Code", "User", "mcp.json");
9040
9351
  }
9041
9352
  async function readAgentRegistrations(agent, homeDir = homedir4(), projectDir, codexHome) {
9042
9353
  const registrations = [];
9043
9354
  for (const config of agentConfigPaths(agent, homeDir, projectDir, codexHome)) {
9044
9355
  if (!existsSync4(config.path)) continue;
9045
9356
  if (config.format === "json") {
9046
- registrations.push(...await readJsonRegistrations(config.path, config.rootKey ?? "mcpServers", agent));
9357
+ registrations.push(...await readJsonRegistrations(config, agent));
9047
9358
  } else if (config.format === "toml") {
9048
9359
  registrations.push(...await readTomlRegistrations(config.path, agent));
9049
9360
  } else {
@@ -9052,14 +9363,12 @@ async function readAgentRegistrations(agent, homeDir = homedir4(), projectDir, c
9052
9363
  }
9053
9364
  return registrations.filter((registration, index, all) => all.findIndex((candidate) => candidate.name === registration.name) === index);
9054
9365
  }
9055
- async function readJsonRegistrations(path, rootKey, agent) {
9056
- let parsed;
9057
- try {
9058
- parsed = JSON.parse(await readFile4(path, "utf8"));
9059
- } catch (error) {
9060
- throw new Error("Cannot read malformed " + (agent === "claude" ? "Claude" : AGENT_LABELS[agent]) + " configuration at " + path + ": " + message(error));
9061
- }
9062
- const servers = parsed[rootKey];
9366
+ function hostConfigLabel(agent) {
9367
+ return agent === "claude" ? "Claude" : AGENT_LABELS[agent];
9368
+ }
9369
+ async function readJsonRegistrations(config, agent) {
9370
+ const target = { path: config.path, label: hostConfigLabel(agent), syntax: config.syntax ?? "json" };
9371
+ const servers = parseHostJson(await readFile4(config.path, "utf8"), target, "read").value[config.rootKey ?? "mcpServers"];
9063
9372
  if (!servers || typeof servers !== "object" || Array.isArray(servers)) return [];
9064
9373
  return Object.entries(servers).filter(([name]) => isPublicRegistrationName(name)).flatMap(([name, value]) => normalizeRegistration(name, value));
9065
9374
  }
@@ -9310,39 +9619,62 @@ function normalizeRegistration(name, value) {
9310
9619
  const env = candidate.env && typeof candidate.env === "object" && !Array.isArray(candidate.env) ? stringEntries(candidate.env) : {};
9311
9620
  return [{ name, command: candidate.command, args, env }];
9312
9621
  }
9313
- function message(error) {
9314
- return error instanceof Error ? error.message : String(error);
9315
- }
9316
9622
 
9317
9623
  // src/commands/agent-config.ts
9318
9624
  import { existsSync as existsSync5 } from "fs";
9319
- import { lstat as lstat3, mkdir as mkdir4, open as open2, readFile as readFile5, rename as rename3, unlink, writeFile as writeFile2 } from "fs/promises";
9625
+ import { lstat as lstat4, mkdir as mkdir5, open as open3, readFile as readFile5, rename as rename3, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
9320
9626
  import { randomUUID as randomUUID4 } from "crypto";
9321
9627
  import { dirname as dirname5 } from "path";
9322
9628
  async function installAgentSkill(agent, source, homeDir, projectDir, codexHome) {
9323
9629
  const path = agentSkillPath(agent, homeDir, projectDir, codexHome);
9324
9630
  if (!path) return { installed: false };
9325
9631
  const contents = formatSkillForAgent(agent, source);
9326
- await atomicWrite(path, contents);
9327
- return { path, installed: true };
9632
+ return { path, installed: true, changed: await atomicWrite(path, contents) };
9328
9633
  }
9329
9634
  async function registerAgentConfig(agent, mcp, homeDir, registration, projectDir, codexHome) {
9330
9635
  if (agent === "claude" && projectDir) await assertClaudeProjectScope(homeDir, projectDir, registration.name);
9331
- for (const config of agentConfigPaths(agent, homeDir, projectDir, codexHome)) {
9332
- if (config.format === "json") {
9333
- await registerJson(config, agent, mcp, registration);
9334
- } else if (config.format === "toml") {
9335
- await registerToml(config.path, agent, mcp, registration);
9336
- } else {
9337
- await registerGoose(config.path, mcp, registration);
9636
+ return editHostConfigs(agentConfigPaths(agent, homeDir, projectDir, codexHome), async (config) => {
9637
+ if (config.format === "json") return registerJson(config, agent, mcp, registration, homeDir);
9638
+ if (config.format === "toml") return { changed: await registerToml(config.path, agent, mcp, registration) };
9639
+ return { changed: await registerGoose(config.path, mcp, registration) };
9640
+ });
9641
+ }
9642
+ var HostConfigPartialError = class extends Error {
9643
+ constructor(reason, changed, backups) {
9644
+ super(
9645
+ reason.message + (changed.length ? " Before this, the same run had already changed: " + changed.map(printable).join(", ") + "." : "") + (backups.length ? " Backups written: " + backups.map(printable).join(", ") + "." : "")
9646
+ );
9647
+ this.reason = reason;
9648
+ this.changed = changed;
9649
+ this.backups = backups;
9650
+ }
9651
+ reason;
9652
+ changed;
9653
+ backups;
9654
+ };
9655
+ async function editHostConfigs(configs, edit) {
9656
+ const changed = [];
9657
+ const backups = [];
9658
+ for (const config of configs) {
9659
+ let result;
9660
+ try {
9661
+ result = await edit(config);
9662
+ } catch (error) {
9663
+ const partial = error instanceof HostConfigPartialError ? error : void 0;
9664
+ const written = [...backups, ...partial?.backups ?? []];
9665
+ if (!changed.length && !written.length) throw error;
9666
+ throw new HostConfigPartialError(partial?.reason ?? error, changed, written);
9338
9667
  }
9668
+ if (result.backup) backups.push(result.backup);
9669
+ if (result.changed) changed.push(config.path);
9339
9670
  }
9671
+ return backups;
9340
9672
  }
9341
9673
  var SHARED_HUB_AGENTS = ["claude", "codex"];
9342
9674
  async function registerAgentHttpConfig(agent, homeDir, registration, codexHome) {
9343
9675
  const [config] = agentConfigPaths(agent, homeDir, void 0, codexHome);
9344
9676
  if (agent === "claude") {
9345
- await writeJsonServer(config, agent, registration.name, {
9677
+ await writeJsonServer(config, agent, registration.name, homeDir, {
9346
9678
  type: "http",
9347
9679
  url: registration.url,
9348
9680
  headers: registration.headers
@@ -9356,17 +9688,15 @@ async function registerAgentHttpConfig(agent, homeDir, registration, codexHome)
9356
9688
  await atomicWrite(config.path, text3 + block, before);
9357
9689
  }
9358
9690
  async function unregisterAgentConfig(agent, homeDir, name, projectDir, codexHome) {
9359
- for (const config of agentConfigPaths(agent, homeDir, projectDir, codexHome)) {
9360
- if (!existsSync5(config.path)) continue;
9361
- if (config.format === "json") {
9362
- await unregisterJson(config, agent, name);
9363
- } else if (config.format === "toml") {
9691
+ const configs = agentConfigPaths(agent, homeDir, projectDir, codexHome).filter((config) => existsSync5(config.path));
9692
+ return editHostConfigs(configs, async (config) => {
9693
+ if (config.format === "json") return editJsonServer(config, agent, name, void 0, homeDir);
9694
+ if (config.format === "toml") {
9364
9695
  const text3 = withoutMcpServerSections(await readFile5(config.path, "utf8"), name);
9365
- await atomicWrite(config.path, text3 ? text3 + "\n" : "");
9366
- } else {
9367
- await unregisterGoose(config.path, name);
9696
+ return { changed: await atomicWrite(config.path, text3 ? text3 + "\n" : "") };
9368
9697
  }
9369
- }
9698
+ return { changed: await unregisterGoose(config.path, name) };
9699
+ });
9370
9700
  }
9371
9701
  function registrationEnvironment(agent, registration) {
9372
9702
  let privateAliases = {};
@@ -9398,56 +9728,36 @@ function registrationEnvironment(agent, registration) {
9398
9728
  ...privateAliases
9399
9729
  };
9400
9730
  }
9401
- async function registerJson(config, agent, mcp, registration) {
9731
+ async function registerJson(config, agent, mcp, registration, homeDir) {
9402
9732
  const entry = {
9403
9733
  command: mcp.command,
9404
9734
  args: mcp.args,
9405
9735
  env: registrationEnvironment(agent, registration)
9406
9736
  };
9407
9737
  if (agent === "claude" || agent === "claude-desktop" || (config.rootKey ?? "mcpServers") === "servers") entry.type = "stdio";
9408
- await writeJsonServer(config, agent, registration.name, entry);
9738
+ return writeJsonServer(config, agent, registration.name, homeDir, entry);
9409
9739
  }
9410
- async function writeJsonServer(config, agent, name, entry) {
9411
- const before = await fileSnapshot(config.path);
9412
- let parsed = {};
9413
- if (before.contents !== void 0) {
9414
- try {
9415
- parsed = JSON.parse(before.contents);
9416
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Expected an object.");
9417
- } catch (error) {
9418
- throw new Error(
9419
- "Cannot update malformed " + (agent === "claude" ? "Claude" : AGENT_LABELS[agent]) + " configuration at " + config.path + ": " + (error instanceof Error ? error.message : String(error))
9420
- );
9421
- }
9422
- }
9423
- const rootKey = config.rootKey ?? "mcpServers";
9424
- const existingServers = parsed[rootKey];
9425
- if (existingServers !== void 0 && (!existingServers || typeof existingServers !== "object" || Array.isArray(existingServers))) {
9426
- throw new Error("Cannot update malformed " + AGENT_LABELS[agent] + " MCP configuration at " + config.path + ".");
9427
- }
9428
- const servers = existingServers ?? {};
9429
- servers[name] = entry;
9430
- parsed[rootKey] = servers;
9431
- await atomicWrite(config.path, JSON.stringify(parsed, null, 2) + "\n", before);
9740
+ async function writeJsonServer(config, agent, name, homeDir, entry) {
9741
+ return editJsonServer(config, agent, name, entry, homeDir);
9432
9742
  }
9433
- async function unregisterJson(config, agent, name) {
9434
- let parsed;
9743
+ async function editJsonServer(config, agent, name, entry, homeDir) {
9744
+ const target = { path: config.path, label: hostConfigLabel(agent), syntax: config.syntax ?? "json" };
9745
+ const rootKey = config.rootKey ?? "mcpServers";
9746
+ const before = await fileSnapshot(config.path);
9747
+ const document = parseHostJson(before.contents, target);
9748
+ if (entry === void 0) {
9749
+ const servers = document.value[rootKey];
9750
+ if (!servers || typeof servers !== "object" || Array.isArray(servers) || !Object.hasOwn(servers, name)) return { changed: false };
9751
+ }
9752
+ const contents = editHostServer(document, rootKey, name, entry, target);
9753
+ if (contents === before.contents) return { changed: false };
9754
+ const backup = editsInPlace(document, target) ? await writeHostJsonBackup(target, before.contents, homeDir, agent) : void 0;
9435
9755
  try {
9436
- parsed = JSON.parse(await readFile5(config.path, "utf8"));
9756
+ return { changed: await atomicWrite(config.path, contents, before), ...backup ? { backup } : {} };
9437
9757
  } catch (error) {
9438
- throw new Error(
9439
- "Cannot update malformed " + AGENT_LABELS[agent] + " configuration at " + config.path + ": " + (error instanceof Error ? error.message : String(error))
9440
- );
9441
- }
9442
- const rootKey = config.rootKey ?? "mcpServers";
9443
- const existingServers = parsed[rootKey];
9444
- if (existingServers !== void 0 && (!existingServers || typeof existingServers !== "object" || Array.isArray(existingServers))) {
9445
- throw new Error("Cannot update malformed " + AGENT_LABELS[agent] + " MCP configuration at " + config.path + ".");
9758
+ if (backup) throw new HostConfigPartialError(error, [], [backup]);
9759
+ throw error;
9446
9760
  }
9447
- const servers = existingServers ?? {};
9448
- delete servers[name];
9449
- parsed[rootKey] = servers;
9450
- await atomicWrite(config.path, JSON.stringify(parsed, null, 2) + "\n");
9451
9761
  }
9452
9762
  async function registerToml(path, agent, mcp, registration) {
9453
9763
  const before = await fileSnapshot(path);
@@ -9457,7 +9767,7 @@ async function registerToml(path, agent, mcp, registration) {
9457
9767
  const environment = registrationEnvironment(agent, registration);
9458
9768
  const environmentText = Object.entries(environment).map(([key, value]) => JSON.stringify(key) + " = " + JSON.stringify(value)).join(", ");
9459
9769
  const block = agent === "grok" ? "\n\n[mcp_servers." + registration.name + "]\ncommand = " + JSON.stringify(mcp.command) + args + "\nenv = { " + environmentText + " }\n" : "\n\n[mcp_servers." + registration.name + "]\ncommand = " + JSON.stringify(mcp.command) + args + "\n\n[mcp_servers." + registration.name + ".env]\n" + Object.entries(environment).map(([key, value]) => key + " = " + JSON.stringify(value)).join("\n") + "\n";
9460
- await atomicWrite(path, text3 + block, before);
9770
+ return atomicWrite(path, text3 + block, before);
9461
9771
  }
9462
9772
  async function registerGoose(path, mcp, registration) {
9463
9773
  const existing = existsSync5(path) ? await readFile5(path, "utf8") : "";
@@ -9474,11 +9784,11 @@ async function registerGoose(path, mcp, registration) {
9474
9784
  " envs:",
9475
9785
  ...Object.entries(environment).map(([key, value]) => " " + key + ": " + yamlScalar(value))
9476
9786
  ].join("\n");
9477
- await atomicWrite(path, insertGooseExtension(withoutTopolo, block));
9787
+ return atomicWrite(path, insertGooseExtension(withoutTopolo, block));
9478
9788
  }
9479
9789
  async function unregisterGoose(path, name) {
9480
9790
  const existing = await readFile5(path, "utf8");
9481
- await atomicWrite(path, removeGooseExtension(existing, name));
9791
+ return atomicWrite(path, removeGooseExtension(existing, name));
9482
9792
  }
9483
9793
  function removeGooseExtension(text3, name) {
9484
9794
  const lines = text3.split(/\r?\n/);
@@ -9534,7 +9844,7 @@ async function fileSnapshot(path) {
9534
9844
  const parents = [];
9535
9845
  for (let parent = dirname5(path); ; parent = dirname5(parent)) {
9536
9846
  try {
9537
- const stat3 = await lstat3(parent);
9847
+ const stat3 = await lstat4(parent);
9538
9848
  if (!stat3.isDirectory() || stat3.isSymbolicLink()) throw new Error("Host destination contains a symbolic link or non-directory at " + parent + ".");
9539
9849
  parents.push({ path: parent, identity: [stat3.dev, stat3.ino].join(":") });
9540
9850
  } catch (error) {
@@ -9543,10 +9853,10 @@ async function fileSnapshot(path) {
9543
9853
  if (dirname5(parent) === parent) break;
9544
9854
  }
9545
9855
  try {
9546
- const stat3 = await lstat3(path);
9856
+ const stat3 = await lstat4(path);
9547
9857
  if (!stat3.isFile() || stat3.isSymbolicLink() || stat3.nlink !== 1) throw new Error("Host configuration must be a single regular file at " + path + ".");
9548
9858
  const contents = await readFile5(path, "utf8");
9549
- if (fileIdentity(await lstat3(path)) !== fileIdentity(stat3)) throw new Error("Host configuration changed while reading at " + path + ".");
9859
+ if (fileIdentity(await lstat4(path)) !== fileIdentity(stat3)) throw new Error("Host configuration changed while reading at " + path + ".");
9550
9860
  return { contents, identity: fileIdentity(stat3), parents };
9551
9861
  } catch (error) {
9552
9862
  if (error.code === "ENOENT") return { parents };
@@ -9562,12 +9872,12 @@ async function assertSnapshot(path, before) {
9562
9872
  async function atomicWrite(path, contents, before) {
9563
9873
  before ??= await fileSnapshot(path);
9564
9874
  await assertSnapshot(path, before);
9565
- if (before.contents === contents) return;
9566
- await mkdir4(dirname5(path), { recursive: true });
9875
+ if (before.contents === contents) return false;
9876
+ await mkdir5(dirname5(path), { recursive: true });
9567
9877
  const destination = await fileSnapshot(path);
9568
9878
  await assertSnapshot(path, before);
9569
9879
  const temporaryPath = path + ".topolo-" + randomUUID4() + ".tmp";
9570
- const handle = await open2(temporaryPath, "wx", 384);
9880
+ const handle = await open3(temporaryPath, "wx", 384);
9571
9881
  let temporaryIdentity;
9572
9882
  let handleClosed = false;
9573
9883
  let temporaryPending = true;
@@ -9579,7 +9889,7 @@ async function atomicWrite(path, contents, before) {
9579
9889
  await handle.close();
9580
9890
  handleClosed = true;
9581
9891
  await assertSnapshot(path, destination);
9582
- const temporary = await lstat3(temporaryPath);
9892
+ const temporary = await lstat4(temporaryPath);
9583
9893
  if (!temporary.isFile() || temporary.isSymbolicLink() || temporary.nlink !== 1 || [temporary.dev, temporary.ino].join(":") !== temporaryIdentity) {
9584
9894
  throw new Error("Host temporary file custody changed before rename at " + path + ".");
9585
9895
  }
@@ -9587,20 +9897,21 @@ async function atomicWrite(path, contents, before) {
9587
9897
  temporaryPending = false;
9588
9898
  const installed = await fileSnapshot(path);
9589
9899
  if (installed.contents !== contents) throw new Error("Host configuration read-back failed at " + path + ".");
9900
+ return true;
9590
9901
  } finally {
9591
9902
  if (!handleClosed) await handle.close();
9592
9903
  if (temporaryPending && temporaryIdentity) {
9593
9904
  let cleanupOwned = false;
9594
9905
  try {
9595
9906
  const parentsOwned = await Promise.all(destination.parents.map(async (parent) => {
9596
- const current2 = await lstat3(parent.path);
9907
+ const current2 = await lstat4(parent.path);
9597
9908
  return current2.isDirectory() && !current2.isSymbolicLink() && [current2.dev, current2.ino].join(":") === parent.identity;
9598
9909
  }));
9599
- const current = await lstat3(temporaryPath);
9910
+ const current = await lstat4(temporaryPath);
9600
9911
  cleanupOwned = parentsOwned.every(Boolean) && current.isFile() && !current.isSymbolicLink() && current.nlink === 1 && [current.dev, current.ino].join(":") === temporaryIdentity;
9601
9912
  } catch {
9602
9913
  }
9603
- if (cleanupOwned) await unlink(temporaryPath);
9914
+ if (cleanupOwned) await unlink2(temporaryPath);
9604
9915
  }
9605
9916
  }
9606
9917
  }
@@ -9622,12 +9933,12 @@ function withoutMcpServerSections(text3, name) {
9622
9933
  // src/commands/mcp-hub-setup.ts
9623
9934
  import { realpathSync } from "fs";
9624
9935
  import { readFile as readFile8 } from "fs/promises";
9625
- import { join as join10 } from "path";
9936
+ import { join as join11 } from "path";
9626
9937
 
9627
9938
  // src/lib/mcp-hub-config.ts
9628
9939
  import { randomBytes } from "crypto";
9629
- import { lstat as lstat4, mkdir as mkdir5, open as open3, readFile as readFile6, rename as rename4, unlink as unlink2 } from "fs/promises";
9630
- import { dirname as dirname6, join as join8 } from "path";
9940
+ import { lstat as lstat5, mkdir as mkdir6, open as open4, readFile as readFile6, rename as rename4, unlink as unlink3 } from "fs/promises";
9941
+ import { dirname as dirname6, join as join9 } from "path";
9631
9942
  var MCP_HUB_DEFAULT_PORT = 7337;
9632
9943
  var MCP_HUB_ENVIRONMENTS = ["production", "staging", "development", "local"];
9633
9944
  var RESERVED_NAME = ["topolo", "platform"].join("-");
@@ -9635,7 +9946,7 @@ var NAME_PATTERN = /^topolo(-[a-z0-9][a-z0-9_-]*)?$/;
9635
9946
  var TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
9636
9947
  function mcpHubPaths(credentialStorePath = configPath()) {
9637
9948
  const directory = dirname6(credentialStorePath);
9638
- return { config: join8(directory, "mcp-hub.json"), token: join8(directory, "mcp-hub.token") };
9949
+ return { config: join9(directory, "mcp-hub.json"), token: join9(directory, "mcp-hub.token") };
9639
9950
  }
9640
9951
  function mcpHubUrl(port, name) {
9641
9952
  return `http://127.0.0.1:${port}/mcp/${name}`;
@@ -9732,9 +10043,9 @@ function upsertMcpHubIdentity(config, identity) {
9732
10043
  return { ...config, identities };
9733
10044
  }
9734
10045
  async function writePrivateFile(path, contents) {
9735
- await mkdir5(dirname6(path), { recursive: true, mode: 448 });
10046
+ await mkdir6(dirname6(path), { recursive: true, mode: 448 });
9736
10047
  const temporary = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
9737
- const handle = await open3(temporary, "wx", 384);
10048
+ const handle = await open4(temporary, "wx", 384);
9738
10049
  try {
9739
10050
  await handle.writeFile(contents, "utf8");
9740
10051
  } finally {
@@ -9743,7 +10054,7 @@ async function writePrivateFile(path, contents) {
9743
10054
  try {
9744
10055
  await rename4(temporary, path);
9745
10056
  } catch (error) {
9746
- await unlink2(temporary).catch(() => void 0);
10057
+ await unlink3(temporary).catch(() => void 0);
9747
10058
  throw error;
9748
10059
  }
9749
10060
  }
@@ -9751,7 +10062,7 @@ async function writeMcpHubConfig(path, config) {
9751
10062
  await writePrivateFile(path, serializeMcpHubConfig(config));
9752
10063
  }
9753
10064
  async function assertPrivateTokenFile(path) {
9754
- const stat3 = await lstat4(path);
10065
+ const stat3 = await lstat5(path);
9755
10066
  if (!stat3.isFile() || stat3.isSymbolicLink() || stat3.nlink !== 1) {
9756
10067
  throw new Error(`MCP hub token at ${path} must be a single regular file.`);
9757
10068
  }
@@ -9780,9 +10091,9 @@ async function ensureMcpHubToken(path) {
9780
10091
  } catch (error) {
9781
10092
  if (!/is missing at/.test(error.message)) throw error;
9782
10093
  }
9783
- await mkdir5(dirname6(path), { recursive: true, mode: 448 });
10094
+ await mkdir6(dirname6(path), { recursive: true, mode: 448 });
9784
10095
  const token = randomBytes(32).toString("base64url");
9785
- const handle = await open3(path, "wx", 384);
10096
+ const handle = await open4(path, "wx", 384);
9786
10097
  try {
9787
10098
  await handle.writeFile(token + "\n", "utf8");
9788
10099
  } finally {
@@ -9794,7 +10105,7 @@ async function ensureMcpHubToken(path) {
9794
10105
  // src/lib/mcp-hub-service.ts
9795
10106
  import { spawnSync as spawnSync4 } from "child_process";
9796
10107
  import { readFile as readFile7 } from "fs/promises";
9797
- import { join as join9 } from "path";
10108
+ import { join as join10 } from "path";
9798
10109
  var MCP_HUB_LAUNCHD_LABEL = "io.topolo.mcp-hub";
9799
10110
  var MCP_HUB_SYSTEMD_UNIT = "topolo-mcp-hub.service";
9800
10111
  var PASSTHROUGH_ENVIRONMENT = ["TOPOLO_CONFIG_PATH", "XDG_CONFIG_HOME"];
@@ -9808,7 +10119,7 @@ function serviceKindFor(platform4) {
9808
10119
  return void 0;
9809
10120
  }
9810
10121
  function mcpHubServicePath(kind, homeDir, xdgConfigHome) {
9811
- return kind === "launchd" ? join9(homeDir, "Library", "LaunchAgents", `${MCP_HUB_LAUNCHD_LABEL}.plist`) : join9(xdgConfigHome || join9(homeDir, ".config"), "systemd", "user", MCP_HUB_SYSTEMD_UNIT);
10122
+ return kind === "launchd" ? join10(homeDir, "Library", "LaunchAgents", `${MCP_HUB_LAUNCHD_LABEL}.plist`) : join10(xdgConfigHome || join10(homeDir, ".config"), "systemd", "user", MCP_HUB_SYSTEMD_UNIT);
9812
10123
  }
9813
10124
  function serviceEnvironment(environment) {
9814
10125
  const selected = {};
@@ -9903,7 +10214,7 @@ async function installMcpHubService(options) {
9903
10214
  };
9904
10215
  }
9905
10216
  const path = mcpHubServicePath(kind, options.homeDir, options.xdgConfigHome);
9906
- const spec = kind === "launchd" ? { ...options.spec, logPath: options.spec.logPath ?? join9(options.homeDir, "Library", "Logs", "topolo-mcp-hub.log") } : options.spec;
10217
+ const spec = kind === "launchd" ? { ...options.spec, logPath: options.spec.logPath ?? join10(options.homeDir, "Library", "Logs", "topolo-mcp-hub.log") } : options.spec;
9907
10218
  const contents = kind === "launchd" ? renderLaunchdPlist(spec) : renderSystemdUnit(spec);
9908
10219
  const changed = await readExisting(path) !== contents;
9909
10220
  if (changed) await writePrivateFile(path, contents);
@@ -10085,7 +10396,7 @@ async function installSharedAgentSurfaces(options, dependencies) {
10085
10396
  function defaultSharedSetupDependencies(skillsDir, realHome) {
10086
10397
  return {
10087
10398
  paths: () => mcpHubPaths(),
10088
- skillSource: () => readFile8(join10(skillsDir, "topolo", "SKILL.md"), "utf8"),
10399
+ skillSource: () => readFile8(join11(skillsDir, "topolo", "SKILL.md"), "utf8"),
10089
10400
  platform: process.platform,
10090
10401
  realHome,
10091
10402
  nodeCommand: stableNodeCommand,
@@ -10106,10 +10417,10 @@ function formatSharedSetup(result) {
10106
10417
  lines.push(` skill -> ${row.skillPath}`);
10107
10418
  lines.push(row.mcp === "config-file" ? ` HTTP MCP registered: ${row.registrations.join(", ")}` : ` HTTP MCP registration FAILED: ${row.detail}`);
10108
10419
  }
10109
- for (const entry of result.skipped) lines.push(`Skipped ${entry.name} (${AGENT_LABELS[entry.agent]}): ${entry.reason}`);
10420
+ for (const entry of result.skipped) lines.push(`Skipped ${printable(entry.name)} (${AGENT_LABELS[entry.agent]}): ${entry.reason}`);
10110
10421
  if (result.service) lines.push(`Service (${result.service.kind}): ${result.service.detail}`);
10111
10422
  lines.push("", "Next: restart the agents, then run `topolo doctor --shared`.");
10112
- return lines.join("\n");
10423
+ return printableLines(lines.join("\n"));
10113
10424
  }
10114
10425
 
10115
10426
  // src/commands/skills.ts
@@ -10119,7 +10430,7 @@ function skillsSourceDir() {
10119
10430
  const distCandidate = resolve4(here, "../skills");
10120
10431
  const srcCandidate = resolve4(here, "../../skills");
10121
10432
  for (const candidate of [distCandidate, srcCandidate]) {
10122
- if (existsSync6(join11(candidate, "topolo", "SKILL.md"))) return candidate;
10433
+ if (existsSync6(join12(candidate, "topolo", "SKILL.md"))) return candidate;
10123
10434
  }
10124
10435
  return distCandidate;
10125
10436
  }
@@ -10177,26 +10488,30 @@ async function installForAgent(agent, sourceDir, mcp, opts) {
10177
10488
  return { agent, skillInstalled: false, mcp: "failed", detail: err instanceof Error ? err.message : String(err) };
10178
10489
  }
10179
10490
  }
10180
- const source = await readFile9(join11(sourceDir, "topolo", "SKILL.md"), "utf8");
10491
+ const source = await readFile9(join12(sourceDir, "topolo", "SKILL.md"), "utf8");
10181
10492
  const skill = await installAgentSkill(agent, source, homeDir, projectDir, codexHome);
10182
10493
  if (!registration) {
10183
10494
  return { agent, skillPath: skill.path, skillInstalled: skill.installed, mcp: "skipped" };
10184
10495
  }
10496
+ let backups = [];
10185
10497
  try {
10186
- await registerAgentConfig(agent, mcp, homeDir, registration, projectDir, codexHome);
10498
+ backups = await registerAgentConfig(agent, mcp, homeDir, registration, projectDir, codexHome);
10187
10499
  const installed = (await readAgentRegistrations(agent, homeDir, projectDir, codexHome)).find((entry) => entry.name === registration.name);
10188
10500
  const expectedEnvironment = registrationEnvironment(agent, registration);
10189
10501
  if (!installed || installed.command !== mcp.command || JSON.stringify(installed.args) !== JSON.stringify(mcp.args) || Object.entries(expectedEnvironment).some(([key, value]) => installed.env[key] !== value)) {
10190
10502
  throw new Error(`Host configuration did not retain the ${registration.name} MCP registration.`);
10191
10503
  }
10192
- return { agent, skillPath: skill.path, skillInstalled: skill.installed, mcp: "config-file" };
10504
+ return { agent, skillPath: skill.path, skillInstalled: skill.installed, mcp: "config-file", ...backups.length ? { backups } : {} };
10193
10505
  } catch (err) {
10506
+ if (err instanceof HostConfigPartialError) backups = err.backups;
10507
+ const skillNote = skill.changed ? " This run also wrote the Topolo skill at " + printable(skill.path) + "." : "";
10194
10508
  return {
10195
10509
  agent,
10196
10510
  skillPath: skill.path,
10197
10511
  skillInstalled: skill.installed,
10198
10512
  mcp: "failed",
10199
- detail: err instanceof Error ? err.message : String(err)
10513
+ detail: (err instanceof Error ? err.message : String(err)) + skillNote,
10514
+ ...backups.length ? { backups } : {}
10200
10515
  };
10201
10516
  }
10202
10517
  }
@@ -10263,11 +10578,7 @@ function registerSkillsCommand(program2) {
10263
10578
  ...opts,
10264
10579
  homeDir: opts.agentHome ? resolve4(opts.agentHome) : void 0
10265
10580
  });
10266
- emit(
10267
- { removed },
10268
- mode,
10269
- () => removed.map((r) => `${AGENT_LABELS2[r.agent]}: ${r.name} removed=${r.registrationRemoved}, skill removed=${r.skillRemoved}`).join("\n")
10270
- );
10581
+ emit({ removed }, mode, () => formatUninstall(removed));
10271
10582
  } catch (err) {
10272
10583
  fail2(err, mode);
10273
10584
  }
@@ -10275,7 +10586,7 @@ function registerSkillsCommand(program2) {
10275
10586
  }
10276
10587
  async function installAgentSurfaces(opts) {
10277
10588
  const sourceDir = skillsSourceDir();
10278
- if (!existsSync6(join11(sourceDir, "topolo", "SKILL.md"))) {
10589
+ if (!existsSync6(join12(sourceDir, "topolo", "SKILL.md"))) {
10279
10590
  throw new Error("Bundled skill file not found. Reinstall the CLI: npm install -g @topolo/cli");
10280
10591
  }
10281
10592
  const mcp = resolveMcpCommand();
@@ -10325,19 +10636,19 @@ function registerSetupCommand(program2) {
10325
10636
  function assertSharedSetupSucceeded(result) {
10326
10637
  const failed = result.installed.filter((row) => row.mcp === "failed");
10327
10638
  if (failed.length) {
10328
- throw new Error(failed.map((row) => `${AGENT_LABELS2[row.agent]}: ${row.detail}`).join("; "));
10639
+ throw new Error(printableLines(failed.map((row) => `${AGENT_LABELS2[row.agent]}: ${row.detail}`).join("; ")));
10329
10640
  }
10330
10641
  }
10331
10642
  function assertAgentSetupSucceeded(results) {
10332
10643
  const failed = results.filter((result) => result.mcp === "failed");
10333
10644
  if (failed.length) {
10334
- throw new Error(failed.map((result) => `${AGENT_LABELS2[result.agent]}: ${result.detail ?? "MCP registration failed"}`).join("; "));
10645
+ throw new Error(printableLines(failed.map((result) => `${AGENT_LABELS2[result.agent]}: ${result.detail ?? "MCP registration failed"}`).join("; ")));
10335
10646
  }
10336
10647
  }
10337
10648
  async function agentSurfaceStatus(options = {}) {
10338
10649
  const target = resolveAgentHostTarget({ homeDir: options.homeDir, projectDir: options.project, codexHome: options.codexHome });
10339
10650
  const { homeDir, projectDir, codexHome } = target;
10340
- const sourceSkill = await readFile9(join11(skillsSourceDir(), "topolo", "SKILL.md"), "utf8");
10651
+ const sourceSkill = await readFile9(join12(skillsSourceDir(), "topolo", "SKILL.md"), "utf8");
10341
10652
  const agents = resolveTargets({
10342
10653
  agent: options.agent,
10343
10654
  all: options.all,
@@ -10369,15 +10680,19 @@ async function uninstallAgentSurfaces(options) {
10369
10680
  for (const agent of resolveTargets({ ...options, homeDir, project: projectDir, codexHome })) {
10370
10681
  const before = await readAgentRegistrations(agent, homeDir, projectDir, codexHome);
10371
10682
  const registrationRemoved = before.some((entry) => entry.name === name);
10372
- await unregisterAgentConfig(agent, homeDir, name, projectDir, codexHome);
10683
+ const backups = await unregisterAgentConfig(agent, homeDir, name, projectDir, codexHome);
10373
10684
  const remaining = await readAgentRegistrations(agent, homeDir, projectDir, codexHome);
10374
10685
  const skillPath = agentSkillPath(agent, homeDir, projectDir, codexHome);
10375
10686
  const skillRemoved = remaining.length === 0 && Boolean(skillPath && existsSync6(skillPath));
10376
10687
  if (skillRemoved && skillPath) await rm3(dirname7(skillPath), { recursive: true, force: true });
10377
- removed.push({ agent, name, registrationRemoved, skillRemoved });
10688
+ removed.push({ agent, name, registrationRemoved, skillRemoved, ...backups.length ? { backups } : {} });
10378
10689
  }
10379
10690
  return removed;
10380
10691
  }
10692
+ function formatUninstall(removed) {
10693
+ return printableLines(removed.map((r) => `${AGENT_LABELS2[r.agent]}: ${r.name} removed=${r.registrationRemoved}, skill removed=${r.skillRemoved}` + (r.backups ?? []).map((path) => `
10694
+ previous configuration backed up -> ${path}`).join("")).join("\n"));
10695
+ }
10381
10696
  function formatInstall(results, mcp) {
10382
10697
  const lines = [];
10383
10698
  const mcpStr = [mcp.command, ...mcp.args].join(" ");
@@ -10388,17 +10703,18 @@ function formatInstall(results, mcp) {
10388
10703
  else if (r.mcp === "config-file") lines.push(` MCP registered by editing the config file (${mcpStr})`);
10389
10704
  else if (r.mcp === "skipped") lines.push(" MCP registration skipped (--no-mcp)");
10390
10705
  else lines.push(` MCP registration FAILED: ${r.detail ?? "unknown error"}`);
10706
+ for (const backup of r.backups ?? []) lines.push(` previous configuration backed up -> ${backup}`);
10391
10707
  }
10392
10708
  lines.push("");
10393
10709
  lines.push("Next: run `topolo auth login` if you have not, then restart the agent.");
10394
10710
  lines.push("The MCP reads that credential \u2014 no API key needed. Available tools are");
10395
10711
  lines.push("scoped to your role, permissions, and organization at runtime.");
10396
- return lines.join("\n");
10712
+ return printableLines(lines.join("\n"));
10397
10713
  }
10398
10714
  function formatStatus(rows) {
10399
- return rows.map(
10400
- (r) => `${AGENT_LABELS2[r.agent]}: skill ${!r.skillPath ? "host-managed" : r.skillInstalled ? r.skillCurrent ? "current" : "stale" : "not installed"} (${r.skillPath ?? "native host surface"}), ${r.agent} CLI ${r.cliAvailable ? "available" : "not found"}, MCP registrations: ${r.registrations.map((registration) => registration.name).join(", ") || "none"}`
10401
- ).join("\n");
10715
+ return printableLines(rows.map(
10716
+ (r) => `${AGENT_LABELS2[r.agent]}: skill ${!r.skillPath ? "host-managed" : r.skillInstalled ? r.skillCurrent ? "current" : "stale" : "not installed"} (${r.skillPath ?? "native host surface"}), ${r.agent} CLI ${r.cliAvailable ? "available" : "not found"}, MCP registrations: ${r.registrations.map((registration) => printable(registration.name)).join(", ") || "none"}`
10717
+ ).join("\n"));
10402
10718
  }
10403
10719
 
10404
10720
  // src/commands/doctor.ts
@@ -10439,7 +10755,7 @@ var defaultSharedDoctorDependencies = () => ({
10439
10755
  fileExists: async (path) => access(path).then(() => true, () => false),
10440
10756
  environment: process.env
10441
10757
  });
10442
- function message2(error) {
10758
+ function message(error) {
10443
10759
  return error instanceof Error ? error.message : String(error);
10444
10760
  }
10445
10761
  async function runSharedDoctor(options, dependencies) {
@@ -10456,7 +10772,7 @@ async function runSharedDoctor(options, dependencies) {
10456
10772
  config = { ...config, identities: selected };
10457
10773
  checks.push({ name: "hub-config", ok: true, detail: `${paths.config}: port ${config.port}, ${selected.length} identities` });
10458
10774
  } catch (error) {
10459
- checks.push({ name: "hub-config", ok: false, detail: message2(error) });
10775
+ checks.push({ name: "hub-config", ok: false, detail: message(error) });
10460
10776
  return done();
10461
10777
  }
10462
10778
  let token;
@@ -10464,7 +10780,7 @@ async function runSharedDoctor(options, dependencies) {
10464
10780
  token = await readMcpHubToken(paths.token);
10465
10781
  checks.push({ name: "hub-token", ok: true, detail: `${paths.token} is private (0600)` });
10466
10782
  } catch (error) {
10467
- checks.push({ name: "hub-token", ok: false, detail: message2(error) });
10783
+ checks.push({ name: "hub-token", ok: false, detail: message(error) });
10468
10784
  return done();
10469
10785
  }
10470
10786
  const target = resolveAgentHostTarget({ homeDir: options.agentHome, codexHome: options.codexHome });
@@ -10481,14 +10797,14 @@ async function runSharedDoctor(options, dependencies) {
10481
10797
  if (response.status !== 200) throw new Error(`hub /health answered ${response.status}`);
10482
10798
  checks.push({ name: "hub", ok: true, detail: `listening on 127.0.0.1:${config.port}` });
10483
10799
  } catch (error) {
10484
- checks.push({ name: "hub", ok: false, detail: `hub is not reachable on 127.0.0.1:${config.port}: ${message2(error)}` });
10800
+ checks.push({ name: "hub", ok: false, detail: `hub is not reachable on 127.0.0.1:${config.port}: ${message(error)}` });
10485
10801
  return done();
10486
10802
  }
10487
10803
  const explicit = [...options.agent ?? [], ...options.claude ? ["claude"] : [], ...options.codex ? ["codex"] : []];
10488
10804
  const agents = (explicit.length ? explicit : [...SHARED_HUB_AGENTS]).filter((agent) => SHARED_HUB_AGENTS.includes(agent));
10489
10805
  for (const agent of agents) {
10490
10806
  const registrations = await readAgentRegistrations(agent, target.homeDir, void 0, target.codexHome).catch((error) => {
10491
- checks.push({ name: "registration", ok: false, detail: `${agent}: ${message2(error)}` });
10807
+ checks.push({ name: "registration", ok: false, detail: `${agent}: ${message(error)}` });
10492
10808
  return void 0;
10493
10809
  });
10494
10810
  if (!registrations) continue;
@@ -10511,13 +10827,13 @@ async function runSharedDoctor(options, dependencies) {
10511
10827
  );
10512
10828
  checks.push({ name: "mcp", ok: true, detail: `${identity.name}: ${detail}` });
10513
10829
  } catch (error) {
10514
- checks.push({ name: "mcp", ok: false, detail: `${identity.name}: ${message2(error)}` });
10830
+ checks.push({ name: "mcp", ok: false, detail: `${identity.name}: ${message(error)}` });
10515
10831
  }
10516
10832
  }
10517
10833
  return done();
10518
10834
  }
10519
10835
  function formatSharedDoctor(result) {
10520
- return result.checks.map((check) => `${check.ok ? "PASS" : "FAIL"} ${check.name}: ${check.detail}`).join("\n");
10836
+ return result.checks.map((check) => `${check.ok ? "PASS" : "FAIL"} ${check.name}: ${printableLines(check.detail)}`).join("\n");
10521
10837
  }
10522
10838
 
10523
10839
  // src/commands/doctor.ts
@@ -10648,7 +10964,7 @@ function normalizeDoctorContext(value) {
10648
10964
  return context;
10649
10965
  }
10650
10966
  function formatDoctor(result) {
10651
- return result.checks.map((check) => `${check.ok ? "PASS" : "FAIL"} ${check.name}: ${check.detail}`).join("\n");
10967
+ return result.checks.map((check) => `${check.ok ? "PASS" : "FAIL"} ${check.name}: ${printableLines(check.detail)}`).join("\n");
10652
10968
  }
10653
10969
  function errorMessage(error) {
10654
10970
  return error instanceof Error ? error.message : String(error);
@@ -11234,7 +11550,7 @@ function registerInstallationsCommand(program2) {
11234
11550
  import { createTopoloMcpLocalHandler } from "@topolo/mcp/local";
11235
11551
 
11236
11552
  // src/lib/mcp-hub-server.ts
11237
- import { createHash as createHash5, timingSafeEqual } from "crypto";
11553
+ import { createHash as createHash6, timingSafeEqual } from "crypto";
11238
11554
  import { stat as stat2 } from "fs/promises";
11239
11555
  import { createServer } from "http";
11240
11556
  var MCP_HUB_HOST = "127.0.0.1";
@@ -11245,11 +11561,11 @@ var STRIPPED_HEADERS = /* @__PURE__ */ new Set(["authorization", "x-api-key", "c
11245
11561
  function json(status, body, headers = {}) {
11246
11562
  return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json", ...headers } });
11247
11563
  }
11248
- function refusal(status, message3, headers = {}) {
11249
- return json(status, { jsonrpc: "2.0", error: { code: -32e3, message: message3 }, id: null }, headers);
11564
+ function refusal(status, message2, headers = {}) {
11565
+ return json(status, { jsonrpc: "2.0", error: { code: -32e3, message: message2 }, id: null }, headers);
11250
11566
  }
11251
11567
  function digest(value) {
11252
- return createHash5("sha256").update(value, "utf8").digest();
11568
+ return createHash6("sha256").update(value, "utf8").digest();
11253
11569
  }
11254
11570
  function bearerTokenMatches(header, token) {
11255
11571
  const match = /^Bearer[ \t]+(\S+)[ \t]*$/i.exec(header ?? "");