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