@topolo/cli 0.13.27 → 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 +448 -220
  2. package/package.json +3 -2
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
  }
@@ -328,8 +328,8 @@ function validTopoloActionAccessPolicy(value) {
328
328
  }
329
329
  var TopoloSdkError = class extends Error {
330
330
  code;
331
- constructor(code, message3) {
332
- super(message3);
331
+ constructor(code, message2) {
332
+ super(message2);
333
333
  this.name = "TopoloSdkError";
334
334
  this.code = code;
335
335
  }
@@ -450,8 +450,8 @@ var TopoloAuthError = class extends TopoloSdkError {
450
450
  requestId;
451
451
  retryAfter;
452
452
  details;
453
- constructor(message3, code = "auth_error", options = {}) {
454
- super(canonicalTopoloServiceErrorCode(code) ?? "auth_error", message3);
453
+ constructor(message2, code = "auth_error", options = {}) {
454
+ super(canonicalTopoloServiceErrorCode(code) ?? "auth_error", message2);
455
455
  this.name = "TopoloAuthError";
456
456
  this.status = options.status ?? 401;
457
457
  this.body = options.body;
@@ -473,10 +473,10 @@ var TopoloPermissionError = class extends TopoloSdkError {
473
473
  requestId;
474
474
  retryAfter;
475
475
  details;
476
- constructor(message3, required, options = {}) {
476
+ constructor(message2, required, options = {}) {
477
477
  super(
478
478
  canonicalTopoloServiceErrorCode(options.code) ?? serviceErrorCode(options.body, "permission_denied"),
479
- message3
479
+ message2
480
480
  );
481
481
  this.name = "TopoloPermissionError";
482
482
  this.required = canonicalTopoloRequired(required);
@@ -517,8 +517,8 @@ function extractTopoloServiceErrorMessage(body) {
517
517
  candidates.push(record5.message, record5.error_description);
518
518
  if (typeof nested === "string" && !canonicalTopoloServiceErrorCode(nested)) candidates.push(nested);
519
519
  for (const candidate of candidates) {
520
- const message3 = sanitizeTopoloServiceMessage(candidate);
521
- if (message3) return message3;
520
+ const message2 = sanitizeTopoloServiceMessage(candidate);
521
+ if (message2) return message2;
522
522
  }
523
523
  return void 0;
524
524
  }
@@ -592,8 +592,8 @@ function validationIssueDetails(issues) {
592
592
  }
593
593
  return paths.length ? { paths, issues } : { issues };
594
594
  }
595
- function issue(path, message3) {
596
- const text4 = sanitizeTopoloServiceMessage(message3);
595
+ function issue(path, message2) {
596
+ const text4 = sanitizeTopoloServiceMessage(message2);
597
597
  if (!path && !text4) return null;
598
598
  return { ...path ? { path } : {}, ...text4 ? { message: text4 } : {} };
599
599
  }
@@ -613,8 +613,8 @@ function canonicalTopoloValidationIssues(values) {
613
613
  function flattenedValidationIssues(formErrors, fieldErrors) {
614
614
  const issues = [];
615
615
  if (Array.isArray(formErrors)) {
616
- for (const message3 of formErrors) {
617
- const projected = issue(void 0, message3);
616
+ for (const message2 of formErrors) {
617
+ const projected = issue(void 0, message2);
618
618
  if (projected) issues.push(projected);
619
619
  }
620
620
  }
@@ -623,8 +623,8 @@ function flattenedValidationIssues(formErrors, fieldErrors) {
623
623
  const path = canonicalTopoloValidationPath(key);
624
624
  if (!path) continue;
625
625
  const list = Array.isArray(messages) && messages.length ? messages : [void 0];
626
- for (const message3 of list) {
627
- const projected = issue(path, message3);
626
+ for (const message2 of list) {
627
+ const projected = issue(path, message2);
628
628
  if (projected) issues.push(projected);
629
629
  }
630
630
  }
@@ -704,8 +704,8 @@ function topoloErrorHint(error) {
704
704
  return void 0;
705
705
  }
706
706
  var TopoloConfirmationRequiredError = class extends TopoloSdkError {
707
- constructor(message3) {
708
- super("confirmation_required", message3);
707
+ constructor(message2) {
708
+ super("confirmation_required", message2);
709
709
  this.name = "TopoloConfirmationRequiredError";
710
710
  }
711
711
  };
@@ -717,8 +717,8 @@ var TopoloHttpError = class extends TopoloSdkError {
717
717
  requestId;
718
718
  retryAfter;
719
719
  details;
720
- constructor(service, path, status, body, message3, options = {}) {
721
- 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}`);
722
722
  this.name = "TopoloHttpError";
723
723
  this.status = status;
724
724
  this.body = body;
@@ -740,8 +740,8 @@ function topoloRetryAfterSeconds(value, now = Date.now()) {
740
740
  var TopoloRateLimitError = class extends TopoloHttpError {
741
741
  retryable = true;
742
742
  retryAfterSeconds;
743
- constructor(service, path, body, message3, options = {}) {
744
- super(service, path, 429, body, message3, options);
743
+ constructor(service, path, body, message2, options = {}) {
744
+ super(service, path, 429, body, message2, options);
745
745
  this.name = "TopoloRateLimitError";
746
746
  if (this.code === "http_error") this.code = "rate_limited";
747
747
  this.retryAfterSeconds = topoloRetryAfterSeconds(this.retryAfter);
@@ -749,8 +749,8 @@ var TopoloRateLimitError = class extends TopoloHttpError {
749
749
  };
750
750
  var TopoloReauthRequiredError = class extends TopoloAuthError {
751
751
  reauthorization;
752
- constructor(message3, reauthorization, options = {}) {
753
- 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 });
754
754
  this.name = "TopoloReauthRequiredError";
755
755
  this.reauthorization = Object.freeze({ ...reauthorization });
756
756
  }
@@ -1718,8 +1718,8 @@ var ASSURANCE_LEVELS = /* @__PURE__ */ new Set(["authenticated", "workload_ident
1718
1718
  var HASH_PATTERN = /^sha256:[a-f0-9]{64}$/;
1719
1719
  var ActorEnvelopeValidationError = class extends Error {
1720
1720
  path;
1721
- constructor(path, message3) {
1722
- super(`${path}: ${message3}`);
1721
+ constructor(path, message2) {
1722
+ super(`${path}: ${message2}`);
1723
1723
  this.name = "ActorEnvelopeValidationError";
1724
1724
  this.path = path;
1725
1725
  }
@@ -1916,8 +1916,8 @@ function textArray(value, path) {
1916
1916
  function optionalTextFields(value, path, keys) {
1917
1917
  return Object.fromEntries(keys.flatMap((key) => value[key] === void 0 ? [] : [[key, text2(value[key], `${path}.${key}`)]]));
1918
1918
  }
1919
- function fail(path, message3) {
1920
- throw new ActorEnvelopeValidationError(path, message3);
1919
+ function fail(path, message2) {
1920
+ throw new ActorEnvelopeValidationError(path, message2);
1921
1921
  }
1922
1922
  function parseTopoloActionUploadContract(value) {
1923
1923
  if (!isRecord3(value) || value.profile !== "inline-base64" && value.profile !== "url-session") {
@@ -2372,7 +2372,7 @@ var TopoloClient = class _TopoloClient {
2372
2372
  }
2373
2373
  if (!res.ok) {
2374
2374
  const durationMs = Date.now() - startedAt;
2375
- const message3 = describeError(parsed, `HTTP ${res.status}`);
2375
+ const message2 = describeError(parsed, `HTTP ${res.status}`);
2376
2376
  const publicCode = extractTopoloServiceErrorCode(parsed) ?? "http_error";
2377
2377
  this.emit({
2378
2378
  phase: "error",
@@ -2412,7 +2412,7 @@ var TopoloClient = class _TopoloClient {
2412
2412
  resolvedService.serviceKey,
2413
2413
  opts.path,
2414
2414
  parsed,
2415
- message3,
2415
+ message2,
2416
2416
  errorContext
2417
2417
  );
2418
2418
  }
@@ -2421,7 +2421,7 @@ var TopoloClient = class _TopoloClient {
2421
2421
  opts.path,
2422
2422
  res.status,
2423
2423
  parsed,
2424
- message3,
2424
+ message2,
2425
2425
  errorContext
2426
2426
  );
2427
2427
  }
@@ -4409,7 +4409,7 @@ async function pollUntilApproved(params) {
4409
4409
  // src/commands/auth-lock.ts
4410
4410
  import { createHash as createHash3 } from "crypto";
4411
4411
 
4412
- // 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
4413
4413
  import { mkdir as mkdir2, readFile as readFile2, chmod as chmod2, lstat, readdir as readdir2 } from "fs/promises";
4414
4414
  import { existsSync as existsSync2, mkdirSync, lstatSync, readdirSync, readFileSync as readFileSync2, openSync, writeFileSync, fsyncSync, closeSync, renameSync, unlinkSync, rmdirSync, linkSync } from "fs";
4415
4415
  import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
@@ -4426,8 +4426,8 @@ import { homedir as homedir3, platform as hostPlatform } from "os";
4426
4426
  import { dirname as dirname3, join } from "path";
4427
4427
  var TopoloSdkError2 = class extends Error {
4428
4428
  code;
4429
- constructor(code, message3) {
4430
- super(message3);
4429
+ constructor(code, message2) {
4430
+ super(message2);
4431
4431
  this.name = "TopoloSdkError";
4432
4432
  this.code = code;
4433
4433
  }
@@ -4548,8 +4548,8 @@ var TopoloAuthError2 = class extends TopoloSdkError2 {
4548
4548
  requestId;
4549
4549
  retryAfter;
4550
4550
  details;
4551
- constructor(message3, code = "auth_error", options = {}) {
4552
- super(canonicalTopoloServiceErrorCode2(code) ?? "auth_error", message3);
4551
+ constructor(message2, code = "auth_error", options = {}) {
4552
+ super(canonicalTopoloServiceErrorCode2(code) ?? "auth_error", message2);
4553
4553
  this.name = "TopoloAuthError";
4554
4554
  this.status = options.status ?? 401;
4555
4555
  this.body = options.body;
@@ -4647,8 +4647,8 @@ function validationIssueDetails2(issues) {
4647
4647
  }
4648
4648
  return paths.length ? { paths, issues } : { issues };
4649
4649
  }
4650
- function issue2(path, message3) {
4651
- const text3 = sanitizeTopoloServiceMessage2(message3);
4650
+ function issue2(path, message2) {
4651
+ const text3 = sanitizeTopoloServiceMessage2(message2);
4652
4652
  if (!path && !text3) return null;
4653
4653
  return { ...path ? { path } : {}, ...text3 ? { message: text3 } : {} };
4654
4654
  }
@@ -4668,8 +4668,8 @@ function canonicalTopoloValidationIssues2(values) {
4668
4668
  function flattenedValidationIssues2(formErrors, fieldErrors) {
4669
4669
  const issues = [];
4670
4670
  if (Array.isArray(formErrors)) {
4671
- for (const message3 of formErrors) {
4672
- const projected = issue2(void 0, message3);
4671
+ for (const message2 of formErrors) {
4672
+ const projected = issue2(void 0, message2);
4673
4673
  if (projected) issues.push(projected);
4674
4674
  }
4675
4675
  }
@@ -4678,8 +4678,8 @@ function flattenedValidationIssues2(formErrors, fieldErrors) {
4678
4678
  const path = canonicalTopoloValidationPath2(key);
4679
4679
  if (!path) continue;
4680
4680
  const list = Array.isArray(messages) && messages.length ? messages : [void 0];
4681
- for (const message3 of list) {
4682
- const projected = issue2(path, message3);
4681
+ for (const message2 of list) {
4682
+ const projected = issue2(path, message2);
4683
4683
  if (projected) issues.push(projected);
4684
4684
  }
4685
4685
  }
@@ -4713,8 +4713,8 @@ var TopoloHttpError2 = class extends TopoloSdkError2 {
4713
4713
  requestId;
4714
4714
  retryAfter;
4715
4715
  details;
4716
- constructor(service, path, status, body, message3, options = {}) {
4717
- 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}`);
4718
4718
  this.name = "TopoloHttpError";
4719
4719
  this.status = status;
4720
4720
  this.body = body;
@@ -4736,8 +4736,8 @@ function topoloRetryAfterSeconds2(value, now = Date.now()) {
4736
4736
  var TopoloRateLimitError2 = class extends TopoloHttpError2 {
4737
4737
  retryable = true;
4738
4738
  retryAfterSeconds;
4739
- constructor(service, path, body, message3, options = {}) {
4740
- super(service, path, 429, body, message3, options);
4739
+ constructor(service, path, body, message2, options = {}) {
4740
+ super(service, path, 429, body, message2, options);
4741
4741
  this.name = "TopoloRateLimitError";
4742
4742
  if (this.code === "http_error") this.code = "rate_limited";
4743
4743
  this.retryAfterSeconds = topoloRetryAfterSeconds2(this.retryAfter);
@@ -4745,8 +4745,8 @@ var TopoloRateLimitError2 = class extends TopoloHttpError2 {
4745
4745
  };
4746
4746
  var TopoloReauthRequiredError2 = class extends TopoloAuthError2 {
4747
4747
  reauthorization;
4748
- constructor(message3, reauthorization, options = {}) {
4749
- 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 });
4750
4750
  this.name = "TopoloReauthRequiredError";
4751
4751
  this.reauthorization = Object.freeze({ ...reauthorization });
4752
4752
  }
@@ -6287,12 +6287,12 @@ function renewalFailure(error) {
6287
6287
  if (error instanceof TopoloReauthRequiredError2) return error;
6288
6288
  if (error instanceof TopoloSdkError2 && error.code === "config_unreadable") return error;
6289
6289
  if ((error instanceof TopoloAuthError2 || error instanceof TopoloHttpError2) && error.status === 429) {
6290
- 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.";
6291
6291
  return new TopoloRateLimitError2(
6292
6292
  "auth",
6293
6293
  path,
6294
- { code: "rate_limited", retryable: true, message: message3 },
6295
- message3,
6294
+ { code: "rate_limited", retryable: true, message: message2 },
6295
+ message2,
6296
6296
  { requestId: error.requestId, retryAfter: error.retryAfter }
6297
6297
  );
6298
6298
  }
@@ -8065,8 +8065,8 @@ var API_KEYS_LIST_ACTION = "app_topolo_auth.api_keys.list";
8065
8065
  var CUSTODY_ENVIRONMENT_NAMES = ["production", "staging", "development", "local"];
8066
8066
  var CustodyError = class extends Error {
8067
8067
  code;
8068
- constructor(code, message3) {
8069
- super(message3);
8068
+ constructor(code, message2) {
8069
+ super(message2);
8070
8070
  this.name = "CustodyError";
8071
8071
  this.code = code;
8072
8072
  }
@@ -8886,7 +8886,7 @@ import { spawnSync as spawnSync5 } from "child_process";
8886
8886
  import { existsSync as existsSync6 } from "fs";
8887
8887
  import { readFile as readFile9, rm as rm3 } from "fs/promises";
8888
8888
  import { homedir as homedir5 } from "os";
8889
- 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";
8890
8890
  import { fileURLToPath as fileURLToPath3 } from "url";
8891
8891
  import { createRequire } from "module";
8892
8892
 
@@ -8901,7 +8901,231 @@ import { spawnSync as spawnSync3 } from "child_process";
8901
8901
  import { existsSync as existsSync4 } from "fs";
8902
8902
  import { readFile as readFile4 } from "fs/promises";
8903
8903
  import { homedir as homedir4 } from "os";
8904
- 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
8905
9129
  var SUPPORTED_AGENT_IDS = [
8906
9130
  "claude",
8907
9131
  "claude-desktop",
@@ -8950,7 +9174,7 @@ function resolveAgentHostTarget(options) {
8950
9174
  return Object.freeze({
8951
9175
  homeDir,
8952
9176
  ...options.projectDir ? { projectDir: resolve3(options.projectDir) } : {},
8953
- codexHome: codexHome ?? join7(homeDir, ".codex")
9177
+ codexHome: codexHome ?? join8(homeDir, ".codex")
8954
9178
  });
8955
9179
  }
8956
9180
  var RESERVED_OPERATOR_REGISTRATION = ["topolo", "platform"].join("-");
@@ -8971,23 +9195,23 @@ function stableNodeCommand() {
8971
9195
  function agentSkillPath(agent, homeDir = homedir4(), projectDir, codexHome) {
8972
9196
  switch (agent) {
8973
9197
  case "claude":
8974
- 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");
8975
9199
  case "claude-desktop":
8976
- return join7(homeDir, ".claude", "skills", "topolo", "SKILL.md");
9200
+ return join8(homeDir, ".claude", "skills", "topolo", "SKILL.md");
8977
9201
  case "codex":
8978
- return join7(codexHome ?? join7(homeDir, ".codex"), "skills", "topolo", "SKILL.md");
9202
+ return join8(codexHome ?? join8(homeDir, ".codex"), "skills", "topolo", "SKILL.md");
8979
9203
  case "cursor":
8980
- return join7(projectDir ?? homeDir, ".cursor", "rules", "topolo.mdc");
9204
+ return join8(projectDir ?? homeDir, ".cursor", "rules", "topolo.mdc");
8981
9205
  case "grok":
8982
- return join7(projectDir ?? homeDir, ".grok", "skills", "topolo", "SKILL.md");
9206
+ return join8(projectDir ?? homeDir, ".grok", "skills", "topolo", "SKILL.md");
8983
9207
  case "gemini":
8984
- return join7(projectDir ?? homeDir, ".gemini", "skills", "topolo", "SKILL.md");
9208
+ return join8(projectDir ?? homeDir, ".gemini", "skills", "topolo", "SKILL.md");
8985
9209
  case "copilot":
8986
- 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");
8987
9211
  case "windsurf":
8988
- return projectDir ? join7(projectDir, ".windsurf", "rules", "topolo.md") : void 0;
9212
+ return projectDir ? join8(projectDir, ".windsurf", "rules", "topolo.md") : void 0;
8989
9213
  case "cline":
8990
- return join7(projectDir ?? homeDir, ".cline", "skills", "topolo", "SKILL.md");
9214
+ return join8(projectDir ?? homeDir, ".cline", "skills", "topolo", "SKILL.md");
8991
9215
  case "goose":
8992
9216
  case "amazon-q":
8993
9217
  return void 0;
@@ -8996,7 +9220,7 @@ function agentSkillPath(agent, homeDir = homedir4(), projectDir, codexHome) {
8996
9220
  function agentConfigPaths(agent, homeDir = homedir4(), projectDir, codexHome) {
8997
9221
  switch (agent) {
8998
9222
  case "claude":
8999
- 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" }];
9000
9224
  case "claude-desktop":
9001
9225
  return [{
9002
9226
  path: claudeDesktopConfigPath(homeDir),
@@ -9004,56 +9228,56 @@ function agentConfigPaths(agent, homeDir = homedir4(), projectDir, codexHome) {
9004
9228
  rootKey: "mcpServers"
9005
9229
  }];
9006
9230
  case "codex":
9007
- return [{ path: join7(codexHome ?? join7(homeDir, ".codex"), "config.toml"), format: "toml" }];
9231
+ return [{ path: join8(codexHome ?? join8(homeDir, ".codex"), "config.toml"), format: "toml" }];
9008
9232
  case "cursor":
9009
9233
  return [{
9010
- 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"),
9011
9235
  format: "json",
9012
9236
  rootKey: "mcpServers"
9013
9237
  }];
9014
9238
  case "grok":
9015
9239
  return [{
9016
- 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"),
9017
9241
  format: "toml"
9018
9242
  }];
9019
9243
  case "gemini":
9020
9244
  return [{
9021
- 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"),
9022
9246
  format: "json",
9023
9247
  rootKey: "mcpServers"
9024
9248
  }];
9025
9249
  case "copilot":
9026
9250
  return projectDir ? [
9027
- { path: join7(projectDir, ".mcp.json"), format: "json", rootKey: "mcpServers" },
9028
- { 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" }
9029
9253
  ] : [
9030
9254
  {
9031
- path: join7(homeDir, ".copilot", "mcp-config.json"),
9255
+ path: join8(homeDir, ".copilot", "mcp-config.json"),
9032
9256
  format: "json",
9033
9257
  rootKey: "mcpServers"
9034
9258
  },
9035
- { path: vscodeUserConfigPath(homeDir), format: "json", rootKey: "servers" }
9259
+ { path: vscodeUserConfigPath(homeDir), format: "json", rootKey: "servers", syntax: "jsonc" }
9036
9260
  ];
9037
9261
  case "windsurf":
9038
9262
  return [{
9039
- path: join7(homeDir, ".codeium", "windsurf", "mcp_config.json"),
9263
+ path: join8(homeDir, ".codeium", "windsurf", "mcp_config.json"),
9040
9264
  format: "json",
9041
9265
  rootKey: "mcpServers"
9042
9266
  }];
9043
9267
  case "cline":
9044
9268
  return [{
9045
- 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"),
9046
9270
  format: "json",
9047
9271
  rootKey: "mcpServers"
9048
9272
  }];
9049
9273
  case "goose":
9050
- return [{ path: join7(homeDir, ".config", "goose", "config.yaml"), format: "yaml" }];
9274
+ return [{ path: join8(homeDir, ".config", "goose", "config.yaml"), format: "yaml" }];
9051
9275
  case "amazon-q":
9052
9276
  if (projectDir) {
9053
- return [{ path: join7(projectDir, ".amazonq", "mcp.json"), format: "json", rootKey: "mcpServers" }];
9277
+ return [{ path: join8(projectDir, ".amazonq", "mcp.json"), format: "json", rootKey: "mcpServers" }];
9054
9278
  }
9055
9279
  return [{
9056
- 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"),
9057
9281
  format: "json",
9058
9282
  rootKey: "mcpServers"
9059
9283
  }];
@@ -9089,7 +9313,7 @@ function agentConfigExists(agent, homeDir = homedir4(), projectDir, codexHome) {
9089
9313
  return agentConfigPaths(agent, homeDir, projectDir, codexHome).some((config) => existsSync4(config.path));
9090
9314
  }
9091
9315
  async function assertClaudeProjectScope(homeDir, projectDir, name) {
9092
- const path = join7(homeDir, ".claude.json");
9316
+ const path = join8(homeDir, ".claude.json");
9093
9317
  let contents;
9094
9318
  try {
9095
9319
  contents = await readFile4(path, "utf8");
@@ -9099,10 +9323,9 @@ async function assertClaudeProjectScope(homeDir, projectDir, name) {
9099
9323
  }
9100
9324
  let config;
9101
9325
  try {
9102
- config = JSON.parse(contents);
9103
- if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error();
9326
+ config = parseHostJson(contents, { path, label: "Claude", syntax: "json" }).value;
9104
9327
  } catch {
9105
- 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.");
9106
9329
  }
9107
9330
  const object = (value) => {
9108
9331
  if (value === void 0) return void 0;
@@ -9117,21 +9340,21 @@ async function assertClaudeProjectScope(homeDir, projectDir, name) {
9117
9340
  }
9118
9341
  }
9119
9342
  function claudeDesktopConfigPath(homeDir) {
9120
- if (process.platform === "darwin") return join7(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json");
9121
- if (process.platform === "win32") return join7(process.env.APPDATA ?? join7(homeDir, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
9122
- 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");
9123
9346
  }
9124
9347
  function vscodeUserConfigPath(homeDir) {
9125
- if (process.platform === "darwin") return join7(homeDir, "Library", "Application Support", "Code", "User", "mcp.json");
9126
- if (process.platform === "win32") return join7(process.env.APPDATA ?? join7(homeDir, "AppData", "Roaming"), "Code", "User", "mcp.json");
9127
- 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");
9128
9351
  }
9129
9352
  async function readAgentRegistrations(agent, homeDir = homedir4(), projectDir, codexHome) {
9130
9353
  const registrations = [];
9131
9354
  for (const config of agentConfigPaths(agent, homeDir, projectDir, codexHome)) {
9132
9355
  if (!existsSync4(config.path)) continue;
9133
9356
  if (config.format === "json") {
9134
- registrations.push(...await readJsonRegistrations(config.path, config.rootKey ?? "mcpServers", agent));
9357
+ registrations.push(...await readJsonRegistrations(config, agent));
9135
9358
  } else if (config.format === "toml") {
9136
9359
  registrations.push(...await readTomlRegistrations(config.path, agent));
9137
9360
  } else {
@@ -9140,14 +9363,12 @@ async function readAgentRegistrations(agent, homeDir = homedir4(), projectDir, c
9140
9363
  }
9141
9364
  return registrations.filter((registration, index, all) => all.findIndex((candidate) => candidate.name === registration.name) === index);
9142
9365
  }
9143
- async function readJsonRegistrations(path, rootKey, agent) {
9144
- let parsed;
9145
- try {
9146
- parsed = JSON.parse(await readFile4(path, "utf8"));
9147
- } catch (error) {
9148
- throw new Error("Cannot read malformed " + (agent === "claude" ? "Claude" : AGENT_LABELS[agent]) + " configuration at " + path + ": " + message(error));
9149
- }
9150
- 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"];
9151
9372
  if (!servers || typeof servers !== "object" || Array.isArray(servers)) return [];
9152
9373
  return Object.entries(servers).filter(([name]) => isPublicRegistrationName(name)).flatMap(([name, value]) => normalizeRegistration(name, value));
9153
9374
  }
@@ -9398,39 +9619,62 @@ function normalizeRegistration(name, value) {
9398
9619
  const env = candidate.env && typeof candidate.env === "object" && !Array.isArray(candidate.env) ? stringEntries(candidate.env) : {};
9399
9620
  return [{ name, command: candidate.command, args, env }];
9400
9621
  }
9401
- function message(error) {
9402
- return error instanceof Error ? error.message : String(error);
9403
- }
9404
9622
 
9405
9623
  // src/commands/agent-config.ts
9406
9624
  import { existsSync as existsSync5 } from "fs";
9407
- 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";
9408
9626
  import { randomUUID as randomUUID4 } from "crypto";
9409
9627
  import { dirname as dirname5 } from "path";
9410
9628
  async function installAgentSkill(agent, source, homeDir, projectDir, codexHome) {
9411
9629
  const path = agentSkillPath(agent, homeDir, projectDir, codexHome);
9412
9630
  if (!path) return { installed: false };
9413
9631
  const contents = formatSkillForAgent(agent, source);
9414
- await atomicWrite(path, contents);
9415
- return { path, installed: true };
9632
+ return { path, installed: true, changed: await atomicWrite(path, contents) };
9416
9633
  }
9417
9634
  async function registerAgentConfig(agent, mcp, homeDir, registration, projectDir, codexHome) {
9418
9635
  if (agent === "claude" && projectDir) await assertClaudeProjectScope(homeDir, projectDir, registration.name);
9419
- for (const config of agentConfigPaths(agent, homeDir, projectDir, codexHome)) {
9420
- if (config.format === "json") {
9421
- await registerJson(config, agent, mcp, registration);
9422
- } else if (config.format === "toml") {
9423
- await registerToml(config.path, agent, mcp, registration);
9424
- } else {
9425
- 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);
9426
9667
  }
9668
+ if (result.backup) backups.push(result.backup);
9669
+ if (result.changed) changed.push(config.path);
9427
9670
  }
9671
+ return backups;
9428
9672
  }
9429
9673
  var SHARED_HUB_AGENTS = ["claude", "codex"];
9430
9674
  async function registerAgentHttpConfig(agent, homeDir, registration, codexHome) {
9431
9675
  const [config] = agentConfigPaths(agent, homeDir, void 0, codexHome);
9432
9676
  if (agent === "claude") {
9433
- await writeJsonServer(config, agent, registration.name, {
9677
+ await writeJsonServer(config, agent, registration.name, homeDir, {
9434
9678
  type: "http",
9435
9679
  url: registration.url,
9436
9680
  headers: registration.headers
@@ -9444,17 +9688,15 @@ async function registerAgentHttpConfig(agent, homeDir, registration, codexHome)
9444
9688
  await atomicWrite(config.path, text3 + block, before);
9445
9689
  }
9446
9690
  async function unregisterAgentConfig(agent, homeDir, name, projectDir, codexHome) {
9447
- for (const config of agentConfigPaths(agent, homeDir, projectDir, codexHome)) {
9448
- if (!existsSync5(config.path)) continue;
9449
- if (config.format === "json") {
9450
- await unregisterJson(config, agent, name);
9451
- } 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") {
9452
9695
  const text3 = withoutMcpServerSections(await readFile5(config.path, "utf8"), name);
9453
- await atomicWrite(config.path, text3 ? text3 + "\n" : "");
9454
- } else {
9455
- await unregisterGoose(config.path, name);
9696
+ return { changed: await atomicWrite(config.path, text3 ? text3 + "\n" : "") };
9456
9697
  }
9457
- }
9698
+ return { changed: await unregisterGoose(config.path, name) };
9699
+ });
9458
9700
  }
9459
9701
  function registrationEnvironment(agent, registration) {
9460
9702
  let privateAliases = {};
@@ -9486,56 +9728,36 @@ function registrationEnvironment(agent, registration) {
9486
9728
  ...privateAliases
9487
9729
  };
9488
9730
  }
9489
- async function registerJson(config, agent, mcp, registration) {
9731
+ async function registerJson(config, agent, mcp, registration, homeDir) {
9490
9732
  const entry = {
9491
9733
  command: mcp.command,
9492
9734
  args: mcp.args,
9493
9735
  env: registrationEnvironment(agent, registration)
9494
9736
  };
9495
9737
  if (agent === "claude" || agent === "claude-desktop" || (config.rootKey ?? "mcpServers") === "servers") entry.type = "stdio";
9496
- await writeJsonServer(config, agent, registration.name, entry);
9738
+ return writeJsonServer(config, agent, registration.name, homeDir, entry);
9497
9739
  }
9498
- async function writeJsonServer(config, agent, name, entry) {
9499
- const before = await fileSnapshot(config.path);
9500
- let parsed = {};
9501
- if (before.contents !== void 0) {
9502
- try {
9503
- parsed = JSON.parse(before.contents);
9504
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Expected an object.");
9505
- } catch (error) {
9506
- throw new Error(
9507
- "Cannot update malformed " + (agent === "claude" ? "Claude" : AGENT_LABELS[agent]) + " configuration at " + config.path + ": " + (error instanceof Error ? error.message : String(error))
9508
- );
9509
- }
9510
- }
9511
- const rootKey = config.rootKey ?? "mcpServers";
9512
- const existingServers = parsed[rootKey];
9513
- if (existingServers !== void 0 && (!existingServers || typeof existingServers !== "object" || Array.isArray(existingServers))) {
9514
- throw new Error("Cannot update malformed " + AGENT_LABELS[agent] + " MCP configuration at " + config.path + ".");
9515
- }
9516
- const servers = existingServers ?? {};
9517
- servers[name] = entry;
9518
- parsed[rootKey] = servers;
9519
- 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);
9520
9742
  }
9521
- async function unregisterJson(config, agent, name) {
9522
- 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;
9523
9755
  try {
9524
- parsed = JSON.parse(await readFile5(config.path, "utf8"));
9756
+ return { changed: await atomicWrite(config.path, contents, before), ...backup ? { backup } : {} };
9525
9757
  } catch (error) {
9526
- throw new Error(
9527
- "Cannot update malformed " + AGENT_LABELS[agent] + " configuration at " + config.path + ": " + (error instanceof Error ? error.message : String(error))
9528
- );
9529
- }
9530
- const rootKey = config.rootKey ?? "mcpServers";
9531
- const existingServers = parsed[rootKey];
9532
- if (existingServers !== void 0 && (!existingServers || typeof existingServers !== "object" || Array.isArray(existingServers))) {
9533
- 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;
9534
9760
  }
9535
- const servers = existingServers ?? {};
9536
- delete servers[name];
9537
- parsed[rootKey] = servers;
9538
- await atomicWrite(config.path, JSON.stringify(parsed, null, 2) + "\n");
9539
9761
  }
9540
9762
  async function registerToml(path, agent, mcp, registration) {
9541
9763
  const before = await fileSnapshot(path);
@@ -9545,7 +9767,7 @@ async function registerToml(path, agent, mcp, registration) {
9545
9767
  const environment = registrationEnvironment(agent, registration);
9546
9768
  const environmentText = Object.entries(environment).map(([key, value]) => JSON.stringify(key) + " = " + JSON.stringify(value)).join(", ");
9547
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";
9548
- await atomicWrite(path, text3 + block, before);
9770
+ return atomicWrite(path, text3 + block, before);
9549
9771
  }
9550
9772
  async function registerGoose(path, mcp, registration) {
9551
9773
  const existing = existsSync5(path) ? await readFile5(path, "utf8") : "";
@@ -9562,11 +9784,11 @@ async function registerGoose(path, mcp, registration) {
9562
9784
  " envs:",
9563
9785
  ...Object.entries(environment).map(([key, value]) => " " + key + ": " + yamlScalar(value))
9564
9786
  ].join("\n");
9565
- await atomicWrite(path, insertGooseExtension(withoutTopolo, block));
9787
+ return atomicWrite(path, insertGooseExtension(withoutTopolo, block));
9566
9788
  }
9567
9789
  async function unregisterGoose(path, name) {
9568
9790
  const existing = await readFile5(path, "utf8");
9569
- await atomicWrite(path, removeGooseExtension(existing, name));
9791
+ return atomicWrite(path, removeGooseExtension(existing, name));
9570
9792
  }
9571
9793
  function removeGooseExtension(text3, name) {
9572
9794
  const lines = text3.split(/\r?\n/);
@@ -9622,7 +9844,7 @@ async function fileSnapshot(path) {
9622
9844
  const parents = [];
9623
9845
  for (let parent = dirname5(path); ; parent = dirname5(parent)) {
9624
9846
  try {
9625
- const stat3 = await lstat3(parent);
9847
+ const stat3 = await lstat4(parent);
9626
9848
  if (!stat3.isDirectory() || stat3.isSymbolicLink()) throw new Error("Host destination contains a symbolic link or non-directory at " + parent + ".");
9627
9849
  parents.push({ path: parent, identity: [stat3.dev, stat3.ino].join(":") });
9628
9850
  } catch (error) {
@@ -9631,10 +9853,10 @@ async function fileSnapshot(path) {
9631
9853
  if (dirname5(parent) === parent) break;
9632
9854
  }
9633
9855
  try {
9634
- const stat3 = await lstat3(path);
9856
+ const stat3 = await lstat4(path);
9635
9857
  if (!stat3.isFile() || stat3.isSymbolicLink() || stat3.nlink !== 1) throw new Error("Host configuration must be a single regular file at " + path + ".");
9636
9858
  const contents = await readFile5(path, "utf8");
9637
- 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 + ".");
9638
9860
  return { contents, identity: fileIdentity(stat3), parents };
9639
9861
  } catch (error) {
9640
9862
  if (error.code === "ENOENT") return { parents };
@@ -9650,12 +9872,12 @@ async function assertSnapshot(path, before) {
9650
9872
  async function atomicWrite(path, contents, before) {
9651
9873
  before ??= await fileSnapshot(path);
9652
9874
  await assertSnapshot(path, before);
9653
- if (before.contents === contents) return;
9654
- await mkdir4(dirname5(path), { recursive: true });
9875
+ if (before.contents === contents) return false;
9876
+ await mkdir5(dirname5(path), { recursive: true });
9655
9877
  const destination = await fileSnapshot(path);
9656
9878
  await assertSnapshot(path, before);
9657
9879
  const temporaryPath = path + ".topolo-" + randomUUID4() + ".tmp";
9658
- const handle = await open2(temporaryPath, "wx", 384);
9880
+ const handle = await open3(temporaryPath, "wx", 384);
9659
9881
  let temporaryIdentity;
9660
9882
  let handleClosed = false;
9661
9883
  let temporaryPending = true;
@@ -9667,7 +9889,7 @@ async function atomicWrite(path, contents, before) {
9667
9889
  await handle.close();
9668
9890
  handleClosed = true;
9669
9891
  await assertSnapshot(path, destination);
9670
- const temporary = await lstat3(temporaryPath);
9892
+ const temporary = await lstat4(temporaryPath);
9671
9893
  if (!temporary.isFile() || temporary.isSymbolicLink() || temporary.nlink !== 1 || [temporary.dev, temporary.ino].join(":") !== temporaryIdentity) {
9672
9894
  throw new Error("Host temporary file custody changed before rename at " + path + ".");
9673
9895
  }
@@ -9675,20 +9897,21 @@ async function atomicWrite(path, contents, before) {
9675
9897
  temporaryPending = false;
9676
9898
  const installed = await fileSnapshot(path);
9677
9899
  if (installed.contents !== contents) throw new Error("Host configuration read-back failed at " + path + ".");
9900
+ return true;
9678
9901
  } finally {
9679
9902
  if (!handleClosed) await handle.close();
9680
9903
  if (temporaryPending && temporaryIdentity) {
9681
9904
  let cleanupOwned = false;
9682
9905
  try {
9683
9906
  const parentsOwned = await Promise.all(destination.parents.map(async (parent) => {
9684
- const current2 = await lstat3(parent.path);
9907
+ const current2 = await lstat4(parent.path);
9685
9908
  return current2.isDirectory() && !current2.isSymbolicLink() && [current2.dev, current2.ino].join(":") === parent.identity;
9686
9909
  }));
9687
- const current = await lstat3(temporaryPath);
9910
+ const current = await lstat4(temporaryPath);
9688
9911
  cleanupOwned = parentsOwned.every(Boolean) && current.isFile() && !current.isSymbolicLink() && current.nlink === 1 && [current.dev, current.ino].join(":") === temporaryIdentity;
9689
9912
  } catch {
9690
9913
  }
9691
- if (cleanupOwned) await unlink(temporaryPath);
9914
+ if (cleanupOwned) await unlink2(temporaryPath);
9692
9915
  }
9693
9916
  }
9694
9917
  }
@@ -9710,12 +9933,12 @@ function withoutMcpServerSections(text3, name) {
9710
9933
  // src/commands/mcp-hub-setup.ts
9711
9934
  import { realpathSync } from "fs";
9712
9935
  import { readFile as readFile8 } from "fs/promises";
9713
- import { join as join10 } from "path";
9936
+ import { join as join11 } from "path";
9714
9937
 
9715
9938
  // src/lib/mcp-hub-config.ts
9716
9939
  import { randomBytes } from "crypto";
9717
- import { lstat as lstat4, mkdir as mkdir5, open as open3, readFile as readFile6, rename as rename4, unlink as unlink2 } from "fs/promises";
9718
- 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";
9719
9942
  var MCP_HUB_DEFAULT_PORT = 7337;
9720
9943
  var MCP_HUB_ENVIRONMENTS = ["production", "staging", "development", "local"];
9721
9944
  var RESERVED_NAME = ["topolo", "platform"].join("-");
@@ -9723,7 +9946,7 @@ var NAME_PATTERN = /^topolo(-[a-z0-9][a-z0-9_-]*)?$/;
9723
9946
  var TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
9724
9947
  function mcpHubPaths(credentialStorePath = configPath()) {
9725
9948
  const directory = dirname6(credentialStorePath);
9726
- 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") };
9727
9950
  }
9728
9951
  function mcpHubUrl(port, name) {
9729
9952
  return `http://127.0.0.1:${port}/mcp/${name}`;
@@ -9820,9 +10043,9 @@ function upsertMcpHubIdentity(config, identity) {
9820
10043
  return { ...config, identities };
9821
10044
  }
9822
10045
  async function writePrivateFile(path, contents) {
9823
- await mkdir5(dirname6(path), { recursive: true, mode: 448 });
10046
+ await mkdir6(dirname6(path), { recursive: true, mode: 448 });
9824
10047
  const temporary = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
9825
- const handle = await open3(temporary, "wx", 384);
10048
+ const handle = await open4(temporary, "wx", 384);
9826
10049
  try {
9827
10050
  await handle.writeFile(contents, "utf8");
9828
10051
  } finally {
@@ -9831,7 +10054,7 @@ async function writePrivateFile(path, contents) {
9831
10054
  try {
9832
10055
  await rename4(temporary, path);
9833
10056
  } catch (error) {
9834
- await unlink2(temporary).catch(() => void 0);
10057
+ await unlink3(temporary).catch(() => void 0);
9835
10058
  throw error;
9836
10059
  }
9837
10060
  }
@@ -9839,7 +10062,7 @@ async function writeMcpHubConfig(path, config) {
9839
10062
  await writePrivateFile(path, serializeMcpHubConfig(config));
9840
10063
  }
9841
10064
  async function assertPrivateTokenFile(path) {
9842
- const stat3 = await lstat4(path);
10065
+ const stat3 = await lstat5(path);
9843
10066
  if (!stat3.isFile() || stat3.isSymbolicLink() || stat3.nlink !== 1) {
9844
10067
  throw new Error(`MCP hub token at ${path} must be a single regular file.`);
9845
10068
  }
@@ -9868,9 +10091,9 @@ async function ensureMcpHubToken(path) {
9868
10091
  } catch (error) {
9869
10092
  if (!/is missing at/.test(error.message)) throw error;
9870
10093
  }
9871
- await mkdir5(dirname6(path), { recursive: true, mode: 448 });
10094
+ await mkdir6(dirname6(path), { recursive: true, mode: 448 });
9872
10095
  const token = randomBytes(32).toString("base64url");
9873
- const handle = await open3(path, "wx", 384);
10096
+ const handle = await open4(path, "wx", 384);
9874
10097
  try {
9875
10098
  await handle.writeFile(token + "\n", "utf8");
9876
10099
  } finally {
@@ -9882,7 +10105,7 @@ async function ensureMcpHubToken(path) {
9882
10105
  // src/lib/mcp-hub-service.ts
9883
10106
  import { spawnSync as spawnSync4 } from "child_process";
9884
10107
  import { readFile as readFile7 } from "fs/promises";
9885
- import { join as join9 } from "path";
10108
+ import { join as join10 } from "path";
9886
10109
  var MCP_HUB_LAUNCHD_LABEL = "io.topolo.mcp-hub";
9887
10110
  var MCP_HUB_SYSTEMD_UNIT = "topolo-mcp-hub.service";
9888
10111
  var PASSTHROUGH_ENVIRONMENT = ["TOPOLO_CONFIG_PATH", "XDG_CONFIG_HOME"];
@@ -9896,7 +10119,7 @@ function serviceKindFor(platform4) {
9896
10119
  return void 0;
9897
10120
  }
9898
10121
  function mcpHubServicePath(kind, homeDir, xdgConfigHome) {
9899
- 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);
9900
10123
  }
9901
10124
  function serviceEnvironment(environment) {
9902
10125
  const selected = {};
@@ -9991,7 +10214,7 @@ async function installMcpHubService(options) {
9991
10214
  };
9992
10215
  }
9993
10216
  const path = mcpHubServicePath(kind, options.homeDir, options.xdgConfigHome);
9994
- 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;
9995
10218
  const contents = kind === "launchd" ? renderLaunchdPlist(spec) : renderSystemdUnit(spec);
9996
10219
  const changed = await readExisting(path) !== contents;
9997
10220
  if (changed) await writePrivateFile(path, contents);
@@ -10173,7 +10396,7 @@ async function installSharedAgentSurfaces(options, dependencies) {
10173
10396
  function defaultSharedSetupDependencies(skillsDir, realHome) {
10174
10397
  return {
10175
10398
  paths: () => mcpHubPaths(),
10176
- skillSource: () => readFile8(join10(skillsDir, "topolo", "SKILL.md"), "utf8"),
10399
+ skillSource: () => readFile8(join11(skillsDir, "topolo", "SKILL.md"), "utf8"),
10177
10400
  platform: process.platform,
10178
10401
  realHome,
10179
10402
  nodeCommand: stableNodeCommand,
@@ -10194,10 +10417,10 @@ function formatSharedSetup(result) {
10194
10417
  lines.push(` skill -> ${row.skillPath}`);
10195
10418
  lines.push(row.mcp === "config-file" ? ` HTTP MCP registered: ${row.registrations.join(", ")}` : ` HTTP MCP registration FAILED: ${row.detail}`);
10196
10419
  }
10197
- 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}`);
10198
10421
  if (result.service) lines.push(`Service (${result.service.kind}): ${result.service.detail}`);
10199
10422
  lines.push("", "Next: restart the agents, then run `topolo doctor --shared`.");
10200
- return lines.join("\n");
10423
+ return printableLines(lines.join("\n"));
10201
10424
  }
10202
10425
 
10203
10426
  // src/commands/skills.ts
@@ -10207,7 +10430,7 @@ function skillsSourceDir() {
10207
10430
  const distCandidate = resolve4(here, "../skills");
10208
10431
  const srcCandidate = resolve4(here, "../../skills");
10209
10432
  for (const candidate of [distCandidate, srcCandidate]) {
10210
- if (existsSync6(join11(candidate, "topolo", "SKILL.md"))) return candidate;
10433
+ if (existsSync6(join12(candidate, "topolo", "SKILL.md"))) return candidate;
10211
10434
  }
10212
10435
  return distCandidate;
10213
10436
  }
@@ -10265,26 +10488,30 @@ async function installForAgent(agent, sourceDir, mcp, opts) {
10265
10488
  return { agent, skillInstalled: false, mcp: "failed", detail: err instanceof Error ? err.message : String(err) };
10266
10489
  }
10267
10490
  }
10268
- const source = await readFile9(join11(sourceDir, "topolo", "SKILL.md"), "utf8");
10491
+ const source = await readFile9(join12(sourceDir, "topolo", "SKILL.md"), "utf8");
10269
10492
  const skill = await installAgentSkill(agent, source, homeDir, projectDir, codexHome);
10270
10493
  if (!registration) {
10271
10494
  return { agent, skillPath: skill.path, skillInstalled: skill.installed, mcp: "skipped" };
10272
10495
  }
10496
+ let backups = [];
10273
10497
  try {
10274
- await registerAgentConfig(agent, mcp, homeDir, registration, projectDir, codexHome);
10498
+ backups = await registerAgentConfig(agent, mcp, homeDir, registration, projectDir, codexHome);
10275
10499
  const installed = (await readAgentRegistrations(agent, homeDir, projectDir, codexHome)).find((entry) => entry.name === registration.name);
10276
10500
  const expectedEnvironment = registrationEnvironment(agent, registration);
10277
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)) {
10278
10502
  throw new Error(`Host configuration did not retain the ${registration.name} MCP registration.`);
10279
10503
  }
10280
- 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 } : {} };
10281
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) + "." : "";
10282
10508
  return {
10283
10509
  agent,
10284
10510
  skillPath: skill.path,
10285
10511
  skillInstalled: skill.installed,
10286
10512
  mcp: "failed",
10287
- detail: err instanceof Error ? err.message : String(err)
10513
+ detail: (err instanceof Error ? err.message : String(err)) + skillNote,
10514
+ ...backups.length ? { backups } : {}
10288
10515
  };
10289
10516
  }
10290
10517
  }
@@ -10351,11 +10578,7 @@ function registerSkillsCommand(program2) {
10351
10578
  ...opts,
10352
10579
  homeDir: opts.agentHome ? resolve4(opts.agentHome) : void 0
10353
10580
  });
10354
- emit(
10355
- { removed },
10356
- mode,
10357
- () => removed.map((r) => `${AGENT_LABELS2[r.agent]}: ${r.name} removed=${r.registrationRemoved}, skill removed=${r.skillRemoved}`).join("\n")
10358
- );
10581
+ emit({ removed }, mode, () => formatUninstall(removed));
10359
10582
  } catch (err) {
10360
10583
  fail2(err, mode);
10361
10584
  }
@@ -10363,7 +10586,7 @@ function registerSkillsCommand(program2) {
10363
10586
  }
10364
10587
  async function installAgentSurfaces(opts) {
10365
10588
  const sourceDir = skillsSourceDir();
10366
- if (!existsSync6(join11(sourceDir, "topolo", "SKILL.md"))) {
10589
+ if (!existsSync6(join12(sourceDir, "topolo", "SKILL.md"))) {
10367
10590
  throw new Error("Bundled skill file not found. Reinstall the CLI: npm install -g @topolo/cli");
10368
10591
  }
10369
10592
  const mcp = resolveMcpCommand();
@@ -10413,19 +10636,19 @@ function registerSetupCommand(program2) {
10413
10636
  function assertSharedSetupSucceeded(result) {
10414
10637
  const failed = result.installed.filter((row) => row.mcp === "failed");
10415
10638
  if (failed.length) {
10416
- 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("; ")));
10417
10640
  }
10418
10641
  }
10419
10642
  function assertAgentSetupSucceeded(results) {
10420
10643
  const failed = results.filter((result) => result.mcp === "failed");
10421
10644
  if (failed.length) {
10422
- 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("; ")));
10423
10646
  }
10424
10647
  }
10425
10648
  async function agentSurfaceStatus(options = {}) {
10426
10649
  const target = resolveAgentHostTarget({ homeDir: options.homeDir, projectDir: options.project, codexHome: options.codexHome });
10427
10650
  const { homeDir, projectDir, codexHome } = target;
10428
- const sourceSkill = await readFile9(join11(skillsSourceDir(), "topolo", "SKILL.md"), "utf8");
10651
+ const sourceSkill = await readFile9(join12(skillsSourceDir(), "topolo", "SKILL.md"), "utf8");
10429
10652
  const agents = resolveTargets({
10430
10653
  agent: options.agent,
10431
10654
  all: options.all,
@@ -10457,15 +10680,19 @@ async function uninstallAgentSurfaces(options) {
10457
10680
  for (const agent of resolveTargets({ ...options, homeDir, project: projectDir, codexHome })) {
10458
10681
  const before = await readAgentRegistrations(agent, homeDir, projectDir, codexHome);
10459
10682
  const registrationRemoved = before.some((entry) => entry.name === name);
10460
- await unregisterAgentConfig(agent, homeDir, name, projectDir, codexHome);
10683
+ const backups = await unregisterAgentConfig(agent, homeDir, name, projectDir, codexHome);
10461
10684
  const remaining = await readAgentRegistrations(agent, homeDir, projectDir, codexHome);
10462
10685
  const skillPath = agentSkillPath(agent, homeDir, projectDir, codexHome);
10463
10686
  const skillRemoved = remaining.length === 0 && Boolean(skillPath && existsSync6(skillPath));
10464
10687
  if (skillRemoved && skillPath) await rm3(dirname7(skillPath), { recursive: true, force: true });
10465
- removed.push({ agent, name, registrationRemoved, skillRemoved });
10688
+ removed.push({ agent, name, registrationRemoved, skillRemoved, ...backups.length ? { backups } : {} });
10466
10689
  }
10467
10690
  return removed;
10468
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
+ }
10469
10696
  function formatInstall(results, mcp) {
10470
10697
  const lines = [];
10471
10698
  const mcpStr = [mcp.command, ...mcp.args].join(" ");
@@ -10476,17 +10703,18 @@ function formatInstall(results, mcp) {
10476
10703
  else if (r.mcp === "config-file") lines.push(` MCP registered by editing the config file (${mcpStr})`);
10477
10704
  else if (r.mcp === "skipped") lines.push(" MCP registration skipped (--no-mcp)");
10478
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}`);
10479
10707
  }
10480
10708
  lines.push("");
10481
10709
  lines.push("Next: run `topolo auth login` if you have not, then restart the agent.");
10482
10710
  lines.push("The MCP reads that credential \u2014 no API key needed. Available tools are");
10483
10711
  lines.push("scoped to your role, permissions, and organization at runtime.");
10484
- return lines.join("\n");
10712
+ return printableLines(lines.join("\n"));
10485
10713
  }
10486
10714
  function formatStatus(rows) {
10487
- return rows.map(
10488
- (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"}`
10489
- ).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"));
10490
10718
  }
10491
10719
 
10492
10720
  // src/commands/doctor.ts
@@ -10527,7 +10755,7 @@ var defaultSharedDoctorDependencies = () => ({
10527
10755
  fileExists: async (path) => access(path).then(() => true, () => false),
10528
10756
  environment: process.env
10529
10757
  });
10530
- function message2(error) {
10758
+ function message(error) {
10531
10759
  return error instanceof Error ? error.message : String(error);
10532
10760
  }
10533
10761
  async function runSharedDoctor(options, dependencies) {
@@ -10544,7 +10772,7 @@ async function runSharedDoctor(options, dependencies) {
10544
10772
  config = { ...config, identities: selected };
10545
10773
  checks.push({ name: "hub-config", ok: true, detail: `${paths.config}: port ${config.port}, ${selected.length} identities` });
10546
10774
  } catch (error) {
10547
- checks.push({ name: "hub-config", ok: false, detail: message2(error) });
10775
+ checks.push({ name: "hub-config", ok: false, detail: message(error) });
10548
10776
  return done();
10549
10777
  }
10550
10778
  let token;
@@ -10552,7 +10780,7 @@ async function runSharedDoctor(options, dependencies) {
10552
10780
  token = await readMcpHubToken(paths.token);
10553
10781
  checks.push({ name: "hub-token", ok: true, detail: `${paths.token} is private (0600)` });
10554
10782
  } catch (error) {
10555
- checks.push({ name: "hub-token", ok: false, detail: message2(error) });
10783
+ checks.push({ name: "hub-token", ok: false, detail: message(error) });
10556
10784
  return done();
10557
10785
  }
10558
10786
  const target = resolveAgentHostTarget({ homeDir: options.agentHome, codexHome: options.codexHome });
@@ -10569,14 +10797,14 @@ async function runSharedDoctor(options, dependencies) {
10569
10797
  if (response.status !== 200) throw new Error(`hub /health answered ${response.status}`);
10570
10798
  checks.push({ name: "hub", ok: true, detail: `listening on 127.0.0.1:${config.port}` });
10571
10799
  } catch (error) {
10572
- 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)}` });
10573
10801
  return done();
10574
10802
  }
10575
10803
  const explicit = [...options.agent ?? [], ...options.claude ? ["claude"] : [], ...options.codex ? ["codex"] : []];
10576
10804
  const agents = (explicit.length ? explicit : [...SHARED_HUB_AGENTS]).filter((agent) => SHARED_HUB_AGENTS.includes(agent));
10577
10805
  for (const agent of agents) {
10578
10806
  const registrations = await readAgentRegistrations(agent, target.homeDir, void 0, target.codexHome).catch((error) => {
10579
- checks.push({ name: "registration", ok: false, detail: `${agent}: ${message2(error)}` });
10807
+ checks.push({ name: "registration", ok: false, detail: `${agent}: ${message(error)}` });
10580
10808
  return void 0;
10581
10809
  });
10582
10810
  if (!registrations) continue;
@@ -10599,13 +10827,13 @@ async function runSharedDoctor(options, dependencies) {
10599
10827
  );
10600
10828
  checks.push({ name: "mcp", ok: true, detail: `${identity.name}: ${detail}` });
10601
10829
  } catch (error) {
10602
- checks.push({ name: "mcp", ok: false, detail: `${identity.name}: ${message2(error)}` });
10830
+ checks.push({ name: "mcp", ok: false, detail: `${identity.name}: ${message(error)}` });
10603
10831
  }
10604
10832
  }
10605
10833
  return done();
10606
10834
  }
10607
10835
  function formatSharedDoctor(result) {
10608
- 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");
10609
10837
  }
10610
10838
 
10611
10839
  // src/commands/doctor.ts
@@ -10736,7 +10964,7 @@ function normalizeDoctorContext(value) {
10736
10964
  return context;
10737
10965
  }
10738
10966
  function formatDoctor(result) {
10739
- 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");
10740
10968
  }
10741
10969
  function errorMessage(error) {
10742
10970
  return error instanceof Error ? error.message : String(error);
@@ -11322,7 +11550,7 @@ function registerInstallationsCommand(program2) {
11322
11550
  import { createTopoloMcpLocalHandler } from "@topolo/mcp/local";
11323
11551
 
11324
11552
  // src/lib/mcp-hub-server.ts
11325
- import { createHash as createHash5, timingSafeEqual } from "crypto";
11553
+ import { createHash as createHash6, timingSafeEqual } from "crypto";
11326
11554
  import { stat as stat2 } from "fs/promises";
11327
11555
  import { createServer } from "http";
11328
11556
  var MCP_HUB_HOST = "127.0.0.1";
@@ -11333,11 +11561,11 @@ var STRIPPED_HEADERS = /* @__PURE__ */ new Set(["authorization", "x-api-key", "c
11333
11561
  function json(status, body, headers = {}) {
11334
11562
  return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json", ...headers } });
11335
11563
  }
11336
- function refusal(status, message3, headers = {}) {
11337
- 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);
11338
11566
  }
11339
11567
  function digest(value) {
11340
- return createHash5("sha256").update(value, "utf8").digest();
11568
+ return createHash6("sha256").update(value, "utf8").digest();
11341
11569
  }
11342
11570
  function bearerTokenMatches(header, token) {
11343
11571
  const match = /^Bearer[ \t]+(\S+)[ \t]*$/i.exec(header ?? "");