xaq-cli 0.0.0 → 0.1.0-nightly.71
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/LICENSE +21 -0
- package/README.md +79 -0
- package/dist/admin-5gThRwgj.js +162 -0
- package/dist/admin-5gThRwgj.js.map +1 -0
- package/dist/admin-Btrm8mCd.js +218 -0
- package/dist/admin-Btrm8mCd.js.map +1 -0
- package/dist/admin-CWofnT_Q.js +2 -0
- package/dist/api-BFpElwqK.js +89 -0
- package/dist/api-BFpElwqK.js.map +1 -0
- package/dist/audit-BB7jK9Z5.js +2 -0
- package/dist/audit-BVYknW2i.js +79 -0
- package/dist/audit-BVYknW2i.js.map +1 -0
- package/dist/auth-CXmBdMqw.js +2 -0
- package/dist/auth-D-GmwNv-.js +550 -0
- package/dist/auth-D-GmwNv-.js.map +1 -0
- package/dist/brand--ZB8jTn1.js +32 -0
- package/dist/brand--ZB8jTn1.js.map +1 -0
- package/dist/bus-Cz0flS37.js +12 -0
- package/dist/bus-Cz0flS37.js.map +1 -0
- package/dist/bus-OKIGut72.js +2 -0
- package/dist/cli.js +234 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands-BNVLjYXA.js +167 -0
- package/dist/commands-BNVLjYXA.js.map +1 -0
- package/dist/commands-CdA6-WE7.js +112 -0
- package/dist/commands-CdA6-WE7.js.map +1 -0
- package/dist/config-C9XmYZWO.js +94 -0
- package/dist/config-C9XmYZWO.js.map +1 -0
- package/dist/config-CNVi0v5t.js +2 -0
- package/dist/connect-TtJ9n0Rw.js +362 -0
- package/dist/connect-TtJ9n0Rw.js.map +1 -0
- package/dist/content-C1lKMxFS.js +1170 -0
- package/dist/content-C1lKMxFS.js.map +1 -0
- package/dist/content-worker.js +28 -0
- package/dist/content-worker.js.map +1 -0
- package/dist/crypto-fv2fj3S8.js +75 -0
- package/dist/crypto-fv2fj3S8.js.map +1 -0
- package/dist/db-BEUEUPyP.js +561 -0
- package/dist/db-BEUEUPyP.js.map +1 -0
- package/dist/db-Q5BQuwga.js +2 -0
- package/dist/errors-FbMS3QBo.js +39 -0
- package/dist/errors-FbMS3QBo.js.map +1 -0
- package/dist/filter-Ck6OMxvu.js +158 -0
- package/dist/filter-Ck6OMxvu.js.map +1 -0
- package/dist/hermes-CKyzt1mH.js +3 -0
- package/dist/hermes-DHnDTYJp.js +1023 -0
- package/dist/hermes-DHnDTYJp.js.map +1 -0
- package/dist/import-DsN5NZT3.js +910 -0
- package/dist/import-DsN5NZT3.js.map +1 -0
- package/dist/instance-CwWbFbJ8.js +2 -0
- package/dist/instance-Dj9TpJB2.js +195 -0
- package/dist/instance-Dj9TpJB2.js.map +1 -0
- package/dist/listen-B9iO7HgQ.js +1568 -0
- package/dist/listen-B9iO7HgQ.js.map +1 -0
- package/dist/listen-CXVKG9XD.js +2 -0
- package/dist/manage-Bo6COB4p.js +477 -0
- package/dist/manage-Bo6COB4p.js.map +1 -0
- package/dist/manage-DoXqvjnU.js +2 -0
- package/dist/mcpheader-B8c5YO1B.js +2 -0
- package/dist/mcpheader-C9H4vudu.js +185 -0
- package/dist/mcpheader-C9H4vudu.js.map +1 -0
- package/dist/mcpsetup-BnHudwpq.js +2 -0
- package/dist/mcpsetup-DXXo2qjf.js +1053 -0
- package/dist/mcpsetup-DXXo2qjf.js.map +1 -0
- package/dist/node-check-CjTk6VNL.js +24 -0
- package/dist/node-check-CjTk6VNL.js.map +1 -0
- package/dist/paths-d-C2kdjo.js +2 -0
- package/dist/paths-yzOKD6ow.js +46 -0
- package/dist/paths-yzOKD6ow.js.map +1 -0
- package/dist/profile-CcMZ4hn7.js +2 -0
- package/dist/profile-NmMKAczz.js +222 -0
- package/dist/profile-NmMKAczz.js.map +1 -0
- package/dist/prompt-CKMQx_MJ.js +114 -0
- package/dist/prompt-CKMQx_MJ.js.map +1 -0
- package/dist/recipes-C7TIDlRj.js +3 -0
- package/dist/recipes-DG_-doGq.js +314 -0
- package/dist/recipes-DG_-doGq.js.map +1 -0
- package/dist/send-Dk-t8ia-.js +108 -0
- package/dist/send-Dk-t8ia-.js.map +1 -0
- package/dist/server-CDqDp1q3.js +20640 -0
- package/dist/server-CDqDp1q3.js.map +1 -0
- package/dist/service-BVn3soZZ.js +766 -0
- package/dist/service-BVn3soZZ.js.map +1 -0
- package/dist/service-BaWGPmSd.js +2 -0
- package/dist/service-C7jfo-Ex.js +457 -0
- package/dist/service-C7jfo-Ex.js.map +1 -0
- package/dist/service-DxDyKDQ-.js +2 -0
- package/dist/settings-B5qSHxRM.js +134 -0
- package/dist/settings-B5qSHxRM.js.map +1 -0
- package/dist/settings-DQvZaMN1.js +2 -0
- package/dist/setup-token-CwuSzfLK.js +48 -0
- package/dist/setup-token-CwuSzfLK.js.map +1 -0
- package/dist/skill/SKILL.md +445 -0
- package/dist/skill-1UQj3qgW.js +69 -0
- package/dist/skill-1UQj3qgW.js.map +1 -0
- package/dist/skill-pelq18JQ.js +2 -0
- package/dist/ssrf-BlVpGZ7C.js +181 -0
- package/dist/ssrf-BlVpGZ7C.js.map +1 -0
- package/dist/types-BhJDbSzE.js +135 -0
- package/dist/types-BhJDbSzE.js.map +1 -0
- package/dist/ui-B8Sd4Y7Y.js +81 -0
- package/dist/ui-B8Sd4Y7Y.js.map +1 -0
- package/dist/urls-Dp8pBGMU.js +2 -0
- package/dist/urls-U_LfYV61.js +27 -0
- package/dist/urls-U_LfYV61.js.map +1 -0
- package/dist/version-BxH7OuWD.js +20 -0
- package/dist/version-BxH7OuWD.js.map +1 -0
- package/dist/web/assets/_agentId-DonOCOlC.js +1 -0
- package/dist/web/assets/_app-B7t5lEia.js +1 -0
- package/dist/web/assets/_app-DMTGqYpM.js +65 -0
- package/dist/web/assets/_connectionId-DR5vFXOg.js +1 -0
- package/dist/web/assets/_webhookId-LfUlsULp.js +1 -0
- package/dist/web/assets/account-status-CEszs13C.js +1 -0
- package/dist/web/assets/accounts-5m7aiXuZ.js +1 -0
- package/dist/web/assets/accounts-list-YfJzThZa.js +1 -0
- package/dist/web/assets/activity-DOIe7SAg.js +1 -0
- package/dist/web/assets/add-connection-C8JR-13R.js +1 -0
- package/dist/web/assets/agent-create-form-CPxnPYH4.js +1 -0
- package/dist/web/assets/agents-B2Qj5KxT.js +1 -0
- package/dist/web/assets/approvals-BGaVaoFy.js +1 -0
- package/dist/web/assets/approve._id-BLrQnzPM.js +1 -0
- package/dist/web/assets/arrow-left-DZVAXoKj.js +1 -0
- package/dist/web/assets/arrow-right-C2zcqRr2.js +1 -0
- package/dist/web/assets/attention-gO9Nw5uW.js +1 -0
- package/dist/web/assets/audit-BPXXre8S.js +1 -0
- package/dist/web/assets/audit-bits-CGyIprzK.js +1 -0
- package/dist/web/assets/bell-ring-ig3zn1iS.js +1 -0
- package/dist/web/assets/bot-Zk76q_39.js +1 -0
- package/dist/web/assets/calendar-notice-DsCfq3VJ.js +1 -0
- package/dist/web/assets/calendar-plus-Cycfkk0y.js +1 -0
- package/dist/web/assets/calendar-setup-Pn61Wj1n.js +2 -0
- package/dist/web/assets/card-GSO5o-Ug.js +1 -0
- package/dist/web/assets/check-check-Ds1bPIGv.js +1 -0
- package/dist/web/assets/checkbox-B937U-Fd.js +1 -0
- package/dist/web/assets/chevron-right-Cqh4mDk1.js +1 -0
- package/dist/web/assets/chips-input-2xWDofmE.js +1 -0
- package/dist/web/assets/choice-cards-CgAIqBGX.js +1 -0
- package/dist/web/assets/circle-x-DcU-0zVh.js +1 -0
- package/dist/web/assets/command-khhto70y.js +1 -0
- package/dist/web/assets/common-BPZV8TFb.js +1 -0
- package/dist/web/assets/connect-agent-guide-CJdw31p2.js +1 -0
- package/dist/web/assets/connect-host-lr2c3xWh.js +6 -0
- package/dist/web/assets/connection-row-DYx_5o0c.js +1 -0
- package/dist/web/assets/connections-C4NOFb5R.js +1 -0
- package/dist/web/assets/data-table-kW1pkRB5.js +1 -0
- package/dist/web/assets/empty-DzTDioBP.js +1 -0
- package/dist/web/assets/events-C9zJnPKK.js +1 -0
- package/dist/web/assets/eye-B9gT1QSq.js +1 -0
- package/dist/web/assets/eye-off-Diktqz_g.js +1 -0
- package/dist/web/assets/field-C6ejurju.js +1 -0
- package/dist/web/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/web/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/web/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/web/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/web/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/dist/web/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/dist/web/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/dist/web/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/dist/web/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/dist/web/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/dist/web/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/web/assets/get-started-C38bq1gZ.js +2 -0
- package/dist/web/assets/hidden-chars-Dr9TxfvF.js +1 -0
- package/dist/web/assets/index-DGzo3kGh.js +11 -0
- package/dist/web/assets/index-zKOCQLGX.css +2 -0
- package/dist/web/assets/input-CWwJgozn.js +1 -0
- package/dist/web/assets/input-group-HG8OXcJ4.js +1 -0
- package/dist/web/assets/label-multi-select-sH8mllw8.js +1 -0
- package/dist/web/assets/labels-B5UqVwFo.js +1 -0
- package/dist/web/assets/laptop-BxJ4Nbj7.js +1 -0
- package/dist/web/assets/last-call-D-O5O94z.js +1 -0
- package/dist/web/assets/listener-handled-HNCuVrQ7.js +1 -0
- package/dist/web/assets/login-DaeKclcf.js +1 -0
- package/dist/web/assets/mail-C5fm2pTC.js +1 -0
- package/dist/web/assets/mcp-call-UoOTPFyh.js +1 -0
- package/dist/web/assets/mcp-call-card-Cr-OEDX2.js +3 -0
- package/dist/web/assets/message-square-quote-se8Bf3RP.js +1 -0
- package/dist/web/assets/mutations-CLPqJ6cj.js +1 -0
- package/dist/web/assets/network-igNw0VVx.js +1 -0
- package/dist/web/assets/pairing-XjuaVr2u.js +1 -0
- package/dist/web/assets/pairing-notifier-KPzOEZSO.js +1 -0
- package/dist/web/assets/paperclip-znS4iTrg.js +1 -0
- package/dist/web/assets/password-input-DV3aqcNf.js +1 -0
- package/dist/web/assets/pause-o-dK3khK.js +1 -0
- package/dist/web/assets/pencil-DkPFAhEq.js +1 -0
- package/dist/web/assets/play-DvCHND93.js +1 -0
- package/dist/web/assets/plug-CXAniEiR.js +1 -0
- package/dist/web/assets/plug-zap-BwR-l5wb.js +1 -0
- package/dist/web/assets/plus-B_TUslu3.js +1 -0
- package/dist/web/assets/preload-helper-Cc_aKyUb.js +1 -0
- package/dist/web/assets/public-url-rF2Xjyel.js +1 -0
- package/dist/web/assets/queries-Cv3nXx0U.js +42 -0
- package/dist/web/assets/refresh-cw-Ch5FNrby.js +1 -0
- package/dist/web/assets/revealed-keys-C_9NtJPY.js +1 -0
- package/dist/web/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- package/dist/web/assets/rotate-ccw-B-mzPDk0.js +1 -0
- package/dist/web/assets/search-CTVKGomS.js +1 -0
- package/dist/web/assets/select-Bb806bpq.js +1 -0
- package/dist/web/assets/send-DuMVuYs1.js +1 -0
- package/dist/web/assets/settings-BWauhfFf.js +1 -0
- package/dist/web/assets/setup-B7bNKQOp.js +1 -0
- package/dist/web/assets/sheet-CYPwpUsH.js +1 -0
- package/dist/web/assets/shield-alert-CSYB-ThV.js +1 -0
- package/dist/web/assets/shield-check-BulpsYqK.js +1 -0
- package/dist/web/assets/sparkles-CUTiob4K.js +1 -0
- package/dist/web/assets/stamp-BNXUGNa3.js +1 -0
- package/dist/web/assets/step-url-51aMNak-.js +1 -0
- package/dist/web/assets/stream-Du7fY5_Y.js +1 -0
- package/dist/web/assets/tag-I5_-1jFL.js +1 -0
- package/dist/web/assets/terminal-BvxDFTJc.js +1 -0
- package/dist/web/assets/test-email-button-DYi7V_6X.js +1 -0
- package/dist/web/assets/toggle-group-Bdo1gmQ1.js +1 -0
- package/dist/web/assets/trash-Ci0aICEl.js +1 -0
- package/dist/web/assets/use-delivery-speed-BBh7W2q6.js +1 -0
- package/dist/web/assets/use-label-names-BXicesnP.js +1 -0
- package/dist/web/assets/use-sign-out-CPrY_oVO.js +1 -0
- package/dist/web/assets/useInfiniteQuery-DJLIM8yJ.js +1 -0
- package/dist/web/assets/useQuery-MrTwwNy5.js +1 -0
- package/dist/web/assets/useRouterState-D4OBK8l5.js +1 -0
- package/dist/web/assets/webhook-DjeeZiNC.js +1 -0
- package/dist/web/assets/webhook-bits-jw-y2D3u.js +1 -0
- package/dist/web/assets/webhook-sheet-B8h0skDe.js +34 -0
- package/dist/web/assets/webhooks-Cfsdpoyd.js +1 -0
- package/dist/web/assets/x-MF2TZ9aG.js +1 -0
- package/dist/web/favicon.svg +1 -0
- package/dist/web/index.html +27 -0
- package/package.json +98 -1
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { t as NPX } from "./version-BxH7OuWD.js";
|
|
3
|
+
import { a as mcpServerName, n as loadProfile, r as profileExists } from "./profile-NmMKAczz.js";
|
|
4
|
+
import { c as profilePath } from "./paths-yzOKD6ow.js";
|
|
5
|
+
import "./recipes-DG_-doGq.js";
|
|
6
|
+
import { i as safeText } from "./ui-B8Sd4Y7Y.js";
|
|
7
|
+
import { _ as readTomlMcpServers, a as cursorConfigFile, c as mcpTarget, g as UNREADABLE$1, h as readTopMap, i as codexConfigFile, m as readMcpServers, n as claudeConfigFile, p as UNREADABLE, s as hermesHome, u as setupMcp } from "./mcpsetup-DXXo2qjf.js";
|
|
8
|
+
import { _ as chooseAccess, a as isWorking, d as waitWorking, f as reconnectHint, g as agentsLabel, h as accessLabel, m as checkTarget, p as slugify, v as chooseAgents, y as parseAccess } from "./manage-Bo6COB4p.js";
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import pc from "picocolors";
|
|
12
|
+
//#region server/connections-cli/routes.ts
|
|
13
|
+
/**
|
|
14
|
+
* Places in a client's config that confine something to the xaq MCP server alone: Hermes platform toolsets,
|
|
15
|
+
* webhook routes (`toolsets: ["mcp-<server>", …]`) and cron jobs (`enabled_toolsets: ["<server>"]`). Once the
|
|
16
|
+
* imported servers are served through xaq, each of these also sees their tools, so `import` warns before
|
|
17
|
+
* replacing anything (docs/listen.md: an email-triggered route confined to xaq).
|
|
18
|
+
*/
|
|
19
|
+
function confinedRoutes(client, serverName, env = process.env) {
|
|
20
|
+
if (client !== "hermes") return [];
|
|
21
|
+
const home = hermesHome(env);
|
|
22
|
+
const names = /* @__PURE__ */ new Set([serverName, `mcp-${serverName}`]);
|
|
23
|
+
const confines = (v) => Array.isArray(v) && v.some((x) => typeof x === "string" && names.has(x));
|
|
24
|
+
const out = [];
|
|
25
|
+
const read = (file) => {
|
|
26
|
+
try {
|
|
27
|
+
return existsSync(file) ? JSON.parse(readFileSync(file, "utf8").replace(/^/, "")) : null;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
const config = join(home, "config.yaml");
|
|
34
|
+
if (existsSync(config)) {
|
|
35
|
+
const yaml = readFileSync(config, "utf8");
|
|
36
|
+
const platforms = readTopMap(yaml, "platform_toolsets") ?? {};
|
|
37
|
+
for (const [platform, toolsets] of Object.entries(platforms)) if (confines(toolsets)) out.push(`platform_toolsets.${platform} (config.yaml)`);
|
|
38
|
+
const get = (v, k) => v && typeof v === "object" && !Array.isArray(v) ? v[k] : void 0;
|
|
39
|
+
const routes = get(get(get(readTopMap(yaml, "platforms") ?? void 0, "webhook"), "extra"), "routes");
|
|
40
|
+
if (routes === UNREADABLE || Array.isArray(routes) && routes.some((r) => r === UNREADABLE)) out.push("webhook routes in config.yaml (platforms.webhook.extra.routes: couldn't be read, so check their toolsets)");
|
|
41
|
+
else if (Array.isArray(routes)) routes.forEach((r, i) => {
|
|
42
|
+
if (confines(get(r, "toolsets"))) out.push(`webhook route ${String(get(r, "name") ?? `#${i + 1}`)} (config.yaml)`);
|
|
43
|
+
});
|
|
44
|
+
else if (routes && typeof routes === "object") {
|
|
45
|
+
for (const [name, r] of Object.entries(routes)) if (confines(get(r, "toolsets"))) out.push(`webhook route ${name} (config.yaml)`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
const subs = read(join(home, "webhook_subscriptions.json"));
|
|
50
|
+
if (subs && typeof subs === "object" && !Array.isArray(subs)) {
|
|
51
|
+
for (const [route, r] of Object.entries(subs)) if (confines(r?.toolsets)) out.push(`webhook route ${route}`);
|
|
52
|
+
}
|
|
53
|
+
const cron = read(join(home, "cron", "jobs.json"));
|
|
54
|
+
const jobs = Array.isArray(cron) ? cron : cron?.jobs ?? [];
|
|
55
|
+
if (Array.isArray(jobs)) {
|
|
56
|
+
for (const j of jobs) if (confines(j?.enabled_toolsets)) out.push(`cron job ${String(j.name ?? j.id ?? "?")}`);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region server/connections-cli/clients.ts
|
|
62
|
+
/** Reads the MCP servers an agent's own tool is configured with (for `import --from`). */
|
|
63
|
+
const IMPORT_CLIENTS = [
|
|
64
|
+
"hermes",
|
|
65
|
+
"claude-code",
|
|
66
|
+
"cursor",
|
|
67
|
+
"codex"
|
|
68
|
+
];
|
|
69
|
+
const CLIENT_LABELS = {
|
|
70
|
+
hermes: "Hermes",
|
|
71
|
+
"claude-code": "Claude Code",
|
|
72
|
+
cursor: "Cursor",
|
|
73
|
+
codex: "Codex"
|
|
74
|
+
};
|
|
75
|
+
const strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : void 0;
|
|
76
|
+
function droppedFrom(e, filters, settingKeys) {
|
|
77
|
+
const d = { settings: settingKeys.filter((k) => e[k] !== void 0 && e[k] !== null) };
|
|
78
|
+
const inc = strings(filters.include);
|
|
79
|
+
const exc = strings(filters.exclude);
|
|
80
|
+
if (inc?.length) d.include = inc;
|
|
81
|
+
if (exc?.length) d.exclude = exc;
|
|
82
|
+
return d.include || d.exclude || d.settings.length ? d : void 0;
|
|
83
|
+
}
|
|
84
|
+
/** Parses a dotenv file (KEY=VALUE, optional `export`, quotes). */
|
|
85
|
+
function parseDotenv(text) {
|
|
86
|
+
const out = {};
|
|
87
|
+
for (const raw of text.split("\n")) {
|
|
88
|
+
const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(raw);
|
|
89
|
+
if (!m) continue;
|
|
90
|
+
let v = m[2];
|
|
91
|
+
if (v.startsWith("\"") && v.endsWith("\"") && v.length >= 2 || v.startsWith("'") && v.endsWith("'") && v.length >= 2) v = v.slice(1, -1);
|
|
92
|
+
else v = v.replace(/\s+#.*$/, "");
|
|
93
|
+
out[m[1]] = v;
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
/** `${VAR}` (and `${VAR:-default}`, `${env:VAR}`) from `vars`. */
|
|
98
|
+
function expandVars(value, vars) {
|
|
99
|
+
const used = [];
|
|
100
|
+
let missing = null;
|
|
101
|
+
const out = value.replace(/\$\{(?:env:)?([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g, (_m, name, def) => {
|
|
102
|
+
const v = vars[name];
|
|
103
|
+
if (v !== void 0 && v !== "") {
|
|
104
|
+
used.push(name);
|
|
105
|
+
return v;
|
|
106
|
+
}
|
|
107
|
+
if (def !== void 0) return def;
|
|
108
|
+
missing ??= name;
|
|
109
|
+
return "";
|
|
110
|
+
});
|
|
111
|
+
return missing ? { missing } : {
|
|
112
|
+
value: out,
|
|
113
|
+
vars: used
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
117
|
+
const readJson = (file) => {
|
|
118
|
+
if (!existsSync(file)) return null;
|
|
119
|
+
const text = readFileSync(file, "utf8");
|
|
120
|
+
if (!text.trim()) return {};
|
|
121
|
+
try {
|
|
122
|
+
const doc = JSON.parse(text);
|
|
123
|
+
if (!isObj(doc)) throw new Error("not an object");
|
|
124
|
+
return doc;
|
|
125
|
+
} catch {
|
|
126
|
+
throw new Error(`${file} isn't plain JSON (comments?), so it can't be read automatically.`);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
/** Shared by the JSON-shaped clients (Claude Code, Cursor): one `mcpServers` entry. */
|
|
130
|
+
function jsonEntry(name, raw, file, vars, extra = {}) {
|
|
131
|
+
const base = {
|
|
132
|
+
name,
|
|
133
|
+
file,
|
|
134
|
+
transport: "http",
|
|
135
|
+
headers: {},
|
|
136
|
+
headerVars: [],
|
|
137
|
+
headerSources: {},
|
|
138
|
+
oauth: false,
|
|
139
|
+
enabled: true,
|
|
140
|
+
...extra
|
|
141
|
+
};
|
|
142
|
+
if (!isObj(raw)) return {
|
|
143
|
+
...base,
|
|
144
|
+
problem: "this entry isn't an object"
|
|
145
|
+
};
|
|
146
|
+
if (raw.disabled === true || raw.enabled === false) base.enabled = false;
|
|
147
|
+
const type = typeof raw.type === "string" ? raw.type.toLowerCase() : null;
|
|
148
|
+
if (type === "stdio" || typeof raw.command === "string" && !raw.url) {
|
|
149
|
+
const args = Array.isArray(raw.args) ? raw.args.map(String) : [];
|
|
150
|
+
return {
|
|
151
|
+
...base,
|
|
152
|
+
transport: "stdio",
|
|
153
|
+
command: [String(raw.command ?? ""), ...args].join(" ").trim()
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (typeof raw.url !== "string") return {
|
|
157
|
+
...base,
|
|
158
|
+
problem: "no url"
|
|
159
|
+
};
|
|
160
|
+
const url = expandVars(raw.url, vars);
|
|
161
|
+
if ("missing" in url) return {
|
|
162
|
+
...base,
|
|
163
|
+
problem: `its url uses \${${url.missing}}, which isn't set here`
|
|
164
|
+
};
|
|
165
|
+
const entry = {
|
|
166
|
+
...base,
|
|
167
|
+
url: url.value,
|
|
168
|
+
transport: type === "sse" ? "sse" : "http"
|
|
169
|
+
};
|
|
170
|
+
if (raw.headersHelper !== void 0 || raw.headers_helper !== void 0) return {
|
|
171
|
+
...entry,
|
|
172
|
+
problem: "it gets its headers from a helper command, which the broker can't run"
|
|
173
|
+
};
|
|
174
|
+
if (raw.oauth !== void 0 || raw.auth !== void 0) entry.oauth = true;
|
|
175
|
+
return withHeaders(entry, raw.headers, vars);
|
|
176
|
+
}
|
|
177
|
+
function withHeaders(entry, headers, vars) {
|
|
178
|
+
if (headers === void 0 || headers === null) return entry;
|
|
179
|
+
if (!isObj(headers)) return {
|
|
180
|
+
...entry,
|
|
181
|
+
problem: "its headers can't be read"
|
|
182
|
+
};
|
|
183
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
184
|
+
if (typeof v !== "string") return {
|
|
185
|
+
...entry,
|
|
186
|
+
problem: `its ${k} header can't be read`
|
|
187
|
+
};
|
|
188
|
+
const x = expandVars(v, vars);
|
|
189
|
+
if ("missing" in x) return {
|
|
190
|
+
...entry,
|
|
191
|
+
problem: `its ${k} header uses \${${x.missing}}, which isn't set here`
|
|
192
|
+
};
|
|
193
|
+
entry.headers[k] = x.value;
|
|
194
|
+
entry.headerVars.push(...x.vars);
|
|
195
|
+
entry.headerSources[k] = x.vars;
|
|
196
|
+
}
|
|
197
|
+
return entry;
|
|
198
|
+
}
|
|
199
|
+
function hermesFiles(env = process.env) {
|
|
200
|
+
const home = hermesHome(env);
|
|
201
|
+
return {
|
|
202
|
+
config: join(home, "config.yaml"),
|
|
203
|
+
env: join(home, ".env")
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function readHermes(opts) {
|
|
207
|
+
const env = opts.env ?? process.env;
|
|
208
|
+
const files = hermesFiles(env);
|
|
209
|
+
const result = {
|
|
210
|
+
client: "hermes",
|
|
211
|
+
files: [files.config],
|
|
212
|
+
entries: []
|
|
213
|
+
};
|
|
214
|
+
if (!existsSync(files.config)) return result;
|
|
215
|
+
const servers = readMcpServers(readFileSync(files.config, "utf8"));
|
|
216
|
+
if (!servers) throw new Error(`${files.config} has an mcp_servers layout this can't read (flow style, several blocks, or tabs). Nothing was changed.`);
|
|
217
|
+
const vars = {
|
|
218
|
+
...env,
|
|
219
|
+
...existsSync(files.env) ? parseDotenv(readFileSync(files.env, "utf8")) : {}
|
|
220
|
+
};
|
|
221
|
+
for (const [name, raw] of Object.entries(servers)) {
|
|
222
|
+
if (name.startsWith("\0")) continue;
|
|
223
|
+
const base = {
|
|
224
|
+
name,
|
|
225
|
+
file: files.config,
|
|
226
|
+
transport: "http",
|
|
227
|
+
headers: {},
|
|
228
|
+
headerVars: [],
|
|
229
|
+
headerSources: {},
|
|
230
|
+
oauth: false,
|
|
231
|
+
enabled: true
|
|
232
|
+
};
|
|
233
|
+
if (!raw || raw === UNREADABLE || typeof raw !== "object" || Array.isArray(raw)) {
|
|
234
|
+
result.entries.push({
|
|
235
|
+
...base,
|
|
236
|
+
problem: "this entry can't be read"
|
|
237
|
+
});
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const e = raw;
|
|
241
|
+
const unreadable = [
|
|
242
|
+
"url",
|
|
243
|
+
"headers",
|
|
244
|
+
"command",
|
|
245
|
+
"auth",
|
|
246
|
+
"transport",
|
|
247
|
+
"enabled"
|
|
248
|
+
].find((k) => e[k] === UNREADABLE);
|
|
249
|
+
if (unreadable) {
|
|
250
|
+
result.entries.push({
|
|
251
|
+
...base,
|
|
252
|
+
problem: `its ${unreadable} can't be read`
|
|
253
|
+
});
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (e.enabled === false || typeof e.enabled === "string" && [
|
|
257
|
+
"false",
|
|
258
|
+
"no",
|
|
259
|
+
"off",
|
|
260
|
+
"0"
|
|
261
|
+
].includes(e.enabled.toLowerCase())) base.enabled = false;
|
|
262
|
+
if (typeof e.command === "string") {
|
|
263
|
+
const args = Array.isArray(e.args) ? e.args.filter((a) => typeof a === "string") : [];
|
|
264
|
+
result.entries.push({
|
|
265
|
+
...base,
|
|
266
|
+
transport: "stdio",
|
|
267
|
+
command: [e.command, ...args].join(" ")
|
|
268
|
+
});
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (typeof e.url !== "string") {
|
|
272
|
+
result.entries.push({
|
|
273
|
+
...base,
|
|
274
|
+
problem: "no url"
|
|
275
|
+
});
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const url = expandVars(e.url, vars);
|
|
279
|
+
if ("missing" in url) {
|
|
280
|
+
result.entries.push({
|
|
281
|
+
...base,
|
|
282
|
+
problem: `its url uses \${${url.missing}}, which isn't set in ${files.env}`
|
|
283
|
+
});
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const tools = isObj(e.tools) ? e.tools : {};
|
|
287
|
+
const entry = {
|
|
288
|
+
...base,
|
|
289
|
+
url: url.value,
|
|
290
|
+
transport: e.transport === "sse" ? "sse" : "http",
|
|
291
|
+
oauth: e.auth === "oauth" || isObj(e.oauth),
|
|
292
|
+
dropped: droppedFrom(e, {
|
|
293
|
+
include: tools.include,
|
|
294
|
+
exclude: tools.exclude
|
|
295
|
+
}, ["timeout", "connect_timeout"])
|
|
296
|
+
};
|
|
297
|
+
if (e.client_cert !== void 0 || e.identity_header !== void 0) {
|
|
298
|
+
result.entries.push({
|
|
299
|
+
...entry,
|
|
300
|
+
problem: "it uses a client certificate or identity header, which the broker doesn't support yet"
|
|
301
|
+
});
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
result.entries.push(withHeaders(entry, e.headers ?? void 0, vars));
|
|
305
|
+
}
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
function readClaude(opts) {
|
|
309
|
+
const env = opts.env ?? process.env;
|
|
310
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
311
|
+
const userFile = claudeConfigFile();
|
|
312
|
+
const projectFile = join(cwd, ".mcp.json");
|
|
313
|
+
const result = {
|
|
314
|
+
client: "claude-code",
|
|
315
|
+
files: [userFile, projectFile].filter(existsSync),
|
|
316
|
+
entries: []
|
|
317
|
+
};
|
|
318
|
+
const seen = /* @__PURE__ */ new Set();
|
|
319
|
+
const add = (e) => {
|
|
320
|
+
if (seen.has(e.name)) return;
|
|
321
|
+
seen.add(e.name);
|
|
322
|
+
result.entries.push(e);
|
|
323
|
+
};
|
|
324
|
+
const doc = readJson(userFile);
|
|
325
|
+
const proj = doc && isObj(doc.projects) && isObj(doc.projects[cwd]) ? doc.projects[cwd] : {};
|
|
326
|
+
const local = proj.mcpServers;
|
|
327
|
+
if (isObj(local)) for (const [n, raw] of Object.entries(local)) add(jsonEntry(n, raw, userFile, env, {
|
|
328
|
+
scope: "local",
|
|
329
|
+
projectDir: cwd
|
|
330
|
+
}));
|
|
331
|
+
const project = readJson(projectFile);
|
|
332
|
+
if (project && isObj(project.mcpServers)) {
|
|
333
|
+
const list = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
334
|
+
const enabled = new Set(list(proj.enabledMcpjsonServers));
|
|
335
|
+
const disabled = new Set(list(proj.disabledMcpjsonServers));
|
|
336
|
+
const all = proj.enableAllProjectMcpServers === true;
|
|
337
|
+
for (const [n, raw] of Object.entries(project.mcpServers)) {
|
|
338
|
+
const e = jsonEntry(n, raw, projectFile, env, {
|
|
339
|
+
scope: "project",
|
|
340
|
+
projectDir: cwd,
|
|
341
|
+
fromProject: true
|
|
342
|
+
});
|
|
343
|
+
const why = disabled.has(n) ? "turned off for this project in Claude Code" : !all && !enabled.has(n) ? "not approved in Claude Code" : null;
|
|
344
|
+
if (why) result.entries.push({
|
|
345
|
+
...e,
|
|
346
|
+
problem: why
|
|
347
|
+
});
|
|
348
|
+
else add(e);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (doc && isObj(doc.mcpServers)) for (const [n, raw] of Object.entries(doc.mcpServers)) add(jsonEntry(n, raw, userFile, env, { scope: "user" }));
|
|
352
|
+
return result;
|
|
353
|
+
}
|
|
354
|
+
function readCursor(opts) {
|
|
355
|
+
const env = opts.env ?? process.env;
|
|
356
|
+
const file = cursorConfigFile();
|
|
357
|
+
const result = {
|
|
358
|
+
client: "cursor",
|
|
359
|
+
files: [file],
|
|
360
|
+
entries: []
|
|
361
|
+
};
|
|
362
|
+
const doc = readJson(file);
|
|
363
|
+
if (doc && isObj(doc.mcpServers)) for (const [n, raw] of Object.entries(doc.mcpServers)) result.entries.push(jsonEntry(n, raw, file, env));
|
|
364
|
+
return result;
|
|
365
|
+
}
|
|
366
|
+
function readCodex(opts) {
|
|
367
|
+
const env = opts.env ?? process.env;
|
|
368
|
+
const file = codexConfigFile();
|
|
369
|
+
const result = {
|
|
370
|
+
client: "codex",
|
|
371
|
+
files: [file],
|
|
372
|
+
entries: []
|
|
373
|
+
};
|
|
374
|
+
if (!existsSync(file)) return result;
|
|
375
|
+
for (const [name, raw] of Object.entries(readTomlMcpServers(readFileSync(file, "utf8")))) {
|
|
376
|
+
const base = {
|
|
377
|
+
name,
|
|
378
|
+
file,
|
|
379
|
+
transport: "http",
|
|
380
|
+
headers: {},
|
|
381
|
+
headerVars: [],
|
|
382
|
+
headerSources: {},
|
|
383
|
+
oauth: false,
|
|
384
|
+
enabled: true
|
|
385
|
+
};
|
|
386
|
+
if (raw === UNREADABLE$1) {
|
|
387
|
+
result.entries.push({
|
|
388
|
+
...base,
|
|
389
|
+
problem: "this entry can't be read"
|
|
390
|
+
});
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
const e = raw;
|
|
394
|
+
if (e.enabled === false) base.enabled = false;
|
|
395
|
+
if (typeof e.command === "string") {
|
|
396
|
+
const args = Array.isArray(e.args) ? e.args.filter((a) => typeof a === "string") : [];
|
|
397
|
+
result.entries.push({
|
|
398
|
+
...base,
|
|
399
|
+
transport: "stdio",
|
|
400
|
+
command: [e.command, ...args].join(" ")
|
|
401
|
+
});
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (typeof e.url !== "string") {
|
|
405
|
+
result.entries.push({
|
|
406
|
+
...base,
|
|
407
|
+
problem: "no url"
|
|
408
|
+
});
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const entry = {
|
|
412
|
+
...base,
|
|
413
|
+
url: e.url,
|
|
414
|
+
oauth: isObj(e.oauth) || e.auth === "oauth",
|
|
415
|
+
dropped: droppedFrom(e, {
|
|
416
|
+
include: e.enabled_tools,
|
|
417
|
+
exclude: e.disabled_tools
|
|
418
|
+
}, ["tool_timeout_sec", "startup_timeout_sec"])
|
|
419
|
+
};
|
|
420
|
+
if (e.http_headers_helper !== void 0) {
|
|
421
|
+
result.entries.push({
|
|
422
|
+
...entry,
|
|
423
|
+
problem: "it gets its headers from a helper command, which the broker can't run"
|
|
424
|
+
});
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const headers = {};
|
|
428
|
+
let problem;
|
|
429
|
+
if (e.http_headers !== void 0) {
|
|
430
|
+
if (!isObj(e.http_headers) || Object.values(e.http_headers).some((v) => typeof v !== "string")) problem = "its http_headers can't be read";
|
|
431
|
+
else {
|
|
432
|
+
Object.assign(headers, e.http_headers);
|
|
433
|
+
for (const h of Object.keys(e.http_headers)) entry.headerSources[h] = [];
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const fromEnv = isObj(e.env_http_headers) ? { ...e.env_http_headers } : {};
|
|
437
|
+
if (e.bearer_token_env_var !== void 0) fromEnv.Authorization = e.bearer_token_env_var;
|
|
438
|
+
for (const [h, varName] of Object.entries(fromEnv)) if (typeof varName !== "string") problem ??= `its ${h} header can't be read`;
|
|
439
|
+
else if (!env[varName]) problem ??= `its ${h} header comes from $${varName}, which isn't set here`;
|
|
440
|
+
else {
|
|
441
|
+
headers[h] = h === "Authorization" && e.bearer_token_env_var === varName ? `Bearer ${env[varName]}` : env[varName];
|
|
442
|
+
entry.headerVars.push(varName);
|
|
443
|
+
entry.headerSources[h] = [varName];
|
|
444
|
+
}
|
|
445
|
+
entry.headers = headers;
|
|
446
|
+
result.entries.push(problem ? {
|
|
447
|
+
...entry,
|
|
448
|
+
problem
|
|
449
|
+
} : entry);
|
|
450
|
+
}
|
|
451
|
+
return result;
|
|
452
|
+
}
|
|
453
|
+
function readClientConfig(client, opts = {}) {
|
|
454
|
+
switch (client) {
|
|
455
|
+
case "hermes": return readHermes(opts);
|
|
456
|
+
case "claude-code": return readClaude(opts);
|
|
457
|
+
case "cursor": return readCursor(opts);
|
|
458
|
+
case "codex": return readCodex(opts);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function parseImportClient(value) {
|
|
462
|
+
const v = value.trim().toLowerCase().replace(/^claude$/, "claude-code");
|
|
463
|
+
if (!IMPORT_CLIENTS.includes(v)) throw new Error(`Unknown --from "${value}". Choose one of: ${IMPORT_CLIENTS.join(", ")}.`);
|
|
464
|
+
return v;
|
|
465
|
+
}
|
|
466
|
+
//#endregion
|
|
467
|
+
//#region server/connections-cli/import.ts
|
|
468
|
+
const SELF_NAME = /^(?:xaq|agentinbox)(?:-[a-z0-9][a-z0-9_-]*)?$/;
|
|
469
|
+
function isSelf(e, selfOrigins) {
|
|
470
|
+
if (SELF_NAME.test(e.name)) return true;
|
|
471
|
+
if (Object.values(e.headers).some((v) => /^Bearer\s+aib_/i.test(v.trim()))) return true;
|
|
472
|
+
if (!e.url) return false;
|
|
473
|
+
try {
|
|
474
|
+
const u = new URL(e.url);
|
|
475
|
+
return u.pathname.replace(/\/+$/, "").endsWith("/mcp") && selfOrigins.includes(u.origin);
|
|
476
|
+
} catch {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const sameUrl = (a, b) => {
|
|
481
|
+
try {
|
|
482
|
+
return !!a && new URL(a).href.replace(/\/+$/, "") === new URL(b).href.replace(/\/+$/, "");
|
|
483
|
+
} catch {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
async function planImport(config, ctx = {}) {
|
|
488
|
+
const label = CLIENT_LABELS[config.client];
|
|
489
|
+
const taken = new Set((ctx.existing ?? []).map((c) => c.slug));
|
|
490
|
+
const items = [];
|
|
491
|
+
for (const entry of config.entries) {
|
|
492
|
+
if (isSelf(entry, ctx.selfOrigins ?? [])) {
|
|
493
|
+
items.push({
|
|
494
|
+
action: "self",
|
|
495
|
+
entry
|
|
496
|
+
});
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (entry.transport === "stdio") {
|
|
500
|
+
items.push({
|
|
501
|
+
action: "stdio",
|
|
502
|
+
entry
|
|
503
|
+
});
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (entry.problem) {
|
|
507
|
+
items.push({
|
|
508
|
+
action: "skip",
|
|
509
|
+
entry,
|
|
510
|
+
reason: entry.problem
|
|
511
|
+
});
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
if (!entry.enabled) {
|
|
515
|
+
items.push({
|
|
516
|
+
action: "skip",
|
|
517
|
+
entry,
|
|
518
|
+
reason: `it's turned off in ${label}`
|
|
519
|
+
});
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
let url;
|
|
523
|
+
try {
|
|
524
|
+
url = new URL(entry.url);
|
|
525
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("scheme");
|
|
526
|
+
} catch {
|
|
527
|
+
items.push({
|
|
528
|
+
action: "skip",
|
|
529
|
+
entry,
|
|
530
|
+
reason: "its url isn't an http(s) URL"
|
|
531
|
+
});
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
const headerNames = Object.keys(entry.headers);
|
|
535
|
+
if (headerNames.length > 1) {
|
|
536
|
+
items.push({
|
|
537
|
+
action: "skip",
|
|
538
|
+
entry,
|
|
539
|
+
reason: `it sends ${headerNames.length} headers (${headerNames.join(", ")}); a connection keeps one (not supported yet)`
|
|
540
|
+
});
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const existing = (ctx.existing ?? []).find((c) => sameUrl(c.url, url.href));
|
|
544
|
+
if (existing) {
|
|
545
|
+
items.push({
|
|
546
|
+
action: "exists",
|
|
547
|
+
entry,
|
|
548
|
+
connection: existing
|
|
549
|
+
});
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
const target = await checkTarget(url, ctx.lookup);
|
|
553
|
+
if (target.kind === "never") {
|
|
554
|
+
items.push({
|
|
555
|
+
action: "skip",
|
|
556
|
+
entry,
|
|
557
|
+
reason: target.why === "ipv6_literal" ? `the private-network allow takes a hostname or an IPv4 address, not ${target.address}` : `it's on ${target.address}, a link-local or cloud metadata address, which is never allowed`
|
|
558
|
+
});
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (target.kind === "public" && url.protocol === "http:") {
|
|
562
|
+
items.push({
|
|
563
|
+
action: "skip",
|
|
564
|
+
entry,
|
|
565
|
+
reason: "plain http to a public host isn't allowed; use its https URL"
|
|
566
|
+
});
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
let slug = slugify(entry.name);
|
|
570
|
+
for (let n = 2; taken.has(slug); n++) slug = `${slugify(entry.name).slice(0, 21)}-${n}`;
|
|
571
|
+
taken.add(slug);
|
|
572
|
+
const auth = headerNames.length ? {
|
|
573
|
+
type: "header",
|
|
574
|
+
name: headerNames[0],
|
|
575
|
+
value: entry.headers[headerNames[0]]
|
|
576
|
+
} : entry.oauth ? { type: "oauth" } : { type: "none" };
|
|
577
|
+
items.push({
|
|
578
|
+
action: "create",
|
|
579
|
+
entry,
|
|
580
|
+
slug,
|
|
581
|
+
url: entry.url.trim(),
|
|
582
|
+
auth,
|
|
583
|
+
privateTarget: target.kind === "private" ? target.origin : null
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
return items;
|
|
587
|
+
}
|
|
588
|
+
const where = (e) => e.scope ? ` (${e.scope} scope)` : "";
|
|
589
|
+
const hostOf = (url) => {
|
|
590
|
+
try {
|
|
591
|
+
return new URL(url).host;
|
|
592
|
+
} catch {
|
|
593
|
+
return url;
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
/** "its Authorization header (from $GITHUB_TOKEN) is sent to mcp.example.com": where each secret ends up. */
|
|
597
|
+
function headerLine(i) {
|
|
598
|
+
if (i.auth.type !== "header") return null;
|
|
599
|
+
const vars = i.entry.headerSources[i.auth.name] ?? [];
|
|
600
|
+
const from = vars.length ? ` (from ${vars.map((v) => `$${v}`).join(", ")})` : "";
|
|
601
|
+
return `its ${i.auth.name} header${from} moves into xaq (encrypted; never shown) and is sent to ${safeText(hostOf(i.url), 120)}`;
|
|
602
|
+
}
|
|
603
|
+
/** The plan in plain words. Never prints a header value. */
|
|
604
|
+
function renderPlan(config, items) {
|
|
605
|
+
const label = CLIENT_LABELS[config.client];
|
|
606
|
+
const out = [];
|
|
607
|
+
const width = Math.min(24, Math.max(4, ...items.map((i) => i.entry.name.length)));
|
|
608
|
+
const pad = (s) => s.padEnd(width);
|
|
609
|
+
const cont = " ".repeat(width + 4);
|
|
610
|
+
out.push(`${label}: ${config.files.join(", ") || "no config file"} lists ${items.length} MCP server${items.length === 1 ? "" : "s"}.`);
|
|
611
|
+
for (const i of items) {
|
|
612
|
+
const name = ` ${pad(safeText(i.entry.name, 40))} `;
|
|
613
|
+
switch (i.action) {
|
|
614
|
+
case "self":
|
|
615
|
+
out.push(`${name}${pc.dim("skip: this is xaq itself")}`);
|
|
616
|
+
break;
|
|
617
|
+
case "stdio":
|
|
618
|
+
out.push(`${name}${pc.yellow("not supported yet (M4)")}: a local command (${safeText(i.entry.command ?? "", 60)}); it stays in ${label}`);
|
|
619
|
+
break;
|
|
620
|
+
case "skip":
|
|
621
|
+
out.push(`${name}${pc.yellow("skip")}: ${i.reason}; it stays in ${label}`);
|
|
622
|
+
break;
|
|
623
|
+
case "exists":
|
|
624
|
+
if (isWorking(i.connection)) out.push(`${name}already connected: "${i.connection.slug}" (${i.connection.tools.total} tools)`);
|
|
625
|
+
else {
|
|
626
|
+
const why = i.entry.oauth || i.connection.authType === "oauth" || i.connection.status === "needs_reauth" ? "needs Connect" : `${i.connection.status}, ${i.connection.tools.total} tools`;
|
|
627
|
+
out.push(`${name}already a connection: "${i.connection.slug}", ${why}; it stays in ${label} until it works`);
|
|
628
|
+
out.push(`${cont}${reconnectHint(i.connection)}`);
|
|
629
|
+
}
|
|
630
|
+
out.push(...droppedLines(i.entry, cont));
|
|
631
|
+
break;
|
|
632
|
+
case "create": {
|
|
633
|
+
out.push(`${name}${pc.green("create")} connection "${i.slug}" → ${safeText(i.url, 120)}${where(i.entry)}`);
|
|
634
|
+
if (i.entry.fromProject) out.push(`${cont}${pc.yellow("from the project's .mcp.json")}: asked separately (--yes doesn't answer it)`);
|
|
635
|
+
const h = headerLine(i);
|
|
636
|
+
if (h) out.push(`${cont}${h}`);
|
|
637
|
+
else if (i.auth.type === "oauth") out.push(`${cont}OAuth: created needing Connect (${label} keeps its own tokens); sign in once with the link it prints`);
|
|
638
|
+
else out.push(`${cont}no credential in ${label}: connected as it is (if it turns out to use OAuth, it needs Connect)`);
|
|
639
|
+
if (i.privateTarget) out.push(`${cont}${pc.yellow("private network")}: needs the allow for exactly ${i.privateTarget}`);
|
|
640
|
+
out.push(...droppedLines(i.entry, cont));
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return out;
|
|
646
|
+
}
|
|
647
|
+
/** What the client did for this server that the broker doesn't: said in the plan, never silently dropped. */
|
|
648
|
+
function droppedLines(e, cont) {
|
|
649
|
+
const d = e.dropped;
|
|
650
|
+
if (!d) return [];
|
|
651
|
+
const out = [];
|
|
652
|
+
const list = (xs) => xs.map((x) => safeText(x, 64)).join(", ");
|
|
653
|
+
if (d.exclude) out.push(`${cont}${pc.yellow("not carried over")}: its tool filter hides ${list(d.exclude)}; through xaq every tool is listed (subject to the access level)`);
|
|
654
|
+
if (d.include) out.push(`${cont}${pc.yellow("not carried over")}: its tool filter shows only ${list(d.include)}; through xaq every tool is listed (subject to the access level)`);
|
|
655
|
+
if (d.settings.length) out.push(`${cont}${pc.yellow("not carried over")}: ${d.settings.join(", ")} (xaq uses its own 60 s per call)`);
|
|
656
|
+
return out;
|
|
657
|
+
}
|
|
658
|
+
const works = isWorking;
|
|
659
|
+
function setupLines(r) {
|
|
660
|
+
const out = [r.ok ? `${r.dryRun ? pc.cyan("○") : pc.green("✔")} ${r.message}` : `${pc.yellow("!")} ${r.message}`];
|
|
661
|
+
for (const l of r.changes ?? []) out.push(l.startsWith(" ") ? pc.gray(` ${l}`) : ` - ${l}`);
|
|
662
|
+
if (r.backup) out.push(pc.gray(` Backup: ${r.backup}`));
|
|
663
|
+
if (!r.ok) for (const l of r.manual ?? []) out.push(` ${l}`);
|
|
664
|
+
for (const n of r.notes ?? []) out.push(pc.dim(` ${n}`));
|
|
665
|
+
if (r.ok && r.verify?.length) out.push(` ${r.dryRun ? "Afterwards, check it with" : "Check it with"}: ${r.verify.map((v) => pc.cyan(v)).join("; ")}`);
|
|
666
|
+
return out;
|
|
667
|
+
}
|
|
668
|
+
const toRemove = (e) => ({
|
|
669
|
+
name: e.name,
|
|
670
|
+
scope: e.scope,
|
|
671
|
+
projectDir: e.scope === "user" ? void 0 : e.projectDir
|
|
672
|
+
});
|
|
673
|
+
async function runImport(flags, deps) {
|
|
674
|
+
const p = deps.prompter;
|
|
675
|
+
const client = parseImportClient(flags.from);
|
|
676
|
+
const label = CLIENT_LABELS[client];
|
|
677
|
+
const access = flags.access ? parseAccess(flags.access) : void 0;
|
|
678
|
+
const config = readClientConfig(client, {
|
|
679
|
+
env: deps.env,
|
|
680
|
+
cwd: deps.cwd
|
|
681
|
+
});
|
|
682
|
+
if (!config.entries.length) {
|
|
683
|
+
p.print(`${label} has no MCP servers configured (${config.files.join(", ") || "no config file"}). Nothing to import.`);
|
|
684
|
+
return 0;
|
|
685
|
+
}
|
|
686
|
+
const profileName = flags.profile ?? "default";
|
|
687
|
+
if (flags.dryRun) {
|
|
688
|
+
let existing;
|
|
689
|
+
try {
|
|
690
|
+
existing = await deps.openApi().list();
|
|
691
|
+
} catch {
|
|
692
|
+
existing = void 0;
|
|
693
|
+
}
|
|
694
|
+
const items = await planImport(config, {
|
|
695
|
+
existing,
|
|
696
|
+
lookup: deps.lookup,
|
|
697
|
+
selfOrigins: deps.selfOrigins
|
|
698
|
+
});
|
|
699
|
+
p.print(pc.bold("Dry run: nothing is created or changed."));
|
|
700
|
+
for (const l of renderPlan(config, items)) p.print(l);
|
|
701
|
+
const creates = items.filter((i) => i.action === "create");
|
|
702
|
+
const priv = creates.filter((i) => i.privateTarget);
|
|
703
|
+
p.print("");
|
|
704
|
+
p.print(`Would create ${creates.length} connection${creates.length === 1 ? "" : "s"} with ${access ? accessLabel(access) : "the access you choose (default: Ask before risky actions)"}.`);
|
|
705
|
+
if (priv.length) p.print(`Would ask before allowing ${priv.map((i) => i.privateTarget).join(", ")} (--yes never does; --allow-private <origin> does for that origin).`);
|
|
706
|
+
if (!existing) p.print(pc.dim("xaq wasn't reachable, so existing connections weren't checked (the real run does: nothing is created twice)."));
|
|
707
|
+
const removable = [...items.filter((i) => i.action === "exists" && works(i.connection)).map((i) => toRemove(i.entry)), ...creates.filter((i) => i.auth.type !== "oauth").map((i) => toRemove(i.entry))];
|
|
708
|
+
p.print("");
|
|
709
|
+
if (!removable.length) p.print(`${label}'s config isn't changed.`);
|
|
710
|
+
else if (!profileExists(profileName)) p.print(`Afterwards it offers to replace the imported entries in ${label} with the single xaq entry, once this machine's ${label} is paired (${NPX} connect <xaq-url> --tool ${client}).`);
|
|
711
|
+
else {
|
|
712
|
+
p.print(`Afterwards it offers setup-mcp --tool ${client}, which would (for the entries whose connection works and that ${label}'s agent can use; OAuth ones after you Reconnect them):`);
|
|
713
|
+
const r = setupMcp(client, mcpTarget(profileName, loadProfile(profileName)), profilePath(profileName), {
|
|
714
|
+
dryRun: true,
|
|
715
|
+
removeServers: removable
|
|
716
|
+
});
|
|
717
|
+
for (const l of setupLines(r)) p.print(l);
|
|
718
|
+
}
|
|
719
|
+
return 0;
|
|
720
|
+
}
|
|
721
|
+
const api = deps.openApi();
|
|
722
|
+
const items = await planImport(config, {
|
|
723
|
+
existing: await api.list(),
|
|
724
|
+
lookup: deps.lookup,
|
|
725
|
+
selfOrigins: deps.selfOrigins
|
|
726
|
+
});
|
|
727
|
+
for (const l of renderPlan(config, items)) p.print(l);
|
|
728
|
+
let creates = items.filter((i) => i.action === "create");
|
|
729
|
+
const already = items.filter((i) => i.action === "exists");
|
|
730
|
+
p.print("");
|
|
731
|
+
const fromProject = creates.filter((i) => i.entry.fromProject);
|
|
732
|
+
if (fromProject.length) {
|
|
733
|
+
p.print(pc.yellow(`These come from ${fromProject[0].entry.file}, which ships with the project (anyone who can commit to it wrote them):`));
|
|
734
|
+
for (const i of fromProject) p.print(` ${safeText(i.entry.name, 40)} → ${safeText(i.url, 120)}${headerLine(i) ? `; ${headerLine(i)}` : ""}`);
|
|
735
|
+
if (!await p.confirmExplicit(`Import ${fromProject.length === 1 ? "it" : `these ${fromProject.length}`} into xaq?`, "they stay in Claude Code")) creates = creates.filter((i) => !i.entry.fromProject);
|
|
736
|
+
}
|
|
737
|
+
const preAllowed = new Set(flags.allowPrivate ?? []);
|
|
738
|
+
const priv = creates.filter((i) => i.privateTarget && !preAllowed.has(i.privateTarget));
|
|
739
|
+
for (const i of creates.filter((x) => x.privateTarget && preAllowed.has(x.privateTarget))) p.print(`Allowing ${i.privateTarget} (${safeText(i.entry.name, 40)}): --allow-private.`);
|
|
740
|
+
if (priv.length) {
|
|
741
|
+
p.print(pc.yellow("These servers are on this machine or your private network:"));
|
|
742
|
+
for (const i of priv) p.print(` ${i.privateTarget} (${safeText(i.entry.name, 40)})`);
|
|
743
|
+
p.print(pc.dim("A connection to such a server can reach other services there too. Each allow covers exactly that scheme://host:port, for that connection only."));
|
|
744
|
+
const names = priv.map((i) => i.entry.name).join(", ");
|
|
745
|
+
if (!await p.confirmExplicit(`Allow xaq to connect to ${priv.length === 1 ? "it" : `these ${priv.length}`}?`, `skipping ${names} (pass --allow-private <origin> to allow one)`)) creates = creates.filter((i) => !priv.includes(i));
|
|
746
|
+
}
|
|
747
|
+
const who = await clientAgent(api, profileName);
|
|
748
|
+
if (!creates.length) {
|
|
749
|
+
p.print("Nothing to create.");
|
|
750
|
+
return offerSetup(deps, api, client, who, already.filter((a) => works(a.connection)).map((a) => ({
|
|
751
|
+
entry: a.entry,
|
|
752
|
+
connection: a.connection
|
|
753
|
+
})), already.filter((a) => !works(a.connection)).map((a) => a.entry.name), 0);
|
|
754
|
+
}
|
|
755
|
+
const level = await chooseAccess(p, access);
|
|
756
|
+
let agents = await chooseAgents(p, api, flags.agent);
|
|
757
|
+
if (agents !== "all" && who.agent && !agents.includes(who.agent.id)) {
|
|
758
|
+
p.print(pc.yellow(`${safeText(who.agent.name, 60)} (the agent ${label} on this machine uses) isn't among them, so ${label} couldn't use these through xaq.`));
|
|
759
|
+
if (await p.confirm(`Include ${safeText(who.agent.name, 60)}?`, true)) agents = [...agents, who.agent.id];
|
|
760
|
+
}
|
|
761
|
+
if (!await p.confirm(`Create ${creates.length} connection${creates.length === 1 ? "" : "s"} (${accessLabel(level)}, ${agentsLabel(agents)})?`, true)) {
|
|
762
|
+
p.print("Nothing was created.");
|
|
763
|
+
return 0;
|
|
764
|
+
}
|
|
765
|
+
const outcomes = [];
|
|
766
|
+
for (const item of creates) {
|
|
767
|
+
let created = null;
|
|
768
|
+
try {
|
|
769
|
+
created = await api.create({
|
|
770
|
+
slug: item.slug,
|
|
771
|
+
name: item.entry.name,
|
|
772
|
+
url: item.url,
|
|
773
|
+
levelDefault: level,
|
|
774
|
+
...agents === "all" ? {} : { agentIds: agents }
|
|
775
|
+
});
|
|
776
|
+
if (item.privateTarget) await api.allowPrivateTarget(created.id, true);
|
|
777
|
+
const r = await api.connect(created.id, item.auth.type === "header" ? { header: {
|
|
778
|
+
name: item.auth.name,
|
|
779
|
+
value: item.auth.value
|
|
780
|
+
} } : {});
|
|
781
|
+
if (r.status === "connected") {
|
|
782
|
+
const after = await waitWorking(api, r.connection, {
|
|
783
|
+
pollMs: deps.pollMs,
|
|
784
|
+
ms: deps.verifyMs
|
|
785
|
+
});
|
|
786
|
+
outcomes.push({
|
|
787
|
+
item,
|
|
788
|
+
status: works(after) ? "connected" : "unverified",
|
|
789
|
+
connection: after
|
|
790
|
+
});
|
|
791
|
+
} else if (r.status === "authorize") outcomes.push({
|
|
792
|
+
item,
|
|
793
|
+
status: "authorize",
|
|
794
|
+
connection: created,
|
|
795
|
+
authorizeUrl: r.authorizeUrl
|
|
796
|
+
});
|
|
797
|
+
else throw new Error(r.message);
|
|
798
|
+
} catch (err) {
|
|
799
|
+
if (created) await api.remove(created.id).catch(() => {});
|
|
800
|
+
outcomes.push({
|
|
801
|
+
item,
|
|
802
|
+
status: "failed",
|
|
803
|
+
error: err instanceof Error ? err.message : String(err)
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
p.print("");
|
|
808
|
+
for (const o of outcomes) {
|
|
809
|
+
const name = safeText(o.item.entry.name, 40);
|
|
810
|
+
if (o.status === "failed") p.print(`${pc.red("✘")} ${name}: ${safeText(o.error, 300)} (it stays in ${label})`);
|
|
811
|
+
else if (o.status === "connected") p.print(`${pc.green("✔")} ${name} → "${o.connection.slug}", ${o.connection.tools.total} tools`);
|
|
812
|
+
else if (o.status === "unverified") {
|
|
813
|
+
const c = o.connection;
|
|
814
|
+
p.print(`${pc.yellow("○")} ${name} → "${c.slug}": not working (${c.status}${c.statusReason ? `: ${safeText(c.statusReason, 200)}` : ""}, ${c.tools.total} tools), so it stays in ${label}.`);
|
|
815
|
+
p.print(` ${reconnectHint(c)} then run this import again.`);
|
|
816
|
+
} else {
|
|
817
|
+
p.print(`${pc.yellow("○")} ${name} → "${o.connection.slug}": needs Connect. Sign in (link valid 15 minutes):`);
|
|
818
|
+
p.print(` ${pc.cyan(o.authorizeUrl)}`);
|
|
819
|
+
p.print(` ${reconnectHint(o.connection)}`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
const failed = outcomes.filter((o) => o.status === "failed").length;
|
|
823
|
+
return offerSetup(deps, api, client, who, [...already.filter((a) => works(a.connection)).map((a) => ({
|
|
824
|
+
entry: a.entry,
|
|
825
|
+
connection: a.connection
|
|
826
|
+
})), ...outcomes.filter((o) => o.status === "connected").map((o) => ({
|
|
827
|
+
entry: o.item.entry,
|
|
828
|
+
connection: o.connection
|
|
829
|
+
}))], [...already.filter((a) => !works(a.connection)).map((a) => a.entry.name), ...outcomes.filter((o) => o.status === "authorize" || o.status === "unverified").map((o) => o.item.entry.name)], failed ? 1 : 0);
|
|
830
|
+
}
|
|
831
|
+
async function clientAgent(api, profileName) {
|
|
832
|
+
if (!profileExists(profileName)) return {
|
|
833
|
+
profileName,
|
|
834
|
+
profile: null,
|
|
835
|
+
agent: null
|
|
836
|
+
};
|
|
837
|
+
const profile = loadProfile(profileName);
|
|
838
|
+
return {
|
|
839
|
+
profileName,
|
|
840
|
+
profile,
|
|
841
|
+
agent: (await api.agents()).find((a) => a.id === profile.agentId && !a.disabled) ?? null
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Step 2: replace the entries now served by the broker with the single xaq entry (setup-mcp). Only entries
|
|
846
|
+
* the client's own agent can use through xaq go: the profile's agent must be on this daemon and have a grant
|
|
847
|
+
* on the connection (offered when it has none).
|
|
848
|
+
*/
|
|
849
|
+
async function offerSetup(deps, api, client, who, working, pending, code) {
|
|
850
|
+
const p = deps.prompter;
|
|
851
|
+
const label = CLIENT_LABELS[client];
|
|
852
|
+
if (pending.length) p.print(pc.dim(`${pending.join(", ")} stay${pending.length === 1 ? "s" : ""} in ${label} until ${pending.length === 1 ? "it works" : "they work"} through xaq (Reconnect); then run this import again to replace ${pending.length === 1 ? "it" : "them"}.`));
|
|
853
|
+
if (!working.length) return code;
|
|
854
|
+
if (!who.profile) {
|
|
855
|
+
p.print(`To replace ${working.map((w) => w.entry.name).join(", ")} in ${label} with the single xaq entry, pair it first (${NPX} connect <xaq-url> --tool ${client}) and run this import again.`);
|
|
856
|
+
return code;
|
|
857
|
+
}
|
|
858
|
+
if (!who.agent) {
|
|
859
|
+
p.print(pc.yellow(`The agent in your "${who.profileName}" profile (${safeText(who.profile.agentId, 40)}) isn't an agent of this xaq (or is disabled), so ${label} couldn't use these through it. Not replacing anything in ${label}. Pair it with this xaq (${NPX} connect <xaq-url> --tool ${client} --force), then run this import again.`));
|
|
860
|
+
return code;
|
|
861
|
+
}
|
|
862
|
+
const agentName = safeText(who.agent.name, 60);
|
|
863
|
+
const usable = [];
|
|
864
|
+
for (const w of working) {
|
|
865
|
+
if (w.entry.fromProject) {
|
|
866
|
+
p.print(pc.dim(` "${w.entry.name}" stays in ${w.entry.file}: it ships with the project. Remove it there if the project no longer needs it.`));
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
const grants = await api.grants(w.connection.id);
|
|
870
|
+
if (grants.some((g) => g.agentId === who.agent.id)) {
|
|
871
|
+
usable.push(w.entry);
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
if (await p.confirm(`${agentName} (the agent ${label} uses) has no access to "${w.connection.slug}". Give it ${accessLabel(w.connection.levelDefault)}?`, true)) {
|
|
875
|
+
await api.setGrants(w.connection.id, [...grants.map((g) => ({
|
|
876
|
+
agentId: g.agentId,
|
|
877
|
+
level: g.level
|
|
878
|
+
})), {
|
|
879
|
+
agentId: who.agent.id,
|
|
880
|
+
level: w.connection.levelDefault
|
|
881
|
+
}]);
|
|
882
|
+
usable.push(w.entry);
|
|
883
|
+
} else p.print(pc.dim(` "${w.entry.name}" stays in ${label}: its agent couldn't use "${w.connection.slug}".`));
|
|
884
|
+
}
|
|
885
|
+
if (!usable.length) return code;
|
|
886
|
+
p.print("");
|
|
887
|
+
const question = `Replace ${usable.map((e) => `"${e.name}"`).join(", ")} in ${label} with the single xaq entry (setup-mcp --tool ${client}, with a backup)?`;
|
|
888
|
+
const routes = confinedRoutes(client, mcpServerName(who.profileName), deps.env);
|
|
889
|
+
let ok;
|
|
890
|
+
if (routes.length) {
|
|
891
|
+
const slugs = working.filter((w) => usable.includes(w.entry)).map((w) => w.connection.slug);
|
|
892
|
+
p.print(pc.yellow(`These ${label} settings are limited to the xaq server only:`));
|
|
893
|
+
for (const r of routes) p.print(` ${safeText(r, 120)}`);
|
|
894
|
+
p.print(pc.yellow(`Through xaq they now also see the tools of ${slugs.map((s) => `"${s}"`).join(", ")} (as <name>__<tool>), which ${routes.length === 1 ? "it" : "they"} couldn't reach before. If one is triggered by email or webhooks, those tools become reachable from that content. Give ${agentName} less access on those connections, or keep them out, if that's not what you want.`));
|
|
895
|
+
ok = await p.confirmExplicit(question, `${label}'s config wasn't changed; decide in a terminal`);
|
|
896
|
+
} else ok = await p.confirm(question, true);
|
|
897
|
+
if (!ok) {
|
|
898
|
+
p.print(`${label}'s config wasn't changed. Run this import again any time to do it.`);
|
|
899
|
+
return code;
|
|
900
|
+
}
|
|
901
|
+
const r = setupMcp(client, mcpTarget(who.profileName, who.profile), profilePath(who.profileName), { removeServers: usable.map(toRemove) });
|
|
902
|
+
for (const l of setupLines(r)) p.print(l);
|
|
903
|
+
const vars = [...new Set(usable.flatMap((e) => (r.removed ?? []).includes(e.name) ? e.headerVars : []))];
|
|
904
|
+
if (r.ok && vars.length) p.print(pc.dim(` ${vars.join(", ")} ${vars.length === 1 ? "is" : "are"} no longer needed by the removed entries; delete ${vars.length === 1 ? "it" : "them"} from your environment${client === "hermes" ? " (~/.hermes/.env)" : ""} once everything works through xaq.`));
|
|
905
|
+
return r.ok ? code : 1;
|
|
906
|
+
}
|
|
907
|
+
//#endregion
|
|
908
|
+
export { runImport };
|
|
909
|
+
|
|
910
|
+
//# sourceMappingURL=import-DsN5NZT3.js.map
|