@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.
- package/dist/index.js +448 -220
- 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,
|
|
332
|
-
super(
|
|
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(
|
|
454
|
-
super(canonicalTopoloServiceErrorCode(code) ?? "auth_error",
|
|
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(
|
|
476
|
+
constructor(message2, required, options = {}) {
|
|
477
477
|
super(
|
|
478
478
|
canonicalTopoloServiceErrorCode(options.code) ?? serviceErrorCode(options.body, "permission_denied"),
|
|
479
|
-
|
|
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
|
|
521
|
-
if (
|
|
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,
|
|
596
|
-
const text4 = sanitizeTopoloServiceMessage(
|
|
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
|
|
617
|
-
const projected = issue(void 0,
|
|
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
|
|
627
|
-
const projected = issue(path,
|
|
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(
|
|
708
|
-
super("confirmation_required",
|
|
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,
|
|
721
|
-
super(serviceErrorCode(body, "http_error"),
|
|
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,
|
|
744
|
-
super(service, path, 429, body,
|
|
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(
|
|
753
|
-
super(
|
|
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,
|
|
1722
|
-
super(`${path}: ${
|
|
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,
|
|
1920
|
-
throw new ActorEnvelopeValidationError(path,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
4430
|
-
super(
|
|
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(
|
|
4552
|
-
super(canonicalTopoloServiceErrorCode2(code) ?? "auth_error",
|
|
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,
|
|
4651
|
-
const text3 = sanitizeTopoloServiceMessage2(
|
|
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
|
|
4672
|
-
const projected = issue2(void 0,
|
|
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
|
|
4682
|
-
const projected = issue2(path,
|
|
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,
|
|
4717
|
-
super(serviceErrorCode2(body, "http_error"),
|
|
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,
|
|
4740
|
-
super(service, path, 429, body,
|
|
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(
|
|
4749
|
-
super(
|
|
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
|
|
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:
|
|
6295
|
-
|
|
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,
|
|
8069
|
-
super(
|
|
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
|
|
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
|
|
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 ??
|
|
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 ?
|
|
9198
|
+
return projectDir ? join8(projectDir, ".claude", "skills", "topolo", "SKILL.md") : join8(homeDir, ".claude", "skills", "topolo", "SKILL.md");
|
|
8975
9199
|
case "claude-desktop":
|
|
8976
|
-
return
|
|
9200
|
+
return join8(homeDir, ".claude", "skills", "topolo", "SKILL.md");
|
|
8977
9201
|
case "codex":
|
|
8978
|
-
return
|
|
9202
|
+
return join8(codexHome ?? join8(homeDir, ".codex"), "skills", "topolo", "SKILL.md");
|
|
8979
9203
|
case "cursor":
|
|
8980
|
-
return
|
|
9204
|
+
return join8(projectDir ?? homeDir, ".cursor", "rules", "topolo.mdc");
|
|
8981
9205
|
case "grok":
|
|
8982
|
-
return
|
|
9206
|
+
return join8(projectDir ?? homeDir, ".grok", "skills", "topolo", "SKILL.md");
|
|
8983
9207
|
case "gemini":
|
|
8984
|
-
return
|
|
9208
|
+
return join8(projectDir ?? homeDir, ".gemini", "skills", "topolo", "SKILL.md");
|
|
8985
9209
|
case "copilot":
|
|
8986
|
-
return projectDir ?
|
|
9210
|
+
return projectDir ? join8(projectDir, ".github", "skills", "topolo", "SKILL.md") : join8(homeDir, ".copilot", "skills", "topolo", "SKILL.md");
|
|
8987
9211
|
case "windsurf":
|
|
8988
|
-
return projectDir ?
|
|
9212
|
+
return projectDir ? join8(projectDir, ".windsurf", "rules", "topolo.md") : void 0;
|
|
8989
9213
|
case "cline":
|
|
8990
|
-
return
|
|
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 ?
|
|
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:
|
|
9231
|
+
return [{ path: join8(codexHome ?? join8(homeDir, ".codex"), "config.toml"), format: "toml" }];
|
|
9008
9232
|
case "cursor":
|
|
9009
9233
|
return [{
|
|
9010
|
-
path: projectDir ?
|
|
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 ?
|
|
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 ?
|
|
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:
|
|
9028
|
-
{ path:
|
|
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:
|
|
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:
|
|
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 ?
|
|
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:
|
|
9274
|
+
return [{ path: join8(homeDir, ".config", "goose", "config.yaml"), format: "yaml" }];
|
|
9051
9275
|
case "amazon-q":
|
|
9052
9276
|
if (projectDir) {
|
|
9053
|
-
return [{ path:
|
|
9277
|
+
return [{ path: join8(projectDir, ".amazonq", "mcp.json"), format: "json", rootKey: "mcpServers" }];
|
|
9054
9278
|
}
|
|
9055
9279
|
return [{
|
|
9056
|
-
path: existsSync4(
|
|
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 =
|
|
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 =
|
|
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
|
|
9121
|
-
if (process.platform === "win32") return
|
|
9122
|
-
return
|
|
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
|
|
9126
|
-
if (process.platform === "win32") return
|
|
9127
|
-
return
|
|
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
|
|
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
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
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
|
|
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
|
-
|
|
9420
|
-
if (config.format === "json")
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
|
|
9424
|
-
|
|
9425
|
-
|
|
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
|
-
|
|
9448
|
-
|
|
9449
|
-
if (config.format === "json")
|
|
9450
|
-
|
|
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
|
-
|
|
9738
|
+
return writeJsonServer(config, agent, registration.name, homeDir, entry);
|
|
9497
9739
|
}
|
|
9498
|
-
async function writeJsonServer(config, agent, name, entry) {
|
|
9499
|
-
|
|
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
|
|
9522
|
-
|
|
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
|
-
|
|
9756
|
+
return { changed: await atomicWrite(config.path, contents, before), ...backup ? { backup } : {} };
|
|
9525
9757
|
} catch (error) {
|
|
9526
|
-
throw new
|
|
9527
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
9718
|
-
import { dirname as dirname6, join as
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
10094
|
+
await mkdir6(dirname6(path), { recursive: true, mode: 448 });
|
|
9872
10095
|
const token = randomBytes(32).toString("base64url");
|
|
9873
|
-
const handle = await
|
|
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
|
|
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" ?
|
|
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 ??
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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:
|
|
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:
|
|
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}: ${
|
|
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}: ${
|
|
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}: ${
|
|
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
|
|
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,
|
|
11337
|
-
return json(status, { jsonrpc: "2.0", error: { code: -32e3, message:
|
|
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
|
|
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 ?? "");
|