Grok Build and Muse Code layouts are unverified and not managed here. Codex and OpenCode honor disabled servers; other providers represent disabled by unlinking.
+ ` : ''}
+
+
${icon('info', 'tc-icon--sm')}Shared servers sync into each linked provider’s own config, leaving its other servers alone. Start a new session after you change links.
Secrets are stored in the shared store (0600) and written into linked provider configs. They never appear in status polls. Put secrets in Environment/Headers, not in the command line: the command is shown in the list.
+
+ ${!creating ? html`` : ''}
+
`,
+ });
+ viewer = current;
+ const panel = current.layer.panel;
+ const sync = () => current.layer.render();
+ panel.addEventListener('input', (event) => {
+ if (event.target.id === 'mcp-name') current.draft.name = event.target.value;
+ if (event.target.id === 'mcp-url') current.draft.url = event.target.value;
+ if (event.target.id === 'mcp-command') current.draft.command = event.target.value;
+ if (event.target.id === 'mcp-pairs') current.draft.pairs = event.target.value;
+ });
+ panel.addEventListener('change', (event) => {
+ if (event.target.id === 'mcp-type') { current.draft.type = event.target.value; sync(); }
+ });
+ panel.addEventListener('click', async (event) => {
+ const button = event.target.closest('button');
+ if (!button || button.disabled) return;
+ if (button.hasAttribute('data-mcp') && button.getAttribute('data-mcp') === 'close') { current.layer.close('close'); return; }
+ if (button.id === 'mcp-enabled') { current.draft.enabled = !current.draft.enabled; button.setAttribute('aria-checked', String(current.draft.enabled)); return; }
+ if (button.getAttribute('data-mcp') === 'remove') {
+ const yes = await Kit.confirm({ title: 'Remove ' + current.name + '?', body: 'Removes the shared entry. Linked provider entries stay until you unlink them; unlink first to remove everywhere at once.', confirm: 'Remove server', tone: 'danger' });
+ if (!yes || viewer !== current || current.saving) return;
+ current.saving = true; current.error = null; current.layer.render();
+ const response = await api('/context/mcp/remove', { body: { name: current.name, revision: shared?.shared?.revision } });
+ current.saving = false;
+ if (!response.ok) { if (viewer === current) { current.error = response.error; current.layer.render(); } return; }
+ if (viewer === current) current.layer.close('saved');
+ Kit.toast('Removed shared MCP server');
+ await load(true);
+ return;
+ }
+ if (button.getAttribute('data-mcp') === 'save') {
+ try {
+ const pairs = parsePairs(current.draft.pairs);
+ const server = current.draft.type === 'local'
+ ? { name: (current.draft.name || '').trim(), type: 'local', enabled: current.draft.enabled,
+ command: String(current.draft.command || '').split('\n').map((line) => line.trim()).filter(Boolean), env: pairs }
+ : { name: (current.draft.name || '').trim(), type: 'remote', enabled: current.draft.enabled,
+ url: (current.draft.url || '').trim(), headers: pairs };
+ current.saving = true; current.error = null; current.layer.render();
+ const response = creating
+ ? await api('/context/mcp/create', { body: { server } })
+ : await api('/context/mcp/save', { body: { name: current.name, revision: shared?.shared?.revision, server } });
+ current.saving = false;
+ if (!response.ok) { if (viewer === current) { current.error = response.error; current.layer.render(); } return; }
+ if (viewer === current) current.layer.close('saved');
+ Kit.toast(creating ? 'Added shared MCP server' : 'Saved shared MCP server');
+ await load(true);
+ } catch (err) {
+ current.error = err.message; current.layer.render();
+ }
+ }
+ });
+ if (creating) { current.loading = false; current.layer.render(); return; }
+ const response = await api('/context/mcp/read?name=' + encodeURIComponent(name));
+ if (viewer !== current) return;
+ current.loading = false;
+ if (!response.ok) { current.error = response.error; current.layer.render(); return; }
+ const server = response.data.server;
+ current.data = server;
+ current.draft = { name: server.name, type: server.type, enabled: server.enabled !== false,
+ command: (server.command || []).join('\n'), url: server.url || '',
+ pairs: formatPairs(server.type === 'local' ? server.env : server.headers) };
+ current.layer.render();
+ };
+
+ const setLink = async (provider, name) => {
+ if (linking) return;
+ const current = getStatus()?.context?.mcp?.providers?.find((row) => row.provider === provider);
+ const linked = !(current?.linked || []).includes(name);
+ linking = provider; render();
+ const response = await api('/context/mcp/link', { body: { provider, name, linked, sharedRevision: shared?.shared?.revision } });
+ linking = null;
+ if (!response.ok) Kit.toast('Could not change MCP link', { tone: 'danger', detail: response.error });
+ else Kit.toast(linked ? providerName(provider) + ' now uses ' + name : providerName(provider) + ' stopped using ' + name);
+ await load(true);
+ };
+
+ return { content, load, refresh: () => load(true), add: () => open(null), view: (id) => open(id),
+ activate: () => { if (!shared && !loading && !error) load(); },
+ toggle: (provider, name) => setLink(provider, name),
+ select: () => {},
+ };
+ };
+ return { create };
+})();
diff --git a/docker/setup/client/model.js b/docker/setup/client/model.js
index 3828f03..f212822 100644
--- a/docker/setup/client/model.js
+++ b/docker/setup/client/model.js
@@ -1891,6 +1891,51 @@ const T3Model = (() => {
});
};
+ // Shared MCP servers: redacted summaries in /status, secrets only in the
+ // editor. Provider support is uneven; unsupported providers stay visible
+ // with their reason instead of failing the whole section.
+ const MCP_NAMES = { claude: 'Claude Code', codex: 'Codex', opencode: 'OpenCode', cursor: 'Cursor', antigravity: 'Antigravity', grok: 'Grok Build', muse: 'Muse Code' };
+ const mcpView = (status) => {
+ const mcp = status?.context?.mcp || {};
+ const shared = mcp.shared || {};
+ return { present: Boolean(shared.present), count: shared.count || (shared.servers || []).length, revision: shared.revision || null,
+ bytes: shared.bytes || 0, error: shared.error || null, ok: mcp.ok !== false };
+ };
+ const mcpSharedRow = (status) => {
+ const view = mcpView(status);
+ const linked = (status?.context?.mcp?.providers || []).filter((row) => (row.linked || []).length)
+ .map((row) => (MCP_NAMES[row.provider] || row.provider) + ' (' + row.linked.length + ')');
+ return {
+ id: 'shared', key: 'mcp:shared', target: 'context-mcp', name: 'Shared MCP servers',
+ mono: 'MC', hue: '--id-toolchain', icon: 'plug',
+ state: view.error ? 'unavailable' : 'ok',
+ status: view.error ? { dot: 'danger', text: view.error }
+ : !view.present ? { text: 'Not written yet · link a provider after you add one' }
+ : { text: plural(view.count, 'server') + (linked.length ? ' · used by ' + listOf(linked) : ' · no provider linked yet') },
+ badge: view.error ? { tone: 'warn', text: 'Unavailable' } : null,
+ action: view.error ? null : { cmd: 'mcp.add', label: view.present ? 'Add server' : 'Add server', icon: 'plus' },
+ menu: [], attention: false,
+ };
+ };
+ const mcpProviderRows = (status) => {
+ const providers = status?.context?.mcp?.providers || [];
+ const harnesses = status?.harnesses;
+ const installed = Array.isArray(harnesses) ? new Set(harnesses.filter((h) => h.installed).map((h) => h.id)) : null;
+ const list = !installed || installed.size === 0 ? providers
+ : providers.filter((row) => installed.has(row.provider) || (row.linked || []).length || (row.ownCount || 0) > 0 || row.error);
+ return list.map((row) => ({
+ id: row.provider, key: 'mcp:' + row.provider, name: MCP_NAMES[row.provider] || row.provider,
+ mark: row.provider, mono: 'MC', hue: '--id-toolchain',
+ status: !row.supported || row.error ? { dot: 'danger', text: row.error || 'Unsupported' }
+ : (row.linked || []).length ? { text: 'Uses ' + listOf(row.linked) + ((row.ownCount || 0) ? ' · +' + plural(row.ownCount, 'own server') : '') }
+ : (row.ownCount || 0) ? { text: plural(row.ownCount, 'own server') + ' · none shared' }
+ : { text: 'No shared servers linked' },
+ note: row.warning || null, path: row.path || null,
+ linked: row.linked || [], unlinked: row.unlinked || [], diverged: row.diverged || [],
+ ownCount: row.ownCount || 0, supported: row.supported !== false, error: row.error || null,
+ }));
+ };
+
// Automatic updates: the policy and last run from /status, as the Updates
// page's switches say them. Pure, so the words are testable without a browser.
const autoUpdateView = (status, now) => {
@@ -1942,6 +1987,7 @@ const T3Model = (() => {
readiness, readySummary, needsYou, containerItems, activity, setupBanner, setupFailures,
publicUrlSource, hereOffer, connectView,
contextView, contextAgentsMdRow, contextProviderRows,
+ mcpView, mcpSharedRow, mcpProviderRows,
navBadges, serverWarnings, diskWarning, attentionCount, summaries, updatesView, runtimeView, autoUpdateView,
paletteItems, searchPalette,
redactedDiagnostics, storageView,
diff --git a/docker/setup/mcp.mjs b/docker/setup/mcp.mjs
new file mode 100644
index 0000000..9df5e73
--- /dev/null
+++ b/docker/setup/mcp.mjs
@@ -0,0 +1,142 @@
+// Shared MCP servers: canonical store plus per-provider link state derived
+// by comparison (no new manifest format). No MCP execution here.
+import path from "node:path";
+import { mcpStorePathFor, readMcpStore, createMcpServer, saveMcpServer, removeMcpServer, redactMcpServer, validateMcpServer, MCP_PROVIDERS, UNSUPPORTED_MCP_PROVIDERS } from "./mcp/store.mjs";
+import { readProviderMcp, writeProviderMcpEntry, toClaudeEntry, toCodexEntry, toOpenCodeEntry, toCursorEntry, toGeminiEntry, isSupportedMcpProvider } from "./mcp/adapters.mjs";
+
+const problem = (code, message) => Object.assign(new Error(message), { code });
+const summaryError = (error) => String(error?.message || error).slice(0, 240);
+
+export function mcpProviders() {
+ return [...Object.entries(MCP_PROVIDERS).map(([id, name]) => ({ id, name })), ...Object.entries(UNSUPPORTED_MCP_PROVIDERS).map(([id, reason]) => ({ id, name: id === "grok" ? "Grok Build" : "Muse Code", unsupported: reason }))];
+}
+
+const toEntryFor = (provider, server) =>
+ provider === "claude" ? toClaudeEntry(server)
+ : provider === "codex" ? toCodexEntry(server)
+ : provider === "opencode" ? toOpenCodeEntry(server)
+ : provider === "cursor" ? toCursorEntry(server)
+ : toGeminiEntry(server);
+
+const normalize = (value) => {
+ if (Array.isArray(value)) return value.map(normalize);
+ if (value && typeof value === "object") {
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, normalize(value[key])]));
+ }
+ return value;
+};
+const canonicalEqual = (a, b) => JSON.stringify(normalize(a)) === JSON.stringify(normalize(b));
+// Native `enabled` exists for Codex and OpenCode only. Other providers link
+// by content; a disabled shared server stays unlinked there with a reason.
+const linkable = (provider, server) => server.enabled !== false || provider === "codex" || provider === "opencode";
+
+export function createMcpService({ home, env = process.env, io, baseDir, ttlMs = 15000 } = {}) {
+ if (!io) throw new Error("mcp service needs an fs implementation");
+ home = path.resolve(home || env.HOME || "/home/t3");
+ baseDir = baseDir || process.env.T3CODE_HOME || path.join(home, ".t3");
+ const statusCache = { at: 0, promise: null };
+
+ const linkState = async (provider, shared) => {
+ if (UNSUPPORTED_MCP_PROVIDERS[provider]) return { provider, supported: false, error: UNSUPPORTED_MCP_PROVIDERS[provider], linked: [], unlinked: shared.map((s) => s.name), own: 0 };
+ if (!isSupportedMcpProvider(provider)) throw problem("invalid", "Unknown MCP provider.");
+ try {
+ const read = await readProviderMcp(io, provider, { home, env });
+ const byName = new Map(read.servers.map((row) => [row.name, row]));
+ const linked = [], unlinked = [], diverged = [];
+ for (const server of shared) {
+ const row = byName.get(server.name);
+ if (!row) { unlinked.push(server.name); continue; }
+ if (row.canonical && canonicalEqual(row.canonical, server)) { linked.push(server.name); continue; }
+ if (!linkable(provider, server)) { unlinked.push(server.name); continue; }
+ // Content differs: a provider-side edit, or a shared edit not yet applied.
+ diverged.push(server.name);
+ }
+ const sharedNames = new Set(shared.map((server) => server.name));
+ const own = read.servers.filter((row) => !sharedNames.has(row.name)).map((row) => row.name);
+ return { provider, supported: true, path: read.path, present: read.present, linked, unlinked, diverged,
+ own, ownCount: own.length, error: null,
+ warning: read.servers.some((row) => !row.canonical) ? "One or more provider entries use an unsupported shape and are left in place." : null };
+ } catch (error) {
+ return { provider, supported: true, path: null, linked: [], unlinked: shared.map((s) => s.name), own: [], ownCount: 0, error: summaryError(error), code: error?.code };
+ }
+ };
+
+ const status = async ({ refresh = false } = {}) => {
+ if (!refresh && statusCache.promise && Date.now() - statusCache.at < ttlMs) return statusCache.promise;
+ const promise = (async () => {
+ const store = await readMcpStore(io, baseDir);
+ const providers = [];
+ for (const provider of ["claude", "codex", "opencode", "cursor", "antigravity", "grok", "muse"]) {
+ providers.push(await linkState(provider, store.servers));
+ }
+ return { ok: !store.error, shared: { present: store.present, count: store.servers.length, revision: store.revision, bytes: store.bytes, error: store.error ?? null,
+ servers: store.servers.map(redactMcpServer) }, providers, scannedAt: Date.now() };
+ })();
+ statusCache.promise = promise;
+ statusCache.at = Date.now();
+ promise.then(() => {}, () => { statusCache.promise = null; });
+ return promise;
+ };
+ const invalidate = () => { statusCache.promise = null; statusCache.at = 0; };
+
+ const list = async (provider) => {
+ if (UNSUPPORTED_MCP_PROVIDERS[provider]) throw problem("unsupported", UNSUPPORTED_MCP_PROVIDERS[provider]);
+ if (!isSupportedMcpProvider(provider)) throw problem("invalid", "Unknown MCP provider.");
+ const store = await readMcpStore(io, baseDir);
+ const state = await linkState(provider, store.servers);
+ if (state.error) throw problem(state.code || "unavailable", state.error);
+ let detail = null;
+ try { detail = await readProviderMcp(io, provider, { home, env }); }
+ catch (error) { throw problem(error?.code || "unavailable", summaryError(error)); }
+ return { ok: true, provider, path: detail.path, sharedRevision: store.revision,
+ shared: store.servers.map((server) => {
+ const row = detail.servers.find((entry) => entry.name === server.name);
+ const linked = Boolean(row?.canonical && canonicalEqual(row.canonical, server));
+ return { ...redactMcpServer(server),
+ linked,
+ diverged: Boolean(row && !linked),
+ linkable: linkable(provider, server),
+ providerEntry: row && !linked ? "(differs from shared)" : row ? "(matches shared)" : "(not present)",
+ };
+ }),
+ own: detail.servers.filter((row) => !store.servers.some((server) => server.name === row.name)).map((row) => ({ name: row.name, recognized: Boolean(row.canonical) })),
+ unsupported: detail.servers.filter((row) => !row.canonical).map((row) => row.name),
+ };
+ };
+
+ const read = async (name) => {
+ const store = await readMcpStore(io, baseDir);
+ if (store.error) throw problem(store.code || "unavailable", store.error);
+ const server = store.servers.find((entry) => entry.name === name);
+ if (!server) throw problem("missing", "This shared MCP server no longer exists. Refresh the list.");
+ return { ok: true, server, revision: store.revision };
+ };
+
+ const setLink = async (provider, name, linked, sharedRevision) => {
+ if (UNSUPPORTED_MCP_PROVIDERS[provider]) throw problem("unsupported", UNSUPPORTED_MCP_PROVIDERS[provider]);
+ if (!isSupportedMcpProvider(provider)) throw problem("invalid", "Unknown MCP provider.");
+ if (typeof linked !== "boolean") throw problem("invalid", "Expected a boolean linked value.");
+ const base = baseDir;
+ const store = await readMcpStore(io, base);
+ if (store.error) throw problem(store.code || "unavailable", store.error);
+ if (store.revision !== sharedRevision) throw problem("conflict", "Shared MCP servers changed. Refresh the list before changing links.");
+ const server = store.servers.find((entry) => entry.name === name);
+ if (!server) throw problem("missing", "This shared MCP server no longer exists. Refresh the list.");
+ if (linked && !linkable(provider, server)) {
+ throw problem("unsupported", "This provider has no native enabled flag; disable is represented by unlinking. Unlink instead.");
+ }
+ await writeProviderMcpEntry(io, provider, { home, env }, name, linked ? toEntryFor(provider, server) : null);
+ invalidate();
+ return { ok: true, provider, name, linked };
+ };
+
+ return {
+ status, list, read, setLink, providers: mcpProviders,
+ create: async (...args) => { const result = await createMcpServer(io, baseDir, ...args); invalidate(); if (!result.ok && !result.code) throw problem("invalid", result.error); if (!result.ok) throw problem(result.code || "invalid", result.error); return { ok: true, ...result }; },
+ save: async (...args) => { const result = await saveMcpServer(io, baseDir, ...args); invalidate(); if (!result.ok && !result.code) throw problem("invalid", result.error); if (!result.ok) throw problem(result.code || "invalid", result.error); return { ok: true, ...result }; },
+ remove: async (...args) => { const result = await removeMcpServer(io, baseDir, ...args); invalidate(); if (!result.ok) throw problem(result.code || "invalid", result.error); return { ok: true, ...result }; },
+ validate: validateMcpServer,
+ };
+}
+
+export { mcpStorePathFor };
diff --git a/docker/setup/mcp/adapters.mjs b/docker/setup/mcp/adapters.mjs
new file mode 100644
index 0000000..02d58f2
--- /dev/null
+++ b/docker/setup/mcp/adapters.mjs
@@ -0,0 +1,282 @@
+// Per-provider MCP file adapters. File-based only (no harness CLI): the
+// console reads bounded JSON/TOML, merges one server entry, and preserves
+// every unrelated key, comment and section.
+import path from "node:path";
+import { parse, parseTree, modify, applyEdits } from "../vendor/jsonc.mjs";
+import { parseMcpToml, applyMcpToml } from "./toml.mjs";
+
+const MAX_FILE_BYTES = 1024 * 1024;
+const problem = (code, message) => Object.assign(new Error(message), { code });
+
+const homeOf = (home, env) => path.resolve(home || env.HOME || "/home/t3");
+const readBounded = async (io, file) => {
+ const stat = await io.stat(file);
+ if (!stat.isFile()) throw problem("invalid", "The MCP configuration must be a regular file.");
+ if (stat.size > MAX_FILE_BYTES) throw problem("too_large", "The MCP configuration exceeds the 1 MiB limit.");
+ return io.readFile(file, "utf8");
+};
+const readJsonFile = async (io, file) => {
+ let raw;
+ try { raw = await readBounded(io, file); }
+ catch (error) { if (error?.code === "ENOENT") return { present: false, raw: null, value: {} }; throw error; }
+ let value;
+ try { value = JSON.parse(raw); }
+ catch { throw problem("unsupported", "The MCP configuration is not valid JSON. Repair it before changing MCP servers."); }
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw problem("unsupported", "The MCP configuration must be a JSON object.");
+ return { present: true, raw, value };
+};
+const jsoncKeys = (file, strict) => {
+ try {
+ const errors = [];
+ const tree = parseTree(file.raw, errors, { allowTrailingComma: !strict, disallowComments: strict });
+ if (errors.length || tree?.type !== "object") throw new Error("invalid");
+ return parse(file.raw, [], { allowTrailingComma: !strict, disallowComments: strict });
+ } catch { throw problem("unsupported", "The MCP configuration contains invalid JSON/JSONC. Repair it before changing MCP servers."); }
+};
+
+// --- config paths ------------------------------------------------------------
+export const claudeMcpPath = (home, env) => path.join(homeOf(home, env), ".claude.json");
+export const codexMcpPath = (home, env) => path.join(path.resolve(homeOf(home, env), env.CODEX_HOME || ".codex"), "config.toml");
+export const openCodeConfigPath = async (io, home, env) => {
+ const root = path.resolve(homeOf(home, env), env.OPENCODE_CONFIG_DIR || path.join(env.XDG_CONFIG_HOME || ".config", "opencode"));
+ const jsonc = path.join(root, "opencode.jsonc");
+ try { await io.lstat(jsonc); return { file: jsonc, jsonc: true }; }
+ catch (error) { if (error?.code !== "ENOENT") throw error; return { file: path.join(root, "opencode.json"), jsonc: false }; }
+};
+export const cursorMcpPath = (home, env) => path.join(homeOf(home, env), ".cursor", "mcp.json");
+export const geminiMcpPath = (home, env) => path.join(homeOf(home, env), ".gemini", "settings.json");
+
+// --- canonical translation ---------------------------------------------------
+// The shared store is the source of truth; each function maps it to the
+// provider's own shape. Secrets are carried (the editor needs them); status
+// polling must use redacted summaries instead.
+export function toClaudeEntry(server) {
+ if (server.type === "local") {
+ const entry = { command: server.command[0], args: server.command.slice(1) };
+ if (Object.keys(server.env).length) entry.env = { ...server.env };
+ return entry;
+ }
+ return { type: "http", url: server.url, ...(Object.keys(server.headers).length ? { headers: { ...server.headers } } : {}) };
+}
+export function fromClaudeEntry(name, entry) {
+ if (!entry || typeof entry !== "object") return null;
+ if (typeof entry.url === "string" && /^https?:\/\//.test(entry.url)) {
+ return { name, type: "remote", url: entry.url, headers: { ...(entry.headers ?? {}) }, enabled: true };
+ }
+ if (typeof entry.command === "string" && entry.command) {
+ return { name, type: "local", command: [entry.command, ...(Array.isArray(entry.args) ? entry.args : [])], env: { ...(entry.env ?? {}) }, enabled: true };
+ }
+ return null;
+}
+export function toCodexEntry(server) {
+ if (server.type === "local") {
+ const entry = { command: server.command[0], args: server.command.slice(1) };
+ if (Object.keys(server.env).length) entry.env = { ...server.env };
+ if (server.enabled === false) entry.enabled = false;
+ return entry;
+ }
+ const entry = { url: server.url };
+ if (Object.keys(server.headers).length) entry.http_headers = { ...server.headers };
+ if (server.enabled === false) entry.enabled = false;
+ return entry;
+}
+export function fromCodexEntry(name, entry) {
+ if (!entry || typeof entry !== "object") return null;
+ if (typeof entry.url === "string" && entry.url) {
+ return { name, type: "remote", url: entry.url, headers: { ...(entry.http_headers ?? {}) }, enabled: entry.enabled !== false };
+ }
+ if (typeof entry.command === "string" && entry.command) {
+ return { name, type: "local", command: [entry.command, ...(Array.isArray(entry.args) ? entry.args : [])], env: { ...(entry.env ?? {}) }, enabled: entry.enabled !== false };
+ }
+ return null;
+}
+export function toOpenCodeEntry(server) {
+ if (server.type === "local") {
+ return { type: "local", command: [...server.command], ...(Object.keys(server.env).length ? { environment: { ...server.env } } : {}), enabled: server.enabled !== false };
+ }
+ return { type: "remote", url: server.url, ...(Object.keys(server.headers).length ? { headers: { ...server.headers } } : {}), enabled: server.enabled !== false };
+}
+export function fromOpenCodeEntry(name, entry) {
+ if (!entry || typeof entry !== "object") return null;
+ if (entry.type === "remote" && typeof entry.url === "string") {
+ return { name, type: "remote", url: entry.url, headers: { ...(entry.headers ?? {}) }, enabled: entry.enabled !== false };
+ }
+ if ((entry.type === "local" || entry.type === undefined) && Array.isArray(entry.command)) {
+ return { name, type: "local", command: [...entry.command], env: { ...(entry.environment ?? entry.env ?? {}) }, enabled: entry.enabled !== false };
+ }
+ return null;
+}
+export function toCursorEntry(server) {
+ return toClaudeEntry(server);
+}
+export function fromCursorEntry(name, entry) {
+ return fromClaudeEntry(name, entry);
+}
+export function toGeminiEntry(server) {
+ if (server.type === "local") {
+ const entry = { command: server.command[0], args: server.command.slice(1) };
+ if (Object.keys(server.env).length) entry.env = { ...server.env };
+ return entry;
+ }
+ return { httpUrl: server.url, ...(Object.keys(server.headers).length ? { headers: { ...server.headers } } : {}) };
+}
+export function fromGeminiEntry(name, entry) {
+ if (!entry || typeof entry !== "object") return null;
+ const url = typeof entry.httpUrl === "string" ? entry.httpUrl : typeof entry.url === "string" ? entry.url : null;
+ if (url && /^https?:\/\//.test(url)) return { name, type: "remote", url, headers: { ...(entry.headers ?? {}) }, enabled: true };
+ if (typeof entry.command === "string" && entry.command) {
+ return { name, type: "local", command: [entry.command, ...(Array.isArray(entry.args) ? entry.args : [])], env: { ...(entry.env ?? {}) }, enabled: true };
+ }
+ return null;
+}
+
+export const ADAPTERS = Object.freeze({
+ claude: { file: claudeMcpPath, toEntry: toClaudeEntry, fromEntry: fromClaudeEntry, containerKey: "mcpServers" },
+ cursor: { file: cursorMcpPath, toEntry: toCursorEntry, fromEntry: fromCursorEntry, containerKey: "mcpServers" },
+ antigravity: { file: geminiMcpPath, toEntry: toGeminiEntry, fromEntry: fromGeminiEntry, containerKey: "mcpServers" },
+});
+export const isSupportedMcpProvider = (provider) => ["claude", "codex", "opencode", "cursor", "antigravity"].includes(provider);
+
+// --- JSON container helpers (claude/cursor/gemini) ---------------------------
+const listJsonServers = (value, containerKey) => {
+ const container = value?.[containerKey];
+ if (container === undefined) return { servers: new Map(), present: false };
+ if (!container || typeof container !== "object" || Array.isArray(container)) throw problem("unsupported", "The MCP configuration has an unsupported server section.");
+ return { servers: new Map(Object.entries(container)), present: true };
+};
+
+/** Read one provider's own servers without secrets leaving redacted summaries. */
+export async function readProviderMcp(io, provider, { home, env = {} }) {
+ const resolvedHome = homeOf(home, env);
+ if (provider === "codex") {
+ const file = codexMcpPath(resolvedHome, env);
+ let raw;
+ try { raw = await readBounded(io, file); }
+ catch (error) { if (error?.code === "ENOENT") return { provider, path: file, present: false, servers: [] }; throw error; }
+ let parsed;
+ try { parsed = parseMcpToml(raw); }
+ catch (error) { throw problem(error?.code || "unsupported", "Codex config.toml MCP sections are unsupported: " + (error?.message || "parse error")); }
+ return { provider, path: file, present: true, raw, servers: [...parsed.entries()].map(([name, entry]) => ({ name, entry, canonical: fromCodexEntry(name, entry) })) };
+ }
+ if (provider === "opencode" || provider === "antigravity") {
+ // OpenCode uses JSONC; Gemini settings tolerate comments too. Parse with
+ // the same vendored parser as skill policies so comments survive reads.
+ const file = provider === "opencode" ? (await openCodeConfigPath(io, resolvedHome, env)).file : ADAPTERS.antigravity.file(resolvedHome, env);
+ let raw;
+ try { raw = await readBounded(io, file); }
+ catch (error) { if (error?.code === "ENOENT") return { provider, path: file, present: false, servers: [] }; throw error; }
+ let value;
+ try {
+ value = parse(raw, [], { allowTrailingComma: true, disallowComments: false });
+ } catch { throw problem("unsupported", "The MCP configuration contains invalid JSON/JSONC. Repair it before changing MCP servers."); }
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw problem("unsupported", "The MCP configuration must be a JSON object.");
+ if (provider === "opencode") {
+ const container = value?.mcp;
+ if (container !== undefined && (!container || typeof container !== "object" || Array.isArray(container))) {
+ throw problem("unsupported", "The OpenCode MCP section has an unsupported shape.");
+ }
+ return { provider, path: file, present: true, raw, value,
+ servers: Object.entries(container ?? {}).map(([name, entry]) => ({ name, entry, canonical: fromOpenCodeEntry(name, entry) })) };
+ }
+ const { servers } = listJsonServers(value, "mcpServers");
+ return { provider, path: file, present: true, raw, value,
+ servers: [...servers.entries()].map(([name, entry]) => ({ name, entry, canonical: fromGeminiEntry(name, entry) })) };
+ }
+ const adapter = ADAPTERS[provider];
+ if (!adapter) throw problem("invalid", "Unknown MCP provider.");
+ const file = adapter.file(resolvedHome, env);
+ const read = await readJsonFile(io, file).catch((error) => {
+ if (error?.code === "ENOENT") return { present: false, raw: null, value: {} };
+ throw error;
+ });
+ const { servers } = listJsonServers(read.value, adapter.containerKey);
+ return { provider, path: file, present: read.present, raw: read.raw, value: read.value,
+ servers: [...servers.entries()].map(([name, entry]) => ({ name, entry, canonical: adapter.fromEntry(name, entry) })) };
+}
+
+/** Merge one translated entry into the provider file, preserving the rest. */
+export async function writeProviderMcpEntry(io, provider, { home, env = {} }, name, entryOrNull) {
+ const resolvedHome = homeOf(home, env);
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) throw problem("invalid", "Invalid MCP server name.");
+ if (provider === "codex") {
+ const file = codexMcpPath(resolvedHome, env);
+ await io.mkdir(path.dirname(file), { recursive: true });
+ let raw = "";
+ try { raw = await readBounded(io, file); }
+ catch (error) { if (error?.code !== "ENOENT") throw error; }
+ const next = applyMcpToml(raw, name, entryOrNull);
+ await io.writeFile(file + `.tmp.${process.pid}`, next, { encoding: "utf8", mode: 0o600 }).catch(async () => {
+ await io.mkdir(path.dirname(file), { recursive: true });
+ await io.writeFile(file + `.tmp.${process.pid}`, next, { encoding: "utf8", mode: 0o600 });
+ });
+ await io.chmod(file + `.tmp.${process.pid}`, 0o600);
+ await io.rename(file + `.tmp.${process.pid}`, file);
+ return { ok: true, path: file };
+ }
+ if (provider === "opencode") {
+ const { file } = await openCodeConfigPath(io, resolvedHome, env);
+ await io.mkdir(path.dirname(file), { recursive: true });
+ let raw = "";
+ try { raw = await readBounded(io, file); }
+ catch (error) { if (error?.code !== "ENOENT") throw error; }
+ if (!raw.trim()) raw = '{}\n';
+ const value = jsoncKeys({ raw }, false);
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw problem("unsupported", "The OpenCode MCP configuration must be a JSON object.");
+ if (value.mcp !== undefined && (!value.mcp || typeof value.mcp !== "object" || Array.isArray(value.mcp))) {
+ throw problem("unsupported", "The OpenCode MCP section has an unsupported shape.");
+ }
+ const formatting = { insertSpaces: true, tabSize: 2, eol: raw.includes("\r\n") ? "\r\n" : "\n" };
+ let content = raw;
+ if (entryOrNull) content = applyEdits(content, modify(content, ["mcp", name], entryOrNull, { formattingOptions: formatting }));
+ else if (value.mcp && Object.hasOwn(value.mcp, name)) content = applyEdits(content, modify(content, ["mcp", name], undefined, { formattingOptions: formatting }));
+ if (Buffer.byteLength(content, "utf8") > MAX_FILE_BYTES) throw problem("too_large", "The MCP configuration would exceed the 1 MiB limit.");
+ const tmp = file + `.tmp.${process.pid}`;
+ await io.writeFile(tmp, content, { encoding: "utf8", mode: 0o600 });
+ await io.chmod(tmp, 0o600);
+ await io.rename(tmp, file);
+ return { ok: true, path: file };
+ }
+ const adapter = ADAPTERS[provider];
+ if (!adapter) throw problem("invalid", "Unknown MCP provider.");
+ const file = adapter.file(resolvedHome, env);
+ await io.mkdir(path.dirname(file), { recursive: true });
+ let raw = "";
+ try { raw = await readBounded(io, file); }
+ catch (error) { if (error?.code !== "ENOENT") throw error; }
+ if (!raw.trim()) raw = '{}\n';
+ if (provider === "antigravity") {
+ // settings.json tolerates comments; preserve them like opencode does.
+ const value = jsoncKeys({ raw }, false);
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw problem("unsupported", "The MCP configuration must be a JSON object.");
+ if (value.mcpServers !== undefined && (!value.mcpServers || typeof value.mcpServers !== "object" || Array.isArray(value.mcpServers))) {
+ throw problem("unsupported", "The MCP configuration has an unsupported server section.");
+ }
+ const formatting = { insertSpaces: true, tabSize: 2, eol: raw.includes("\r\n") ? "\r\n" : "\n" };
+ let content = raw;
+ if (entryOrNull) content = applyEdits(content, modify(content, ["mcpServers", name], entryOrNull, { formattingOptions: formatting }));
+ else if (value.mcpServers && Object.hasOwn(value.mcpServers, name)) content = applyEdits(content, modify(content, ["mcpServers", name], undefined, { formattingOptions: formatting }));
+ if (Buffer.byteLength(content, "utf8") > MAX_FILE_BYTES) throw problem("too_large", "The MCP configuration would exceed the 1 MiB limit.");
+ const tmp = file + `.tmp.${process.pid}`;
+ await io.writeFile(tmp, content, { encoding: "utf8", mode: 0o600 });
+ await io.chmod(tmp, 0o600);
+ await io.rename(tmp, file);
+ return { ok: true, path: file };
+ }
+ let value;
+ try { value = JSON.parse(raw); }
+ catch { throw problem("unsupported", "The MCP configuration is not valid JSON. Repair it before changing MCP servers."); }
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw problem("unsupported", "The MCP configuration must be a JSON object.");
+ if (value[adapter.containerKey] !== undefined && (!value[adapter.containerKey] || typeof value[adapter.containerKey] !== "object" || Array.isArray(value[adapter.containerKey]))) {
+ throw problem("unsupported", "The MCP configuration has an unsupported server section.");
+ }
+ const next = { ...value, [adapter.containerKey]: { ...(value[adapter.containerKey] ?? {}) } };
+ if (entryOrNull) next[adapter.containerKey][name] = entryOrNull;
+ else delete next[adapter.containerKey][name];
+ const content = JSON.stringify(next, null, 2) + "\n";
+ if (Buffer.byteLength(content, "utf8") > MAX_FILE_BYTES) throw problem("too_large", "The MCP configuration would exceed the 1 MiB limit.");
+ const tmp = file + `.tmp.${process.pid}`;
+ await io.writeFile(tmp, content, { encoding: "utf8", mode: 0o600 });
+ await io.chmod(tmp, 0o600);
+ await io.rename(tmp, file);
+ return { ok: true, path: file };
+}
diff --git a/docker/setup/mcp/store.mjs b/docker/setup/mcp/store.mjs
new file mode 100644
index 0000000..321e2f8
--- /dev/null
+++ b/docker/setup/mcp/store.mjs
@@ -0,0 +1,195 @@
+// Shared MCP server store. Canonical JSON on the state volume; provider
+// files are only touched by explicit link/unlink saves.
+import path from "node:path";
+import { createHash, randomBytes } from "node:crypto";
+
+export const MCP_PROVIDERS = Object.freeze({
+ claude: "Claude Code",
+ codex: "Codex",
+ opencode: "OpenCode",
+ cursor: "Cursor",
+ antigravity: "Antigravity",
+});
+export const SUPPORTED_MCP_PROVIDERS = Object.freeze(["claude", "codex", "opencode", "cursor", "antigravity"]);
+// Grok Build and Muse Code MCP layouts are unverified against the pinned
+// CLIs (see docs/context-management.md). They stay out until verified.
+export const UNSUPPORTED_MCP_PROVIDERS = Object.freeze({
+ grok: "Grok Build MCP layout is unverified against the pinned CLI; not managed here yet.",
+ muse: "Muse Code MCP layout is unverified against the pinned CLI; not managed here yet.",
+});
+
+const MAX_STORE_BYTES = 256 * 1024;
+const MAX_SERVERS = 128;
+export const mcpDirFor = (baseDir) => path.join(baseDir, "setup", "mcp");
+export const mcpStorePathFor = (baseDir) => path.join(mcpDirFor(baseDir), "servers.json");
+
+export const MCP_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
+const problem = (code, message) => Object.assign(new Error(message), { code });
+
+export function validateMcpServer(input) {
+ const value = input && typeof input === "object" ? input : null;
+ if (!value) return { ok: false, error: "Expected an MCP server object." };
+ const name = String(value.name ?? "").trim();
+ if (!MCP_NAME_RE.test(name) || name.length > 64) return { ok: false, error: "Use a server name of 1–64 lowercase letters, numbers and single hyphens." };
+ const type = value.type === "remote" ? "remote" : value.type === "local" ? "local" : null;
+ if (!type) return { ok: false, error: "Choose a local or remote server type." };
+ const enabled = value.enabled === undefined ? true : value.enabled;
+ if (typeof enabled !== "boolean") return { ok: false, error: "Expected enabled to be a boolean." };
+ const out = { name, type, enabled };
+ if (type === "local") {
+ const command = Array.isArray(value.command) ? value.command : null;
+ if (!command || command.length < 1 || command.length > 32 || !command.every((part) => typeof part === "string" && part.length >= 1 && part.length <= 1024)) {
+ return { ok: false, error: "A local server needs a command of 1–32 entries, each 1–1024 characters." };
+ }
+ if (command.some((part) => part.includes("\0"))) return { ok: false, error: "Command entries must not contain NUL bytes." };
+ out.command = [...command];
+ const env = value.env === undefined ? {} : value.env;
+ if (!env || typeof env !== "object" || Array.isArray(env)) return { ok: false, error: "Expected env to be an object." };
+ const keys = Object.keys(env);
+ if (keys.length > 64) return { ok: false, error: "A server holds at most 64 environment entries." };
+ for (const key of keys) {
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || key.length > 128) return { ok: false, error: `Invalid environment name: ${key}` };
+ if (typeof env[key] !== "string" || env[key].length > 8192 || env[key].includes("\0")) return { ok: false, error: `Invalid environment value for ${key}.` };
+ }
+ out.env = Object.fromEntries(keys.sort().map((key) => [key, String(env[key])]));
+ } else {
+ const url = String(value.url ?? "").trim();
+ if (!/^https?:\/\/.{1,2048}$/.test(url)) return { ok: false, error: "A remote server needs an http(s) URL of at most 2048 characters." };
+ out.url = url;
+ const headers = value.headers === undefined ? {} : value.headers;
+ if (!headers || typeof headers !== "object" || Array.isArray(headers)) return { ok: false, error: "Expected headers to be an object." };
+ const keys = Object.keys(headers);
+ if (keys.length > 64) return { ok: false, error: "A server holds at most 64 header entries." };
+ for (const key of keys) {
+ if (!/^[A-Za-z0-9-]+$/.test(key) || key.length > 128) return { ok: false, error: `Invalid header name: ${key}` };
+ if (typeof headers[key] !== "string" || headers[key].length > 8192 || /[\r\n\0]/.test(headers[key])) return { ok: false, error: `Invalid header value for ${key}.` };
+ }
+ out.headers = Object.fromEntries(keys.sort().map((key) => [key, String(headers[key])]));
+ }
+ return { ok: true, server: out };
+}
+
+const canonicalServer = (server) => JSON.stringify(server);
+export const mcpRevision = (servers) => createHash("sha256").update(JSON.stringify([...servers].sort((a, b) => a.name.localeCompare(b.name)))).digest("hex");
+
+async function readStoreFile(fs, file) {
+ let stat;
+ try { stat = await fs.stat(file); }
+ catch (error) { if (error?.code === "ENOENT") return { present: false }; throw error; }
+ if (!stat.isFile()) throw problem("invalid", "The shared MCP store must be a regular file.");
+ if (stat.size > MAX_STORE_BYTES) throw problem("too_large", "The shared MCP store exceeds the 256 KiB limit. Reduce it directly on disk.");
+ const raw = await fs.readFile(file, "utf8");
+ let parsed;
+ try { parsed = JSON.parse(raw); }
+ catch { throw problem("invalid", "The shared MCP store is not valid JSON. Repair it directly on disk."); }
+ const list = Array.isArray(parsed) ? parsed : parsed?.servers;
+ if (!Array.isArray(list)) throw problem("invalid", "The shared MCP store must hold a server list.");
+ if (list.length > MAX_SERVERS) throw problem("too_large", "The shared MCP store holds too many servers.");
+ const servers = [];
+ for (const entry of list) {
+ const checked = validateMcpServer(entry);
+ if (!checked.ok) throw problem("invalid", "The shared MCP store holds an invalid server: " + checked.error);
+ servers.push(checked.server);
+ }
+ const names = servers.map((server) => server.name);
+ if (new Set(names).size !== names.length) throw problem("invalid", "The shared MCP store holds duplicate server names.");
+ return { present: true, servers, bytes: Buffer.byteLength(raw, "utf8") };
+}
+
+async function writeFileAtomic(fs, file, text) {
+ await fs.mkdir(path.dirname(file), { recursive: true });
+ const tmp = `${file}.tmp.${process.pid}.${randomBytes(6).toString("hex")}`;
+ try {
+ await fs.writeFile(tmp, text, { encoding: "utf8", mode: 0o600 });
+ await fs.chmod(tmp, 0o600);
+ await fs.rename(tmp, file);
+ } finally {
+ await fs.rm(tmp, { force: true });
+ }
+}
+
+const storeQueues = new Map();
+async function withStore(file, work) {
+ const key = path.resolve(file);
+ const previous = storeQueues.get(key) ?? Promise.resolve();
+ const current = previous.catch(() => {}).then(work);
+ storeQueues.set(key, current);
+ try { return await current; }
+ finally { if (storeQueues.get(key) === current) storeQueues.delete(key); }
+}
+
+export async function readMcpStore(fs, baseDir) {
+ const file = mcpStorePathFor(baseDir);
+ try {
+ const read = await readStoreFile(fs, file);
+ if (!read.present) return { present: false, path: file, servers: [], revision: mcpRevision([]), bytes: 0, error: null };
+ return { present: read.servers.length > 0, path: file, servers: read.servers, revision: mcpRevision(read.servers), bytes: read.bytes, error: null };
+ } catch (error) {
+ return { present: error?.code === "too_large", path: file, servers: [], revision: null, bytes: 0,
+ code: error?.code, error: String(error?.message ?? error) };
+ }
+}
+
+const changedStore = () => ({ ok: false, code: "conflict", error: "Shared MCP servers changed since you opened them. Reopen the editor to load the current list." });
+
+export async function createMcpServer(fs, baseDir, input) {
+ const checked = validateMcpServer(input);
+ if (!checked.ok) return checked;
+ const file = mcpStorePathFor(baseDir);
+ return withStore(file, async () => {
+ const read = await readMcpStore(fs, baseDir);
+ if (read.error) return { ok: false, error: read.error };
+ if (read.servers.some((server) => server.name === checked.server.name)) {
+ return { ok: false, code: "conflict", error: "A shared MCP server with that name already exists." };
+ }
+ const servers = [...read.servers, checked.server];
+ await writeFileAtomic(fs, file, JSON.stringify({ version: 1, servers }, null, 2) + "\n");
+ return { ok: true, path: file, created: !read.present && read.servers.length === 0, revision: mcpRevision(servers), server: checked.server };
+ });
+}
+
+export async function saveMcpServer(fs, baseDir, name, revision, input) {
+ const checked = validateMcpServer({ ...(input ?? {}), name });
+ if (!checked.ok) return checked;
+ if (checked.server.name !== String(name ?? "").trim()) return { ok: false, error: "The server name cannot be changed. Remove it and add a new one." };
+ const file = mcpStorePathFor(baseDir);
+ return withStore(file, async () => {
+ const read = await readMcpStore(fs, baseDir);
+ if (read.error) return { ok: false, error: read.error };
+ if (read.revision !== revision) return changedStore();
+ const at = read.servers.findIndex((server) => server.name === checked.server.name);
+ if (at < 0) return { ok: false, code: "missing", error: "This shared MCP server no longer exists. Refresh the list." };
+ const servers = read.servers.map((server, index) => index === at ? checked.server : server);
+ await writeFileAtomic(fs, file, JSON.stringify({ version: 1, servers }, null, 2) + "\n");
+ return { ok: true, path: file, revision: mcpRevision(servers), server: checked.server };
+ });
+}
+
+export async function removeMcpServer(fs, baseDir, name, revision) {
+ const file = mcpStorePathFor(baseDir);
+ return withStore(file, async () => {
+ const read = await readMcpStore(fs, baseDir);
+ if (read.error) return { ok: false, error: read.error };
+ if (read.revision !== revision) return changedStore();
+ if (!read.servers.some((server) => server.name === name)) return { ok: true, path: file, deleted: false, revision: read.revision };
+ const servers = read.servers.filter((server) => server.name !== name);
+ if (servers.length === 0) await fs.rm(file, { force: true });
+ else await writeFileAtomic(fs, file, JSON.stringify({ version: 1, servers }, null, 2) + "\n");
+ return { ok: true, path: file, deleted: true, removed: name, revision: mcpRevision(servers) };
+ });
+}
+
+export function redactMcpServer(server) {
+ const { env, headers, ...rest } = server;
+ return { ...rest,
+ envKeys: Object.keys(env ?? {}),
+ envCount: Object.keys(env ?? {}).length,
+ headerKeys: Object.keys(headers ?? {}),
+ headerCount: Object.keys(headers ?? {}).length,
+ secretCount: Object.keys(env ?? {}).length + Object.keys(headers ?? {}).length,
+ };
+}
+
+// Fingerprint of the translated provider entry, so link state is derived by
+// comparison instead of a new manifest format.
+export const mcpFingerprint = (value) => createHash("sha256").update(canonicalServer(value)).digest("hex");
diff --git a/docker/setup/mcp/toml.mjs b/docker/setup/mcp/toml.mjs
new file mode 100644
index 0000000..346c11e
--- /dev/null
+++ b/docker/setup/mcp/toml.mjs
@@ -0,0 +1,161 @@
+// Minimal TOML handling for Codex `[mcp_servers.*]` sections only. The rest
+// of config.toml is opaque text and is preserved byte-for-byte.
+const problem = (code, message) => Object.assign(new Error(message), { code });
+
+const sectionHead = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/;
+const stripComment = (value) => {
+ let out = "", quote = null;
+ for (let i = 0; i < value.length; i++) {
+ const char = value[i];
+ if (quote) {
+ if (char === "\\") { out += value.slice(i, i + 2); i++; continue; }
+ if (char === quote) quote = null;
+ out += char;
+ } else if (char === '"' || char === "'") { quote = char; out += char; }
+ else if (char === "#") break;
+ else out += char;
+ }
+ return out.trim();
+};
+const unquote = (raw) => {
+ const text = stripComment(raw).trim();
+ if (/^"""/.test(text) || /^'''/.test(text)) throw problem("unsupported", "Multiline TOML strings are not supported in MCP sections.");
+ if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
+ const inner = text.slice(1, -1);
+ if (text.startsWith("'")) {
+ if (inner.includes("'")) throw problem("unsupported", "Unsupported TOML string in MCP sections.");
+ return inner;
+ }
+ return inner.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
+ .replace(/\\(.)/g, (_, char) => ({ n: "\n", t: "\t", r: "\r", '"': '"', "\\": "\\" }[char] ?? char));
+ }
+ if (text === "true") return true;
+ if (text === "false") return false;
+ if (/^[+-]?\d+$/.test(text)) return Number(text);
+ if (/^\[.*\]$/.test(text)) {
+ const inner = text.slice(1, -1).trim();
+ if (!inner) return [];
+ const parts = [];
+ let current = "", quote = null, depth = 0;
+ for (let i = 0; i < inner.length; i++) {
+ const char = inner[i];
+ if (quote) { current += char; if (char === "\\") { current += inner[++i] ?? ""; continue; } if (char === quote) quote = null; continue; }
+ if (char === '"' || char === "'") { quote = char; current += char; continue; }
+ if (char === "[") depth++;
+ if (char === "]") depth--;
+ if (char === "," && depth === 0) { parts.push(current.trim()); current = ""; continue; }
+ current += char;
+ }
+ if (current.trim() || parts.length) parts.push(current.trim());
+ return parts.filter((part) => part !== "").map(unquote);
+ }
+ if (/^\{.*\}$/.test(text)) {
+ const inner = text.slice(1, -1).trim();
+ if (!inner) return {};
+ const out = {};
+ for (const pair of inner.split(",").map((part) => part.trim()).filter(Boolean)) {
+ const match = /^([^=]+)=(.*)$/.exec(pair);
+ if (!match) throw problem("unsupported", "Unsupported TOML table in MCP sections.");
+ out[unquote(match[1].trim())] = unquote(match[2].trim());
+ }
+ return out;
+ }
+ const bare = /^[A-Za-z0-9_.-]+$/.test(text) ? text : null;
+ if (bare !== null) return bare;
+ throw problem("unsupported", "Unsupported TOML value in MCP sections.");
+};
+
+const tomlString = (value) => JSON.stringify(String(value));
+const tomlValue = (value) => {
+ if (typeof value === "string") return tomlString(value);
+ if (typeof value === "boolean") return value ? "true" : "false";
+ if (typeof value === "number" && Number.isInteger(value)) return String(value);
+ if (Array.isArray(value)) return "[ " + value.map(tomlValue).join(", ") + " ]";
+ if (value && typeof value === "object") {
+ const entries = Object.entries(value).map(([key, entry]) => `${tomlString(key)} = ${tomlValue(entry)}`);
+ return "{ " + entries.join(", ") + " }";
+ }
+ throw problem("invalid", "Unsupported MCP value for TOML output.");
+};
+
+/** Parse only `[mcp_servers.]` tables; everything else stays opaque. */
+export function parseMcpToml(text) {
+ const lines = String(text ?? "").split("\n");
+ const servers = new Map();
+ let current = null;
+ let currentName = null;
+ for (const line of lines) {
+ const head = sectionHead.exec(line);
+ if (head) {
+ const table = head[1].trim();
+ const match = /^mcp_servers\.(.+)$/.exec(table);
+ if (match) {
+ currentName = match[1].trim().replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
+ if (!currentName || servers.has(currentName)) throw problem("unsupported", `Duplicate Codex MCP server: ${currentName || "(unnamed)"}.`);
+ current = {};
+ servers.set(currentName, current);
+ } else {
+ current = null;
+ currentName = null;
+ }
+ continue;
+ }
+ if (!current) continue;
+ const trimmed = line.trim();
+ if (!trimmed || trimmed.startsWith("#")) continue;
+ const match = /^([^=]+)=(.*)$/.exec(line);
+ if (!match) throw problem("unsupported", "Unsupported TOML line in Codex MCP sections.");
+ current[unquote(match[1].trim())] = unquote(match[2]);
+ }
+ return servers;
+}
+
+/** Replace/add/remove one `[mcp_servers.]` section, preserving the rest. */
+export function applyMcpToml(text, name, entry) {
+ const lines = String(text ?? "").split("\n");
+ const out = [];
+ let skipping = false;
+ let replaced = false;
+ const pushEntry = () => {
+ if (!entry) return;
+ out.push(`[mcp_servers.${name}]`);
+ if (entry.command !== undefined) out.push(`command = ${tomlValue(entry.command)}`);
+ if (entry.args !== undefined) out.push(`args = ${tomlValue(entry.args)}`);
+ if (entry.env !== undefined) out.push(`env = ${tomlValue(entry.env)}`);
+ if (entry.url !== undefined) out.push(`url = ${tomlValue(entry.url)}`);
+ if (entry.http_headers !== undefined) out.push(`http_headers = ${tomlValue(entry.http_headers)}`);
+ if (entry.enabled !== undefined) out.push(`enabled = ${tomlValue(entry.enabled)}`);
+ if (entry.startup_timeout_ms !== undefined) out.push(`startup_timeout_ms = ${tomlValue(entry.startup_timeout_ms)}`);
+ };
+ for (const line of lines) {
+ const head = sectionHead.exec(line);
+ if (head) {
+ if (skipping) { skipping = false; }
+ const table = head[1].trim();
+ const match = /^mcp_servers\.(.+)$/.exec(table);
+ const target = match && match[1].trim().replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1") === name;
+ if (target) {
+ skipping = true;
+ replaced = true;
+ continue;
+ }
+ out.push(line);
+ continue;
+ }
+ if (skipping) continue;
+ out.push(line);
+ }
+ if (!replaced && entry) {
+ if (out.length && out[out.length - 1].trim() !== "") out.push("");
+ pushEntry();
+ } else if (replaced && entry) {
+ // Re-insert at end to keep output deterministic; the marker order does
+ // not matter to Codex. Existing unrelated sections keep their bytes.
+ while (out.length && out[out.length - 1].trim() === "") out.pop();
+ out.push("");
+ pushEntry();
+ }
+ let result = out.join("\n");
+ if (!result.endsWith("\n")) result += "\n";
+ return result;
+}
diff --git a/docker/setup/page.mjs b/docker/setup/page.mjs
index 7db04b8..d920a3d 100644
--- a/docker/setup/page.mjs
+++ b/docker/setup/page.mjs
@@ -30,6 +30,7 @@ export const CONSOLE_SCRIPTS = [
"client/storage.js",
"client/skill-import.js",
"client/skills.js",
+ "client/mcp.js",
"app.js",
];
export const UNLOCK_SCRIPTS = ["client/base.js", "design/ui.js", "client/unlock.js"];
diff --git a/docker/setup/server.mjs b/docker/setup/server.mjs
index 9a63091..b5315a9 100644
--- a/docker/setup/server.mjs
+++ b/docker/setup/server.mjs
@@ -33,6 +33,7 @@ import { createDeviceLabels } from "./device-labels.mjs";
import * as Context from "./context.mjs";
import { createOpenCodeAuth } from "./opencode-auth.mjs";
import { createSkillsService } from "./skills.mjs";
+import { createMcpService } from "./mcp.mjs";
import { clientAddress } from "./client-address.mjs";
import { newKey, readKeyFile, writeKeyFile } from "./setup-key.mjs";
import { connectState, jsonFrom, parseLinkOutput } from "./connect.mjs";
@@ -519,6 +520,9 @@ const contextFs = async () => (await import("node:fs/promises"));
const skillsService = createSkillsService({ home: CONTEXT_HOME, env: process.env,
getHarness: async (provider) => (await harnessCache.snapshotCheap()).harnesses?.find((row) => row.id === provider),
});
+const mcpService = createMcpService({ home: CONTEXT_HOME, env: process.env, baseDir: STATE_DIR,
+ io: await contextFs(),
+});
// The browser image's agent browser, as built: the versions are build args the
// Dockerfile bakes into the environment. Absent on the core image.
@@ -912,7 +916,14 @@ const status = async () => {
// Every other tool in the global mise config, added here or with `mise use -g`.
packages: withLatest("package", packageRows),
// Global instruction metadata; the editor fetches contents on demand.
- context: await attempt("context", async () => Context.contextStatus(await contextFs(), { baseDir: STATE_DIR, home: CONTEXT_HOME, env: process.env }), {
+ // Shared MCP metadata rides alongside; server entries stay server-side.
+ context: await attempt("context", async () => {
+ const [instructions, mcp] = await Promise.all([
+ Context.contextStatus(await contextFs(), { baseDir: STATE_DIR, home: CONTEXT_HOME, env: process.env }),
+ mcpService.status().catch((error) => ({ ok: false, error: String(error?.message ?? error) })),
+ ]);
+ return { ...instructions, mcp };
+ }, {
agentsMd: { present: false, bytes: 0, error: "unavailable" },
}),
// The background install of everything T3_PREINSTALL names, on a first
@@ -2153,7 +2164,8 @@ const ROUTES = ["/login", "/logout", "/hello", "/status", "/pair", "/revoke", "/
"/providers", "/updates/check",
"/packages/install", "/packages/update", "/packages/uninstall", "/packages/cancel",
"/packages/registry", "/packages/versions", "/packages/info",
- "/context/agents-md", "/context/skills/read", "/context/skills/create", "/context/skills/save", "/context/skills/remove", "/context/skills/state", "/context/skills/source", "/context/skills/plan", "/context/skills/install", "/context/skills/discard", "/context/skills"];
+ "/context/agents-md", "/context/skills/read", "/context/skills/create", "/context/skills/save", "/context/skills/remove", "/context/skills/state", "/context/skills/source", "/context/skills/plan", "/context/skills/install", "/context/skills/discard", "/context/skills",
+ "/context/mcp", "/context/mcp/read", "/context/mcp/create", "/context/mcp/save", "/context/mcp/remove", "/context/mcp/link"];
// One background pass for the opt-in auto-updater. Checks are cheap and
// always safe; installs run only when enabled, an update is known, and
@@ -2670,6 +2682,42 @@ const server = createServer(async (req, res) => {
}
}
+ // --- shared MCP servers ---------------------------------------------
+ // Inventory per provider is on demand, never part of /status polling;
+ // /status carries only redacted shared metadata and link states.
+ if (req.method === "GET" && (route === "/context/mcp" || route === "/context/mcp/read")) {
+ try {
+ const params = raw.searchParams;
+ if (route === "/context/mcp/read") {
+ if (!params.get("name")) return sendJson(res, 400, { ok: false, code: "invalid", error: "Expected an MCP server name." });
+ return sendJson(res, 200, await mcpService.read(params.get("name")));
+ }
+ if (params.get("provider")) return sendJson(res, 200, await mcpService.list(params.get("provider")));
+ return sendJson(res, 200, await mcpService.status({ refresh: params.get("refresh") === "1" }));
+ } catch (error) {
+ const status = { invalid: 400, missing: 404, ENOENT: 404, conflict: 409, too_large: 413, unsupported: 403, EACCES: 403, EPERM: 403 }[error?.code] || 500;
+ return sendJson(res, status, { ok: false, code: error?.code || "unavailable", error: String(error?.message || "MCP discovery is unavailable.").slice(0, 300) });
+ }
+ }
+
+ if (req.method === "POST" && ["/context/mcp/create", "/context/mcp/save", "/context/mcp/remove", "/context/mcp/link"].includes(route)) {
+ try {
+ const input = JSON.parse((await readBody(req, 6 * 64 * 1024 + 4096)) || "{}");
+ if (!input || typeof input !== "object" || Array.isArray(input)) return sendJson(res, 400, { ok: false, error: "Expected an MCP operation object." });
+ const result = route === "/context/mcp/link" ? await mcpService.setLink(input.provider, input.name, input.linked, input.sharedRevision)
+ : route === "/context/mcp/create" ? await mcpService.create(input.server)
+ : route === "/context/mcp/save" ? await mcpService.save(input.name, input.revision, input.server)
+ : await mcpService.remove(input.name, input.revision);
+ recordEvent("context.saved", route === "/context/mcp/link"
+ ? `${input.provider} ${input.linked ? "uses" : "stopped using"} shared MCP server ${input.name}`
+ : route === "/context/mcp/remove" ? "Removed a shared MCP server" : "Saved a shared MCP server");
+ return sendJson(res, 200, result);
+ } catch (error) {
+ const status = { invalid: 400, missing: 404, ENOENT: 404, conflict: 409, EEXIST: 409, too_large: 413, unsupported: 403, EACCES: 403, EPERM: 403 }[error?.code] || (error instanceof SyntaxError ? 400 : 500);
+ return sendJson(res, status, { ok: false, code: error?.code || "unavailable", error: String(error?.message || "The MCP operation failed.").slice(0, 500) });
+ }
+ }
+
// --- global instructions ----------------------------------------------
if (req.method === "GET" && route === "/context/agents-md") {
try {
diff --git a/docs/agent-browser.md b/docs/agent-browser.md
index 3282302..b0e184e 100644
--- a/docs/agent-browser.md
+++ b/docs/agent-browser.md
@@ -31,7 +31,10 @@ docker compose exec t3code t3-browser-mcp --remove
This registers headless Chromium as an MCP server with the installed Claude
Code, Codex and OpenCode, so agents can navigate, take screenshots, click and
-read the browser console, whichever client you use. Run it after the first
+read the browser console, whichever client you use. The console's
+**Instructions & skills → MCP servers** tab manages the same servers as shared
+entries (linked per provider) with the same restart rule; `t3-browser-mcp`
+remains the quick CLI equivalent. Run either after the first
start has installed those agents (the setup page shows when), then start a new
thread, because agents read their MCP configuration when a session starts.
diff --git a/docs/context-management.md b/docs/context-management.md
index cbd6835..e72324f 100644
--- a/docs/context-management.md
+++ b/docs/context-management.md
@@ -2,7 +2,8 @@
**Setup → Instructions & skills** edits the shared AGENTS.md and each agent's
own instruction file, on its **Instructions** tab; skills are on its **Skills**
-tab (`#instructions/skills`). The editor is centered
+tab and shared MCP servers on its **MCP servers** tab (`#instructions/skills`,
+`#instructions/mcp`). The editor is centered
on desktop and opens as a bottom sheet on phones. Uploading a Markdown file
replaces the draft; only **Save** applies it. Downloads include unsaved edits.
@@ -353,6 +354,40 @@ settings. A failed verification reports that the policy may already be saved;
it does not roll back over a possible external change. External applications can
still race the final check. Start a new conversation to load policy changes.
+### Shared MCP servers
+
+The **MCP servers** tab holds one canonical list of Model Context Protocol
+servers at `$T3CODE_HOME/setup/mcp/servers.json` (mode `0600`, at most 128
+entries) and links each entry into the providers that should use it. A local
+server runs a command (`playwright-mcp --headless …`); a remote server points
+at an `http(s)` URL. Environment variables (local) and headers (remote) are
+stored with the entry and written into linked provider configs; they never
+appear in `/status` polling, which carries redacted summaries only
+(`envKeys`, `secretCount`). The full entry loads on demand in the editor.
+
+Linking a provider merges the translated entry into its own config file and
+leaves every other server and setting in place. Unlinking removes only the
+shared entry. A provider-side edit shows as diverged: relink to apply the
+shared entry again. Provider-only servers are never touched. Start a new
+session after changing links: agents read MCP configuration at startup.
+
+How well each provider is supported:
+
+| Provider | Config file | Local | Remote | Notes |
+| --- | --- | --- | --- | --- |
+| Claude Code | `~/.claude.json` (`mcpServers`) | ✅ command/args/env | ✅ `type: http` + url/headers | File-based; project `.mcp.json` is out of scope |
+| Codex | `~/.codex/config.toml` (`[mcp_servers.*]`) | ✅ command/args/env | ✅ url/`http_headers` | Only `mcp_servers` sections are parsed; the rest of the file is preserved byte-for-byte |
+| OpenCode | `opencode.json[c]` (`.mcp`) | ✅ `type: local`, command array | ✅ `type: remote` + url/headers | JSONC wins when both exist; comments preserved |
+| Cursor | `~/.cursor/mcp.json` (`mcpServers`) | ✅ command/args/env | ✅ url/headers | Same shape as Claude user scope |
+| Antigravity | `~/.gemini/settings.json` (`mcpServers`) | ✅ command/args/env | ✅ `httpUrl`/headers | Gemini CLI shape; Antigravity divergence from it is unverified |
+| Grok Build | — | — | — | MCP layout unverified against the pinned CLI; not managed here yet |
+| Muse Code | — | — | — | MCP layout unverified against the pinned CLI; not managed here yet |
+
+Only Codex and OpenCode have a native enabled flag, so a disabled shared
+server links there and stays unlinked elsewhere (unlink represents disabled).
+`t3-browser-mcp` remains the quick CLI path for the two bundled browser
+servers; the console manages the shared list above it.
+
## Validation
`node --test tests/*.test.mjs` checks file writes, existing-file discovery and
@@ -390,6 +425,11 @@ confirmation, native state failures, busy controls and late provider responses.
also cover catalog selection, raw Markdown previews, provider selection,
replacement consent/conflicts, failed backup moves/restoration, supporting-file
edits with restored mtimes, shared-root retargeting and partial results.
+Unit/HTTP checks for shared MCP servers (`tests/mcp.test.mjs`,
+`tests/mcp-api.test.mjs`) cover store validation and revisions, TOML section
+preservation, provider translation round-trips, link/diverged detection,
+comment-preserving JSONC writes, unsupported providers and the authenticated
+boundary including secret redaction in polling responses.
The adapters were also checked with Codex 0.163.0 in a disposable home. Native
enable/disable, stale-request rejection, settings persistence across fresh
diff --git a/docs/setup-page.md b/docs/setup-page.md
index 8f9ea8b..de8c844 100644
--- a/docs/setup-page.md
+++ b/docs/setup-page.md
@@ -128,4 +128,8 @@ skills. Claude exposes its four native visibility states; OpenCode exposes
allow/ask/deny user policies and reports the Build agent's effective rule.
Unmanaged shared skills can be added, edited and removed from Codex/OpenCode.
Other providers show known files with unverified availability.
+The **MCP servers** tab holds one shared list of local and remote servers and
+links each entry into the providers that should use it (Claude Code, Codex,
+OpenCode, Cursor and Antigravity; Grok Build and Muse Code layouts are
+unverified and not managed). Provider-only servers stay in place.
See [Context management](context-management.md) for paths and verification.
diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh
index f557c70..cc916dc 100755
--- a/scripts/smoke-test.sh
+++ b/scripts/smoke-test.sh
@@ -647,6 +647,21 @@ context_instructions_work() {
}
check "Context authenticates saves and reports provider links" context_instructions_work
+mcp_servers_list_without_secrets() {
+ docker exec "$NAME" curl -fsS --max-time 25 -H "x-t3-setup-key: $SETUP_KEY" \
+ http://127.0.0.1:3774/__setup/context/mcp | python3 -c "
+import json, sys
+body = json.load(sys.stdin)
+shared = body['shared']
+assert isinstance(shared.get('servers'), list), shared
+for server in shared['servers']:
+ assert 'env' not in server and 'headers' not in server, server
+providers = {row['provider'] for row in body['providers']}
+assert {'claude', 'codex', 'opencode'} <= providers, providers
+"
+}
+check "shared MCP servers list without secrets" mcp_servers_list_without_secrets
+
hello_says_only_what_it_may() {
local stranger member
stranger="$(docker exec "$NAME" curl -fsS --max-time 25 http://127.0.0.1:3774/__setup/hello)" || return 1
diff --git a/tests/mcp-api.test.mjs b/tests/mcp-api.test.mjs
new file mode 100644
index 0000000..8b6a5cd
--- /dev/null
+++ b/tests/mcp-api.test.mjs
@@ -0,0 +1,79 @@
+// Check the authenticated MCP HTTP boundary in a disposable agent home.
+import assert from "node:assert/strict";
+import test from "node:test";
+import path from "node:path";
+import os from "node:os";
+import * as fs from "node:fs/promises";
+import { spawn } from "node:child_process";
+import { createServer } from "node:net";
+import { once } from "node:events";
+import { fileURLToPath } from "node:url";
+
+test("shared MCP API creates, links and removes servers without leaking secrets", { timeout: 20000 }, async (t) => {
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "t3-mcp-api-"));
+ const home = path.join(root, "home");
+ await fs.mkdir(home, { recursive: true });
+ const reservation = createServer().listen(0, "127.0.0.1");
+ await once(reservation, "listening");
+ const port = reservation.address().port;
+ await new Promise((resolve) => reservation.close(resolve));
+ const child = spawn(process.execPath, [fileURLToPath(new URL("../docker/setup/server.mjs", import.meta.url))], { env: {
+ ...process.env, HOME: home, T3CODE_HOME: path.join(home, ".t3"), T3_WORKSPACE: path.join(root, "workspace"),
+ CODEX_HOME: path.join(home, ".codex"), CLAUDE_CONFIG_DIR: path.join(home, ".claude"), XDG_CONFIG_HOME: path.join(home, ".config"),
+ MISE_DATA_DIR: path.join(home, ".local/share/mise"), MISE_STATE_DIR: path.join(home, ".local/state/mise"),
+ MISE_CONFIG_DIR: path.join(home, ".config/mise"), T3_SETUP_PORT: String(port), T3_SETUP_KEY: "mcp-fixture-key",
+ T3_SETUP_KEY_SOURCE: "env", T3_INFRA_LAUNCHER: "/bin/false", T3CODE_PORT: "1",
+ }, stdio: ["ignore", "pipe", "pipe"] });
+ let logs = "";
+ child.stdout.on("data", (chunk) => { logs += chunk; });
+ child.stderr.on("data", (chunk) => { logs += chunk; });
+ t.after(async () => {
+ if (child.exitCode === null) { const exit = once(child, "exit"); child.kill(); await exit; }
+ await fs.rm(root, { recursive: true, force: true });
+ });
+ const base = `http://127.0.0.1:${port}/__setup`;
+ let ready = false;
+ for (let i = 0; i < 200; i++) {
+ if (child.exitCode !== null) throw new Error(logs);
+ try { if ((await fetch(base + "/hello")).ok) { ready = true; break; } } catch {}
+ await new Promise((resolve) => setTimeout(resolve, 20));
+ }
+ assert.ok(ready, logs);
+ const get = (route) => fetch(base + route, { headers: { "x-t3-setup-key": "mcp-fixture-key" } });
+ const post = (route, body, headers = {}) => fetch(base + route, { method: "POST", headers: { "x-t3-setup-key": "mcp-fixture-key", "content-type": "application/json", ...headers }, body: JSON.stringify(body) });
+ assert.equal((await fetch(base + "/context/mcp")).status, 401);
+ assert.equal((await post("/context/mcp/create", { server: { name: "x", type: "local", command: ["x"] } }, { "sec-fetch-site": "cross-site" })).status, 403);
+ assert.equal((await post("/context/mcp/create", { server: { name: "Bad Name", type: "local", command: ["x"] } })).status, 400);
+ assert.equal((await post("/context/mcp/create", null)).status, 400);
+
+ const created = await (await post("/context/mcp/create", { server: { name: "playwright", type: "local", command: ["playwright-mcp", "--headless"], env: { TOKEN: "secret" } } })).json();
+ assert.equal(created.ok, true);
+ const status = await (await get("/status")).json();
+ assert.equal(status.context.mcp.shared.count, 1);
+ assert.equal(JSON.stringify(status).includes('"secret"'), false);
+ assert.deepEqual(status.context.mcp.providers.find((row) => row.provider === "codex").linked, []);
+ assert.match(status.context.mcp.providers.find((row) => row.provider === "grok").error, /unverified/);
+
+ const read = await (await get("/context/mcp/read?name=playwright")).json();
+ assert.equal(read.server.env.TOKEN, "secret");
+ assert.equal((await get("/context/mcp/read")).status, 400);
+ assert.equal((await get("/context/mcp/read?name=missing")).status, 404);
+ assert.equal((await get("/context/mcp?provider=nope")).status, 400);
+ assert.equal((await get("/context/mcp?provider=grok")).status, 403);
+
+ const link = await post("/context/mcp/link", { provider: "codex", name: "playwright", linked: true, sharedRevision: created.revision });
+ assert.equal(link.status, 200);
+ assert.match(await fs.readFile(path.join(home, ".codex", "config.toml"), "utf8"), /playwright-mcp/);
+ const codex = await (await get("/context/mcp?provider=codex")).json();
+ assert.equal(codex.shared[0].linked, true);
+ assert.equal((await post("/context/mcp/link", { provider: "codex", name: "playwright", linked: true, sharedRevision: "stale" })).status, 409);
+ const unlink = await post("/context/mcp/link", { provider: "claude", name: "playwright", linked: false, sharedRevision: created.revision });
+ assert.equal(unlink.status, 200);
+ assert.equal((await post("/context/mcp/link", { provider: "grok", name: "playwright", linked: true, sharedRevision: created.revision })).status, 403);
+
+ const saved = await post("/context/mcp/save", { name: "playwright", revision: "stale", server: { name: "playwright", type: "local", command: ["other"], env: {} } });
+ assert.equal(saved.status, 409);
+ const removed = await post("/context/mcp/remove", { name: "playwright", revision: created.revision });
+ assert.equal(removed.status, 200);
+ assert.equal((await get("/context/mcp/read?name=playwright")).status, 404);
+});
diff --git a/tests/mcp.test.mjs b/tests/mcp.test.mjs
new file mode 100644
index 0000000..c6c171a
--- /dev/null
+++ b/tests/mcp.test.mjs
@@ -0,0 +1,183 @@
+// Shared MCP servers: store validation, TOML preservation, provider
+// translation and link-state derivation.
+import assert from "node:assert/strict";
+import * as fs from "node:fs/promises";
+import { tmpdir } from "node:os";
+import path from "node:path";
+import test from "node:test";
+import { readFileSync } from "node:fs";
+import vm from "node:vm";
+import { validateMcpServer, readMcpStore, createMcpServer, saveMcpServer, removeMcpServer, redactMcpServer, mcpRevision } from "../docker/setup/mcp/store.mjs";
+import { parseMcpToml, applyMcpToml } from "../docker/setup/mcp/toml.mjs";
+import { toClaudeEntry, fromClaudeEntry, toCodexEntry, fromCodexEntry, toOpenCodeEntry, fromOpenCodeEntry, toCursorEntry, fromCursorEntry, toGeminiEntry, fromGeminiEntry, readProviderMcp, writeProviderMcpEntry } from "../docker/setup/mcp/adapters.mjs";
+import { createMcpService } from "../docker/setup/mcp.mjs";
+
+async function fixture(t) {
+ const root = await fs.mkdtemp(path.join(tmpdir(), "t3-mcp-"));
+ t.after(() => fs.rm(root, { recursive: true, force: true }));
+ return { root, home: path.join(root, "home"), baseDir: path.join(root, ".t3") };
+}
+const svcFor = (f, extraEnv = {}) => createMcpService({ home: f.home, env: { HOME: f.home, ...extraEnv }, io: fs, baseDir: f.baseDir });
+
+test("validation rejects bad names, commands, urls and secrets", () => {
+ assert.equal(validateMcpServer(null).ok, false);
+ assert.equal(validateMcpServer({ name: "Bad Name", type: "local", command: ["x"] }).ok, false);
+ assert.equal(validateMcpServer({ name: "ok", type: "local", command: [] }).ok, false);
+ assert.equal(validateMcpServer({ name: "ok", type: "local", command: ["x"], env: { "bad-key": "v" } }).ok, false);
+ assert.equal(validateMcpServer({ name: "ok", type: "remote", url: "ftp://x" }).ok, false);
+ assert.equal(validateMcpServer({ name: "ok", type: "remote", url: "https://x", headers: { bad: "a\nb" } }).ok, false);
+ const local = validateMcpServer({ name: "playwright", type: "local", command: ["playwright-mcp", "--headless"], env: { A: "1" } });
+ assert.equal(local.ok, true);
+ assert.deepEqual(local.server, { name: "playwright", type: "local", enabled: true, command: ["playwright-mcp", "--headless"], env: { A: "1" } });
+ const remote = validateMcpServer({ name: "notion", type: "remote", url: "https://mcp.notion.com/mcp", enabled: false });
+ assert.equal(remote.ok, true);
+ assert.deepEqual(remote.server.headers, {});
+});
+
+test("the shared store round-trips with revisions and conflicts", async (t) => {
+ const f = await fixture(t);
+ const svc = svcFor(f);
+ assert.equal((await svc.status()).shared.present, false);
+ const created = await svc.create({ name: "playwright", type: "local", command: ["playwright-mcp"], env: {} });
+ assert.equal(created.ok, true);
+ await assert.rejects(svc.create({ name: "playwright", type: "local", command: ["other"], env: {} }), { code: "conflict" });
+ const read = await svc.read("playwright");
+ assert.deepEqual(read.server.command, ["playwright-mcp"]);
+ const saved = await svc.save("playwright", read.revision, { name: "playwright", type: "local", command: ["playwright-mcp", "--headless"], env: {} });
+ assert.equal(saved.ok, true);
+ await assert.rejects(svc.save("playwright", read.revision, { name: "playwright", type: "local", command: ["stale"], env: {} }), { code: "conflict" });
+ await assert.rejects(svc.remove("playwright", "stale"), { code: "conflict" });
+ assert.equal((await svc.remove("playwright", saved.revision)).ok, true);
+ assert.equal((await svc.status({ refresh: true })).shared.present, false);
+ await assert.rejects(svc.read("playwright"), { code: "missing" });
+});
+
+test("status never carries secrets", async (t) => {
+ const f = await fixture(t);
+ const svc = svcFor(f);
+ await svc.create({ name: "db", type: "remote", url: "https://db.example/mcp", headers: { Authorization: "Bearer secret" } });
+ const status = await svc.status({ refresh: true });
+ assert.equal(JSON.stringify(status).includes("Bearer secret"), false);
+ assert.equal(status.shared.servers[0].secretCount, 1);
+ assert.deepEqual(status.shared.servers[0].headerKeys, ["Authorization"]);
+ const redacted = redactMcpServer({ name: "db", type: "remote", enabled: true, url: "https://db.example/mcp", headers: { Authorization: "Bearer secret" } });
+ assert.equal(redacted.headers, undefined);
+ assert.equal(redacted.headerCount, 1);
+});
+
+test("codex TOML edits preserve unrelated sections", () => {
+ const original = '# comment\nmodel = "o4-mini"\n\n[mcp_servers.playwright]\ncommand = "playwright-mcp"\nargs = ["--headless"]\n\n[other]\nkey = 1\n';
+ const parsed = parseMcpToml(original);
+ assert.deepEqual(parsed.get("playwright"), { command: "playwright-mcp", args: ["--headless"] });
+ const updated = applyMcpToml(original, "notion", { url: "https://mcp.notion.com/mcp", enabled: false });
+ assert.match(updated, /model = "o4-mini"/);
+ assert.match(updated, /\[mcp_servers\.playwright\]/);
+ assert.match(updated, /\[mcp_servers\.notion\]/);
+ assert.match(updated, /\[other\]/);
+ const removed = applyMcpToml(updated, "notion", null);
+ assert.doesNotMatch(removed, /notion/);
+ assert.match(removed, /playwright-mcp/);
+ assert.throws(() => parseMcpToml("[mcp_servers.a]\ncommand = 1\n[mcp_servers.a]\ncommand = 2\n"), /Duplicate/);
+});
+
+test("every supported provider translates a local and a remote server", () => {
+ const local = { name: "playwright", type: "local", enabled: true, command: ["playwright-mcp", "--headless"], env: { FOO: "bar" } };
+ const remote = { name: "notion", type: "remote", enabled: true, url: "https://mcp.notion.com/mcp", headers: { Authorization: "Bearer x" } };
+ assert.deepEqual(toClaudeEntry(local), { command: "playwright-mcp", args: ["--headless"], env: { FOO: "bar" } });
+ assert.deepEqual(fromClaudeEntry("playwright", toClaudeEntry(local)), local);
+ assert.deepEqual(fromClaudeEntry("notion", toClaudeEntry(remote)), remote);
+ assert.deepEqual(toCodexEntry(remote), { url: "https://mcp.notion.com/mcp", http_headers: { Authorization: "Bearer x" } });
+ assert.deepEqual(fromCodexEntry("notion", toCodexEntry(remote)), remote);
+ assert.deepEqual(toOpenCodeEntry(local), { type: "local", command: ["playwright-mcp", "--headless"], environment: { FOO: "bar" }, enabled: true });
+ assert.deepEqual(fromOpenCodeEntry("playwright", toOpenCodeEntry(local)), local);
+ assert.deepEqual(fromOpenCodeEntry("notion", toOpenCodeEntry(remote)), remote);
+ assert.deepEqual(toCursorEntry(local), toClaudeEntry(local));
+ assert.deepEqual(fromCursorEntry("playwright", toCursorEntry(local)), local);
+ const geminiRemote = toGeminiEntry(remote);
+ assert.equal(geminiRemote.httpUrl, "https://mcp.notion.com/mcp");
+ assert.deepEqual(fromGeminiEntry("notion", geminiRemote), remote);
+ assert.equal(fromClaudeEntry("x", { nonsense: true }), null);
+});
+
+test("links write provider files and derive state by comparison", async (t) => {
+ const f = await fixture(t);
+ const svc = svcFor(f, { CODEX_HOME: path.join(f.home, ".codex"), XDG_CONFIG_HOME: path.join(f.home, ".config") });
+ const created = await svc.create({ name: "playwright", type: "local", command: ["playwright-mcp", "--headless"], env: {} });
+ const revision = created.revision;
+ for (const provider of ["claude", "codex", "opencode", "cursor", "antigravity"]) {
+ await svc.setLink(provider, "playwright", true, revision);
+ }
+ const status = await svc.status({ refresh: true });
+ for (const provider of ["claude", "codex", "opencode", "cursor", "antigravity"]) {
+ const row = status.providers.find((entry) => entry.provider === provider);
+ assert.deepEqual(row.linked, ["playwright"], provider);
+ assert.deepEqual(row.diverged, [], provider);
+ }
+ // A provider-side edit shows as diverged, not linked.
+ await svc.setLink("claude", "playwright", false, revision);
+ const unlinked = (await svc.status({ refresh: true })).providers.find((entry) => entry.provider === "claude");
+ assert.deepEqual(unlinked.linked, []);
+ assert.deepEqual(unlinked.unlinked, ["playwright"]);
+ const grok = (await svc.status()).providers.find((entry) => entry.provider === "grok");
+ assert.equal(grok.supported, false);
+ assert.match(grok.error, /unverified/);
+});
+
+test("disabled shared servers link only where the provider has an enabled flag", async (t) => {
+ const f = await fixture(t);
+ const svc = svcFor(f);
+ const created = await svc.create({ name: "quiet", type: "local", enabled: false, command: ["cmd"], env: {} });
+ await svc.setLink("codex", "quiet", true, created.revision);
+ await svc.setLink("opencode", "quiet", true, created.revision);
+ assert.equal((await svc.list("codex")).shared[0].linked, true);
+ await assert.rejects(svc.setLink("claude", "quiet", true, created.revision), /no native enabled flag/);
+ await assert.rejects(svc.setLink("grok", "quiet", true, created.revision), /unverified/);
+});
+
+test("unsupported and unknown MCP shapes stay in place with a warning", async (t) => {
+ const f = await fixture(t);
+ const svc = svcFor(f);
+ await svc.create({ name: "playwright", type: "local", command: ["playwright-mcp"], env: {} });
+ await fs.mkdir(path.join(f.home, ".codex"), { recursive: true });
+ await fs.writeFile(path.join(f.home, ".codex", "config.toml"), '[mcp_servers.weird]\nstartup_timeout_ms = 5\n');
+ const status = await svc.status({ refresh: true });
+ const codex = status.providers.find((entry) => entry.provider === "codex");
+ assert.match(codex.warning ?? "", /unsupported shape/);
+ assert.deepEqual(codex.own, ["weird"]);
+ const list = await svc.list("codex");
+ assert.deepEqual(list.unsupported, ["weird"]);
+ await assert.rejects(svc.list("nope"), { code: "invalid" });
+ await assert.rejects(writeProviderMcpEntry(fs, "codex", { home: f.home, env: {} }, "Bad Name", {}), /Invalid MCP server name/);
+});
+
+test("provider files preserve unrelated keys and survive JSONC", async (t) => {
+ const f = await fixture(t);
+ const env = { HOME: f.home, XDG_CONFIG_HOME: path.join(f.home, ".config"), OPENCODE_CONFIG_DIR: path.join(f.home, ".config", "opencode") };
+ await fs.mkdir(path.join(f.home, ".config", "opencode"), { recursive: true });
+ await fs.writeFile(path.join(f.home, ".config", "opencode", "opencode.jsonc"), '{\n// comment\n"theme": "dark"\n}\n');
+ const svc = createMcpService({ home: f.home, env, io: fs, baseDir: f.baseDir });
+ const created = await svc.create({ name: "playwright", type: "local", command: ["playwright-mcp"], env: {} });
+ await svc.setLink("opencode", "playwright", true, created.revision);
+ const raw = await fs.readFile(path.join(f.home, ".config", "opencode", "opencode.jsonc"), "utf8");
+ assert.match(raw, /comment/);
+ assert.match(raw, /"theme": "dark"/);
+ assert.match(raw, /playwright/);
+ const read = await readProviderMcp(fs, "opencode", { home: f.home, env });
+ assert.equal(read.servers.find((row) => row.name === "playwright").canonical.command[0], "playwright-mcp");
+});
+
+const ctx = vm.createContext({ URL });
+vm.runInContext(`${readFileSync(new URL("../docker/setup/client/model.js", import.meta.url), "utf8")}\nthis.M = T3Model;`, ctx);
+const M = ctx.M;
+
+test("MCP view model rows say what each provider uses", () => {
+ const status = { context: { mcp: { ok: true, shared: { present: true, count: 2, servers: [] }, providers: [
+ { provider: "codex", linked: ["playwright"], unlinked: ["notion"], diverged: [], ownCount: 1, supported: true },
+ { provider: "grok", supported: false, error: "unverified", linked: [], unlinked: ["playwright", "notion"], ownCount: 0 },
+ ] } } };
+ assert.equal(M.mcpView(status).count, 2);
+ assert.equal(M.mcpSharedRow(status).status.text, "2 servers · used by Codex (1)");
+ const [codex, grok] = M.mcpProviderRows(status);
+ assert.equal(codex.status.text, "Uses playwright · +1 own server");
+ assert.match(grok.status.text, /unverified/);
+});
From 9dacdd43347d1576826f77676b548fb520326831 Mon Sep 17 00:00:00 2001
From: 0libote <79215282+0libote@users.noreply.github.com>
Date: Mon, 19 Oct 2026 22:35:33 +0100
Subject: [PATCH 2/6] fix(context): harden shared MCP writes and verify native
providers
---
CONTRIBUTING.md | 8 +
docker/bin/t3-browser-mcp | 67 ++---
docker/entrypoint.sh | 11 +-
docker/setup/app.js | 1 +
docker/setup/client/mcp.js | 95 +++----
docker/setup/client/model.js | 2 +-
docker/setup/client/routes.js | 2 +-
docker/setup/console.css | 6 +
docker/setup/design/ui.js | 2 +
docker/setup/mcp.mjs | 43 +--
docker/setup/mcp/adapters.mjs | 350 ++++++++-----------------
docker/setup/mcp/files.mjs | 48 ++++
docker/setup/mcp/store.mjs | 43 ++-
docker/setup/mcp/toml.mjs | 220 +++++-----------
docker/setup/server.mjs | 3 +-
docker/setup/vendor/README.md | 25 ++
docker/setup/vendor/toml-patch.LICENSE | 22 ++
docker/setup/vendor/toml-patch.mjs | 56 ++++
docker/t3-client/setup-bridge.js | 3 +-
docs/agent-browser.md | 6 +-
docs/context-management.md | 113 +++++---
docs/media/mcp/desktop-dark.png | Bin 0 -> 419010 bytes
docs/media/mcp/desktop-light.png | Bin 0 -> 374535 bytes
docs/media/mcp/phone-dark.png | Bin 0 -> 267539 bytes
docs/media/mcp/phone-light.png | Bin 0 -> 291191 bytes
docs/setup-page.md | 8 +-
scripts/console-screens.js | 8 +-
scripts/fixtures/console/mcp.json | 23 ++
scripts/lib/console-fixtures.cjs | 15 ++
scripts/mcp-native-smoke.mjs | 124 +++++++++
scripts/mcp-ui-audit.js | 116 ++++++++
scripts/smoke-test.sh | 24 ++
scripts/ui-audit.js | 4 +
tests/mcp-api.test.mjs | 14 +-
tests/mcp-regressions.test.mjs | 172 ++++++++++++
tests/mcp.test.mjs | 17 +-
tests/persist-credentials.test.sh | 10 +
tests/t3-client.test.mjs | 3 +-
38 files changed, 1080 insertions(+), 584 deletions(-)
create mode 100644 docker/setup/mcp/files.mjs
create mode 100644 docker/setup/vendor/toml-patch.LICENSE
create mode 100644 docker/setup/vendor/toml-patch.mjs
create mode 100644 docs/media/mcp/desktop-dark.png
create mode 100644 docs/media/mcp/desktop-light.png
create mode 100644 docs/media/mcp/phone-dark.png
create mode 100644 docs/media/mcp/phone-light.png
create mode 100644 scripts/fixtures/console/mcp.json
create mode 100644 scripts/mcp-native-smoke.mjs
create mode 100644 scripts/mcp-ui-audit.js
create mode 100644 tests/mcp-regressions.test.mjs
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b3d67b8..ea4f930 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -38,6 +38,14 @@ Issues and pull requests are welcome. Before opening one:
set `T3_STORAGE_UI_OUTPUT` to change that location. The development console
isolates its home, mise state, and caches under `/tmp/t3-dev/home`.
+For shared MCP changes, run `node scripts/mcp-native-smoke.mjs PROVIDER
+/absolute/path/to/executable` for Claude, Codex, OpenCode (both V1 and V2) and
+Cursor. It uses disposable homes and fixture servers without model requests.
+`node scripts/mcp-ui-audit.js` checks the MCP tab's apply/unlink controls,
+revisions, JSON values and failed/late reads in both themes at phone, desktop
+and embedded sizes. It uses response fixtures, with screenshots under
+`/tmp/t3-mcp-ui-audit`. The standard screenshot matrix includes `instructions/mcp`.
+
For OpenCode credential changes, run
`node scripts/opencode-auth-smoke.mjs /absolute/path/to/opencode` against both
1.x and 2.x (including 2.0.18, the supported V2 floor). It uses an isolated
diff --git a/docker/bin/t3-browser-mcp b/docker/bin/t3-browser-mcp
index bd6cf85..172cdf5 100644
--- a/docker/bin/t3-browser-mcp
+++ b/docker/bin/t3-browser-mcp
@@ -50,8 +50,8 @@ Notes:
* Chromium is sandboxed by the container already, so --no-sandbox is passed.
* chrome-devtools-mcp officially supports Google Chrome rather than Debian's
Chromium; it does work here, but playwright is the tested default.
- * Each harness is registered through its managed executable. Install the
- harness (setup page or `t3-harness install`) before registering MCP with it.
+ * Install the managed harness (setup page or `t3-harness install`) first.
+ Claude uses its native CLI; Codex/OpenCode use the setup config adapters.
USAGE
}
@@ -105,8 +105,6 @@ harness_exec() { # id
fi
}
-json_args="$(printf '%s\n' "${args[@]}" | jq -R . | jq -s .)"
-
register_claude() {
local bin
bin="$(harness_exec claude)"
@@ -124,60 +122,33 @@ register_claude() {
fi
}
+# Use the console's adapters so both entry points agree on JSONC precedence,
+# OpenCode V2 nesting, TOML sections, backups and file symlinks.
+write_browser_entry() {
+ "$T3_INFRA_NODE" --input-type=module - "$1" "$name" "$action" "$cmd" "${args[@]}" <<'NODE'
+import * as io from "node:fs/promises";
+const { writeProviderMcpEntry } = await import(process.env.T3_MCP_ADAPTERS_MODULE || "file:///opt/t3-setup/mcp/adapters.mjs");
+const [provider, name, action, command, ...args] = process.argv.slice(2);
+const entry = action === "remove" ? null : provider === "codex"
+ ? { command, args } : { type: "local", command: [command, ...args], enabled: true };
+await writeProviderMcpEntry(io, provider, { home: process.env.HOME, env: process.env }, name, entry);
+NODE
+}
+
register_codex() {
local bin
bin="$(harness_exec codex)"
[ -n "$bin" ] || { echo " codex: not installed"; return; }
- local config="${CODEX_HOME:-$HOME/.codex}/config.toml"
- mkdir -p "$(dirname "$config")"
- touch "$config"
-
- # Drop any block we previously wrote, then re-add if needed.
- local tmp; tmp="$(mktemp)"
- awk -v marker="# >>> t3-browser-mcp:${name} >>>" \
- -v endmarker="# <<< t3-browser-mcp:${name} <<<" '
- $0 == marker { skip = 1 }
- skip != 1 { print }
- $0 == endmarker { skip = 0 }
- ' "$config" > "$tmp"
- mv "$tmp" "$config"
-
- if [ "$action" = "remove" ]; then
- echo " codex: removed $name"
- return
- fi
-
- {
- printf '\n# >>> t3-browser-mcp:%s >>>\n' "$name"
- printf '[mcp_servers.%s]\n' "$name"
- printf 'command = "%s"\n' "$cmd"
- printf 'args = %s\n' "$(printf '%s' "$json_args" | jq -c .)"
- printf '# <<< t3-browser-mcp:%s <<<\n' "$name"
- } >> "$config"
- echo " codex: registered $name in $config (via $bin)"
+ write_browser_entry codex
+ echo " codex: $action $name (via $bin)"
}
register_opencode() {
local bin
bin="$(harness_exec opencode)"
[ -n "$bin" ] || { echo " opencode: not installed"; return; }
- local config="${XDG_CONFIG_HOME:-$HOME/.config}/opencode/opencode.json"
- mkdir -p "$(dirname "$config")"
- [ -s "$config" ] || echo '{"$schema":"https://opencode.ai/config.json"}' > "$config"
-
- local tmp; tmp="$(mktemp)"
- if [ "$action" = "remove" ]; then
- jq --arg n "$name" 'del(.mcp[$n])' "$config" > "$tmp"
- mv "$tmp" "$config"
- echo " opencode: removed $name"
- return
- fi
-
- jq --arg n "$name" --arg c "$cmd" --argjson a "$json_args" \
- '.mcp[$n] = {type: "local", command: ([$c] + $a), enabled: true}' \
- "$config" > "$tmp"
- mv "$tmp" "$config"
- echo " opencode: registered $name in $config (via $bin)"
+ write_browser_entry opencode
+ echo " opencode: $action $name (via $bin)"
}
printf '%s %s (%s)\n' \
diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh
index 7f79347..56ab57a 100644
--- a/docker/entrypoint.sh
+++ b/docker/entrypoint.sh
@@ -268,9 +268,9 @@ register_projects
# and az keep their sign-ins in these.
AGENT_DIRS=".claude .codex .cursor .grok .config/opencode .local/share/opencode .config/muse .local/share/muse .agents/skills .local/state/skills .gemini/config/skills .gemini/antigravity-cli/skills"
AGENT_DIRS="$AGENT_DIRS .config/gh .config/glab-cli .config/tea .local/share/forgejo-cli .azure"
-# And git's own settings: who you commit as, and the credential helpers gh and
-# tea set up so an HTTPS push uses their sign-in. git writes through the link.
-AGENT_FILES=".gitconfig .agents/.skill-lock.json"
+# And git's own settings, plus Claude's global account/MCP config outside its
+# config directory. Git and Claude's native writer preserve these file links.
+AGENT_FILES=".gitconfig .agents/.skill-lock.json .claude.json"
persist_agent_credentials() {
[ "$T3_PERSIST_AGENT_CREDENTIALS" = "1" ] || return 0
@@ -299,7 +299,10 @@ persist_agent_credentials() {
# The skills CLI owns its JSON lock format. An absent lock stays absent;
# its future write follows the link onto the volume without an empty or
# invented JSON document. This does not move the rest of .agents.
- if [ "$rel" != ".agents/.skill-lock.json" ]; then
+ if [ "$rel" = ".claude.json" ]; then
+ [ -e "$dst" ] || printf '{}\n' > "$dst"
+ chmod 600 "$dst"
+ elif [ "$rel" != ".agents/.skill-lock.json" ]; then
[ -e "$dst" ] || : > "$dst"
fi
;;
diff --git a/docker/setup/app.js b/docker/setup/app.js
index cf19616..183a727 100644
--- a/docker/setup/app.js
+++ b/docker/setup/app.js
@@ -3420,6 +3420,7 @@
'mcp.view': (a) => mcpUI.view(a.id),
'mcp.add': () => mcpUI.add(),
'mcp.link': (a) => mcpUI.toggle(a.provider, a.id),
+ 'mcp.apply': (a) => mcpUI.apply(a.provider, a.id),
'storage.goto': () => go('storage'),
refresh: () => { loadStatus(); loadPorts(); },
goto: (a) => go(a.route),
diff --git a/docker/setup/client/mcp.js b/docker/setup/client/mcp.js
index dac8417..36a5f64 100644
--- a/docker/setup/client/mcp.js
+++ b/docker/setup/client/mcp.js
@@ -9,6 +9,7 @@ const T3McpUI = (() => {
const create = ({ api, getStatus, render }) => {
let shared = null, loading = false, error = null, generation = 0, viewer = null, linking = null;
+ const snapshot = () => ({ ...getStatus(), context: { ...getStatus()?.context, mcp: shared || getStatus()?.context?.mcp } });
const load = async (refresh = false) => {
if (loading) return;
@@ -22,18 +23,12 @@ const T3McpUI = (() => {
render();
};
- const parsePairs = (text) => {
- const out = {};
- for (const line of String(text || '').split('\n')) {
- const trimmed = line.trim();
- if (!trimmed || trimmed.startsWith('#')) continue;
- const at = trimmed.indexOf('=');
- if (at < 0) throw new Error('Use KEY=value, one per line: ' + trimmed);
- out[trimmed.slice(0, at).trim()] = trimmed.slice(at + 1);
- }
+ const parseJson = (text, array) => {
+ let out;
+ try { out = JSON.parse(text); } catch { throw new Error(array ? 'Enter the command as a JSON array of strings.' : 'Enter values as a JSON object of strings.'); }
+ if (!out || typeof out !== 'object' || Array.isArray(out) !== array || !Object.values(out).every((value) => typeof value === 'string')) throw new Error(array ? 'Enter a JSON array of strings.' : 'Enter a JSON object of strings.');
return out;
};
- const formatPairs = (obj) => Object.entries(obj || {}).map(([key, value]) => key + '=' + value).join('\n');
const serverRow = (server) => {
const detail = server.type === 'local'
@@ -44,7 +39,7 @@ const T3McpUI = (() => {
@@ -59,23 +54,26 @@ const T3McpUI = (() => {
${icon('bot')}
Grok Build and Muse Code layouts are unverified and not managed here. Codex and OpenCode honor disabled servers; other providers represent disabled by unlinking.
+ ${rows.length ? html`
Global provider configurations
${rows.map(providerRow)}
` : ''}
+ Provider support and authentication
Manages default global config files, not custom T3 provider-instance homes or project overrides. A matching configuration does not establish a live connection. Codex and OpenCode support disabled definitions; unlink for Claude and Cursor. Antigravity, Grok and Muse are not managed here. Grok may inherit Claude/Cursor servers through its compatibility imports.
Remote servers use Streamable HTTP. OAuth sign-in stays with each provider: use claude mcp login, codex mcp login, opencode mcp auth or cursor-agent mcp login when required.
` : ''}
-
${icon('info', 'tc-icon--sm')}Shared servers sync into each linked provider’s own config, leaving its other servers alone. Start a new session after you change links.
+
${icon('info', 'tc-icon--sm')}Save a definition, then apply it to each provider. Existing copies stay unchanged until you apply or unlink. Switches show configuration presence, including disabled entries. Start a new provider session to load changes.
Secrets are stored in the shared store (0600) and written into linked provider configs. They never appear in status polls. Put secrets in Environment/Headers, not in the command line: the command is shown in the list.
+ `}
Environment and header values load only here. URL paths and queries are hidden in lists. Commands are shown in lists: put secrets in Environment or Headers. Provider-native variable interpolation rules still apply.
${!creating ? html`` : ''}
-
`,
+ `,
});
viewer = current;
const panel = current.layer.panel;
@@ -130,7 +128,7 @@ const T3McpUI = (() => {
if (event.target.id === 'mcp-name') current.draft.name = event.target.value;
if (event.target.id === 'mcp-url') current.draft.url = event.target.value;
if (event.target.id === 'mcp-command') current.draft.command = event.target.value;
- if (event.target.id === 'mcp-pairs') current.draft.pairs = event.target.value;
+ if (event.target.id === 'mcp-pairs') current.draft[current.draft.type === 'local' ? 'env' : 'headers'] = event.target.value;
});
panel.addEventListener('change', (event) => {
if (event.target.id === 'mcp-type') { current.draft.type = event.target.value; sync(); }
@@ -141,10 +139,10 @@ const T3McpUI = (() => {
if (button.hasAttribute('data-mcp') && button.getAttribute('data-mcp') === 'close') { current.layer.close('close'); return; }
if (button.id === 'mcp-enabled') { current.draft.enabled = !current.draft.enabled; button.setAttribute('aria-checked', String(current.draft.enabled)); return; }
if (button.getAttribute('data-mcp') === 'remove') {
- const yes = await Kit.confirm({ title: 'Remove ' + current.name + '?', body: 'Removes the shared entry. Linked provider entries stay until you unlink them; unlink first to remove everywhere at once.', confirm: 'Remove server', tone: 'danger' });
+ const yes = await Kit.confirm({ title: 'Remove ' + current.name + '?', body: 'Unlink this server from all supported providers first. Removing the definition does not remove unmanaged compatibility copies or existing sessions.', confirm: 'Remove server', tone: 'danger' });
if (!yes || viewer !== current || current.saving) return;
current.saving = true; current.error = null; current.layer.render();
- const response = await api('/context/mcp/remove', { body: { name: current.name, revision: shared?.shared?.revision } });
+ const response = await api('/context/mcp/remove', { body: { name: current.name, revision: current.revision } });
current.saving = false;
if (!response.ok) { if (viewer === current) { current.error = response.error; current.layer.render(); } return; }
if (viewer === current) current.layer.close('saved');
@@ -154,16 +152,16 @@ const T3McpUI = (() => {
}
if (button.getAttribute('data-mcp') === 'save') {
try {
- const pairs = parsePairs(current.draft.pairs);
+ const pairs = parseJson(current.draft[current.draft.type === 'local' ? 'env' : 'headers'], false);
const server = current.draft.type === 'local'
? { name: (current.draft.name || '').trim(), type: 'local', enabled: current.draft.enabled,
- command: String(current.draft.command || '').split('\n').map((line) => line.trim()).filter(Boolean), env: pairs }
+ command: parseJson(current.draft.command, true), env: pairs }
: { name: (current.draft.name || '').trim(), type: 'remote', enabled: current.draft.enabled,
url: (current.draft.url || '').trim(), headers: pairs };
current.saving = true; current.error = null; current.layer.render();
const response = creating
? await api('/context/mcp/create', { body: { server } })
- : await api('/context/mcp/save', { body: { name: current.name, revision: shared?.shared?.revision, server } });
+ : await api('/context/mcp/save', { body: { name: current.name, revision: current.revision, server } });
current.saving = false;
if (!response.ok) { if (viewer === current) { current.error = response.error; current.layer.render(); } return; }
if (viewer === current) current.layer.close('saved');
@@ -180,28 +178,37 @@ const T3McpUI = (() => {
current.loading = false;
if (!response.ok) { current.error = response.error; current.layer.render(); return; }
const server = response.data.server;
+ current.revision = response.data.revision;
current.data = server;
current.draft = { name: server.name, type: server.type, enabled: server.enabled !== false,
- command: (server.command || []).join('\n'), url: server.url || '',
- pairs: formatPairs(server.type === 'local' ? server.env : server.headers) };
+ command: JSON.stringify(server.command || [], null, 2), url: server.url || '',
+ env: JSON.stringify(server.env || {}, null, 2), headers: JSON.stringify(server.headers || {}, null, 2) };
current.layer.render();
};
- const setLink = async (provider, name) => {
+ const setLink = async (provider, name, apply = false) => {
if (linking) return;
- const current = getStatus()?.context?.mcp?.providers?.find((row) => row.provider === provider);
- const linked = !(current?.linked || []).includes(name);
+ const current = shared?.providers?.find((row) => row.provider === provider);
+ const sharedRevision = shared?.shared?.revision;
+ if (!current?.revision) return;
+ const differs = (current.diverged || []).includes(name);
+ const linked = apply || ![...(current.linked || []), ...(current.diverged || [])].includes(name);
+ if (differs || !linked) {
+ const yes = await Kit.confirm({ title: linked ? 'Apply shared ' + name + '?' : 'Unlink ' + name + '?', body: linked ? 'Replaces this provider’s complete entry, including any provider-only options. A private backup of its config is kept beside the file.' : 'Removes this entry from the provider’s global config. A private backup is kept. Existing sessions and project or compatibility definitions can still use the server.', confirm: linked ? 'Apply shared' : 'Unlink', tone: linked ? 'primary' : 'danger' });
+ if (!yes || linking) return;
+ }
linking = provider; render();
- const response = await api('/context/mcp/link', { body: { provider, name, linked, sharedRevision: shared?.shared?.revision } });
+ const response = await api('/context/mcp/link', { body: { provider, name, linked, sharedRevision, providerRevision: current.revision } });
linking = null;
if (!response.ok) Kit.toast('Could not change MCP link', { tone: 'danger', detail: response.error });
- else Kit.toast(linked ? providerName(provider) + ' now uses ' + name : providerName(provider) + ' stopped using ' + name);
+ else Kit.toast(linked ? 'Applied ' + name + ' to ' + providerName(provider) : 'Removed ' + name + ' from ' + providerName(provider) + ' global config');
await load(true);
};
return { content, load, refresh: () => load(true), add: () => open(null), view: (id) => open(id),
activate: () => { if (!shared && !loading && !error) load(); },
toggle: (provider, name) => setLink(provider, name),
+ apply: (provider, name) => setLink(provider, name, true),
select: () => {},
};
};
diff --git a/docker/setup/client/model.js b/docker/setup/client/model.js
index f212822..c522c86 100644
--- a/docker/setup/client/model.js
+++ b/docker/setup/client/model.js
@@ -1930,7 +1930,7 @@ const T3Model = (() => {
: (row.linked || []).length ? { text: 'Uses ' + listOf(row.linked) + ((row.ownCount || 0) ? ' · +' + plural(row.ownCount, 'own server') : '') }
: (row.ownCount || 0) ? { text: plural(row.ownCount, 'own server') + ' · none shared' }
: { text: 'No shared servers linked' },
- note: row.warning || null, path: row.path || null,
+ note: row.warning || null, path: row.path || null, revision: row.revision || null,
linked: row.linked || [], unlinked: row.unlinked || [], diverged: row.diverged || [],
ownCount: row.ownCount || 0, supported: row.supported !== false, error: row.error || null,
}));
diff --git a/docker/setup/client/routes.js b/docker/setup/client/routes.js
index fa6e045..4f07780 100644
--- a/docker/setup/client/routes.js
+++ b/docker/setup/client/routes.js
@@ -22,7 +22,7 @@ const T3Routes = (() => {
{ route: 'agents', title: 'Agents', icon: 'bot', key: 'a', group: 'agents',
summary: 'Install, sign in and update your coding agents.', doc: 'agents.md' },
{ route: 'instructions', title: 'Instructions & skills', icon: 'book-open', key: 'i', group: 'agents',
- summary: 'What your agents read at the start of every session.', doc: 'context-management.md' },
+ summary: 'Global instructions, skills and shared MCP server definitions.', doc: 'context-management.md' },
{ route: 'toolchains', title: 'Toolchains', icon: 'wrench', key: 't', group: 'tools',
summary: 'Languages and tools on every agent’s path, at exact versions.', doc: 'toolchains.md' },
{ route: 'sourcecontrol', title: 'Source control', icon: 'git-pull-request', key: 's', group: 'tools',
diff --git a/docker/setup/console.css b/docker/setup/console.css
index 9579950..1bdd60b 100644
--- a/docker/setup/console.css
+++ b/docker/setup/console.css
@@ -638,6 +638,12 @@ html.tc-locked { overflow: hidden; }
.tc-context-sheet .tc-sheet-head, .tc-context-sheet .tc-sheet-foot { flex-shrink: 0; }
.tc-context-sheet .tc-sheet-foot { flex-wrap: wrap; }
.tc-context-sheet .tc-input--area { box-sizing: border-box; width: 100%; min-height: min(42dvh, 420px); resize: vertical; font-family: var(--font-mono); line-height: 1.6; }
+.tc-mcp-editor .tc-sheet-body { display: flex; flex-direction: column; }
+.tc-mcp-editor textarea.tc-input { flex: none; width: 100%; min-height: 88px; height: auto; resize: vertical; line-height: 1.5; }
+.tc-mcp-control { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
+.tc-mcp-providers { border-top: 1px solid var(--border); }
+.tc-mcp-providers > .tc-label { padding: 12px 16px; }
+.tc-mcp .tc-hint.tc-wrap { overflow-wrap: anywhere; white-space: normal; }
.tc-context-path { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--font-mono); }
.tc-editor-toolbar { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: var(--space-2) var(--space-3); }
.tc-editor-toolbar .tc-label { margin: 0; }
diff --git a/docker/setup/design/ui.js b/docker/setup/design/ui.js
index 15482ab..3a24be7 100644
--- a/docker/setup/design/ui.js
+++ b/docker/setup/design/ui.js
@@ -3,6 +3,7 @@
shipped console (docker/setup/app.js). Markup carries the state; this file only
wires icons and the few interactions every surface shares. */
(function () {
+ // Lucide plug, used by the shared MCP list and editor.
var ICONS = {"layout-dashboard": "", "smartphone": "", "bot": "", "wrench": "", "git-pull-request": "", "ethernet-port": "", "settings-2": "", "search": "", "command": "", "copy": "", "check": "", "x": "", "external-link": "", "qr-code": "", "ellipsis": "", "circle-arrow-up": "", "download": "", "trash-2": "", "key-round": "", "log-in": "", "plus": "", "refresh-cw": "", "circle-alert": "", "triangle-alert": "", "info": "", "circle-check": "", "loader-circle": "", "sun": "", "moon": "", "monitor": "", "chevron-right": "", "chevron-down": "", "lock": "", "link": "", "globe": "", "circle-stop": "", "activity": "", "terminal": "", "server": "", "shield-check": "", "eye": "", "eye-off": "", "laptop": "", "log-out": "", "arrow-right": "", "history": ""};
var SAMPLE = { qrPair: "\n", qrPort: "\n" };
@@ -41,6 +42,7 @@
ICONS.sparkles = '';
ICONS['file-text'] = '';
+ ICONS.plug = '';
function agentMark(name, cls) {
var mark = AGENT_MARKS[name];
if (!mark) return '';
diff --git a/docker/setup/mcp.mjs b/docker/setup/mcp.mjs
index 9df5e73..d43d583 100644
--- a/docker/setup/mcp.mjs
+++ b/docker/setup/mcp.mjs
@@ -1,22 +1,21 @@
// Shared MCP servers: canonical store plus per-provider link state derived
// by comparison (no new manifest format). No MCP execution here.
import path from "node:path";
-import { mcpStorePathFor, readMcpStore, createMcpServer, saveMcpServer, removeMcpServer, redactMcpServer, validateMcpServer, MCP_PROVIDERS, UNSUPPORTED_MCP_PROVIDERS } from "./mcp/store.mjs";
-import { readProviderMcp, writeProviderMcpEntry, toClaudeEntry, toCodexEntry, toOpenCodeEntry, toCursorEntry, toGeminiEntry, isSupportedMcpProvider } from "./mcp/adapters.mjs";
+import { mcpStorePathFor, readMcpStore, createMcpServer, saveMcpServer, removeMcpServer, redactMcpServer, validateMcpServer, MCP_PROVIDERS, UNSUPPORTED_MCP_PROVIDERS, withStore } from "./mcp/store.mjs";
+import { readProviderMcp, writeProviderMcpEntry, toClaudeEntry, toCodexEntry, toOpenCodeEntry, toCursorEntry, isSupportedMcpProvider } from "./mcp/adapters.mjs";
const problem = (code, message) => Object.assign(new Error(message), { code });
const summaryError = (error) => String(error?.message || error).slice(0, 240);
export function mcpProviders() {
- return [...Object.entries(MCP_PROVIDERS).map(([id, name]) => ({ id, name })), ...Object.entries(UNSUPPORTED_MCP_PROVIDERS).map(([id, reason]) => ({ id, name: id === "grok" ? "Grok Build" : "Muse Code", unsupported: reason }))];
+ return [...Object.entries(MCP_PROVIDERS).map(([id, name]) => ({ id, name })), ...Object.entries(UNSUPPORTED_MCP_PROVIDERS).map(([id, reason]) => ({ id, name: { grok: "Grok Build", muse: "Muse Code", antigravity: "Antigravity" }[id], unsupported: reason }))];
}
const toEntryFor = (provider, server) =>
provider === "claude" ? toClaudeEntry(server)
: provider === "codex" ? toCodexEntry(server)
: provider === "opencode" ? toOpenCodeEntry(server)
- : provider === "cursor" ? toCursorEntry(server)
- : toGeminiEntry(server);
+ : toCursorEntry(server);
const normalize = (value) => {
if (Array.isArray(value)) return value.map(normalize);
@@ -26,8 +25,8 @@ const normalize = (value) => {
return value;
};
const canonicalEqual = (a, b) => JSON.stringify(normalize(a)) === JSON.stringify(normalize(b));
-// Native `enabled` exists for Codex and OpenCode only. Other providers link
-// by content; a disabled shared server stays unlinked there with a reason.
+// Apply disabled definitions only to Codex/OpenCode. A surviving old copy
+// elsewhere remains visible as differing until explicitly unlinked.
const linkable = (provider, server) => server.enabled !== false || provider === "codex" || provider === "opencode";
export function createMcpService({ home, env = process.env, io, baseDir, ttlMs = 15000 } = {}) {
@@ -37,7 +36,7 @@ export function createMcpService({ home, env = process.env, io, baseDir, ttlMs =
const statusCache = { at: 0, promise: null };
const linkState = async (provider, shared) => {
- if (UNSUPPORTED_MCP_PROVIDERS[provider]) return { provider, supported: false, error: UNSUPPORTED_MCP_PROVIDERS[provider], linked: [], unlinked: shared.map((s) => s.name), own: 0 };
+ if (UNSUPPORTED_MCP_PROVIDERS[provider]) return { provider, supported: false, error: UNSUPPORTED_MCP_PROVIDERS[provider], linked: [], unlinked: shared.map((s) => s.name), diverged: [], own: [], ownCount: 0 };
if (!isSupportedMcpProvider(provider)) throw problem("invalid", "Unknown MCP provider.");
try {
const read = await readProviderMcp(io, provider, { home, env });
@@ -46,14 +45,13 @@ export function createMcpService({ home, env = process.env, io, baseDir, ttlMs =
for (const server of shared) {
const row = byName.get(server.name);
if (!row) { unlinked.push(server.name); continue; }
- if (row.canonical && canonicalEqual(row.canonical, server)) { linked.push(server.name); continue; }
- if (!linkable(provider, server)) { unlinked.push(server.name); continue; }
+ if (row.canonical && !row.extra && canonicalEqual(row.canonical, server)) { linked.push(server.name); continue; }
// Content differs: a provider-side edit, or a shared edit not yet applied.
diverged.push(server.name);
}
const sharedNames = new Set(shared.map((server) => server.name));
const own = read.servers.filter((row) => !sharedNames.has(row.name)).map((row) => row.name);
- return { provider, supported: true, path: read.path, present: read.present, linked, unlinked, diverged,
+ return { provider, supported: true, path: read.path, present: read.present, revision: read.revision, linked, unlinked, diverged,
own, ownCount: own.length, error: null,
warning: read.servers.some((row) => !row.canonical) ? "One or more provider entries use an unsupported shape and are left in place." : null };
} catch (error) {
@@ -88,10 +86,10 @@ export function createMcpService({ home, env = process.env, io, baseDir, ttlMs =
let detail = null;
try { detail = await readProviderMcp(io, provider, { home, env }); }
catch (error) { throw problem(error?.code || "unavailable", summaryError(error)); }
- return { ok: true, provider, path: detail.path, sharedRevision: store.revision,
+ return { ok: true, provider, path: detail.path, revision: detail.revision, sharedRevision: store.revision,
shared: store.servers.map((server) => {
const row = detail.servers.find((entry) => entry.name === server.name);
- const linked = Boolean(row?.canonical && canonicalEqual(row.canonical, server));
+ const linked = Boolean(row?.canonical && !row.extra && canonicalEqual(row.canonical, server));
return { ...redactMcpServer(server),
linked,
diverged: Boolean(row && !linked),
@@ -112,7 +110,7 @@ export function createMcpService({ home, env = process.env, io, baseDir, ttlMs =
return { ok: true, server, revision: store.revision };
};
- const setLink = async (provider, name, linked, sharedRevision) => {
+ const setLink = async (provider, name, linked, sharedRevision, providerRevision) => withStore(mcpStorePathFor(baseDir), async () => {
if (UNSUPPORTED_MCP_PROVIDERS[provider]) throw problem("unsupported", UNSUPPORTED_MCP_PROVIDERS[provider]);
if (!isSupportedMcpProvider(provider)) throw problem("invalid", "Unknown MCP provider.");
if (typeof linked !== "boolean") throw problem("invalid", "Expected a boolean linked value.");
@@ -125,16 +123,25 @@ export function createMcpService({ home, env = process.env, io, baseDir, ttlMs =
if (linked && !linkable(provider, server)) {
throw problem("unsupported", "This provider has no native enabled flag; disable is represented by unlinking. Unlink instead.");
}
- await writeProviderMcpEntry(io, provider, { home, env }, name, linked ? toEntryFor(provider, server) : null);
+ const result = await writeProviderMcpEntry(io, provider, { home, env }, name, linked ? toEntryFor(provider, server) : null, { revision: providerRevision });
invalidate();
- return { ok: true, provider, name, linked };
- };
+ return { ...result, ok: true, provider, name, linked };
+ });
return {
status, list, read, setLink, providers: mcpProviders,
create: async (...args) => { const result = await createMcpServer(io, baseDir, ...args); invalidate(); if (!result.ok && !result.code) throw problem("invalid", result.error); if (!result.ok) throw problem(result.code || "invalid", result.error); return { ok: true, ...result }; },
save: async (...args) => { const result = await saveMcpServer(io, baseDir, ...args); invalidate(); if (!result.ok && !result.code) throw problem("invalid", result.error); if (!result.ok) throw problem(result.code || "invalid", result.error); return { ok: true, ...result }; },
- remove: async (...args) => { const result = await removeMcpServer(io, baseDir, ...args); invalidate(); if (!result.ok) throw problem(result.code || "invalid", result.error); return { ok: true, ...result }; },
+ remove: async (name, revision) => {
+ const check = async () => {
+ for (const provider of Object.keys(MCP_PROVIDERS)) {
+ const detail = await readProviderMcp(io, provider, { home, env });
+ if (detail.servers.some((row) => row.name === name)) throw problem("conflict", "Unlink this server from every provider before removing its shared definition.");
+ }
+ };
+ const result = await removeMcpServer(io, baseDir, name, revision, check); invalidate();
+ if (!result.ok) throw problem(result.code || "invalid", result.error); return { ok: true, ...result };
+ },
validate: validateMcpServer,
};
}
diff --git a/docker/setup/mcp/adapters.mjs b/docker/setup/mcp/adapters.mjs
index 02d58f2..7a223af 100644
--- a/docker/setup/mcp/adapters.mjs
+++ b/docker/setup/mcp/adapters.mjs
@@ -1,282 +1,142 @@
-// Per-provider MCP file adapters. File-based only (no harness CLI): the
-// console reads bounded JSON/TOML, merges one server entry, and preserves
-// every unrelated key, comment and section.
+// Provider-native config translation. Inventory is read-only; explicit apply
+// and unlink use revision-checked, symlink-preserving native-settings writes.
import path from "node:path";
import { parse, parseTree, modify, applyEdits } from "../vendor/jsonc.mjs";
import { parseMcpToml, applyMcpToml } from "./toml.mjs";
+import { readConfig, changeConfig } from "./files.mjs";
+import { validateMcpServer } from "./store.mjs";
-const MAX_FILE_BYTES = 1024 * 1024;
const problem = (code, message) => Object.assign(new Error(message), { code });
-
+const object = (value) => value && typeof value === "object" && !Array.isArray(value);
const homeOf = (home, env) => path.resolve(home || env.HOME || "/home/t3");
-const readBounded = async (io, file) => {
- const stat = await io.stat(file);
- if (!stat.isFile()) throw problem("invalid", "The MCP configuration must be a regular file.");
- if (stat.size > MAX_FILE_BYTES) throw problem("too_large", "The MCP configuration exceeds the 1 MiB limit.");
- return io.readFile(file, "utf8");
-};
-const readJsonFile = async (io, file) => {
- let raw;
- try { raw = await readBounded(io, file); }
- catch (error) { if (error?.code === "ENOENT") return { present: false, raw: null, value: {} }; throw error; }
- let value;
- try { value = JSON.parse(raw); }
- catch { throw problem("unsupported", "The MCP configuration is not valid JSON. Repair it before changing MCP servers."); }
- if (!value || typeof value !== "object" || Array.isArray(value)) throw problem("unsupported", "The MCP configuration must be a JSON object.");
- return { present: true, raw, value };
-};
-const jsoncKeys = (file, strict) => {
- try {
- const errors = [];
- const tree = parseTree(file.raw, errors, { allowTrailingComma: !strict, disallowComments: strict });
- if (errors.length || tree?.type !== "object") throw new Error("invalid");
- return parse(file.raw, [], { allowTrailingComma: !strict, disallowComments: strict });
- } catch { throw problem("unsupported", "The MCP configuration contains invalid JSON/JSONC. Repair it before changing MCP servers."); }
-};
-
-// --- config paths ------------------------------------------------------------
-export const claudeMcpPath = (home, env) => path.join(homeOf(home, env), ".claude.json");
+export const claudeMcpPath = (home, env) => env.CLAUDE_CONFIG_DIR?.trim()
+ ? path.join(path.resolve(homeOf(home, env), env.CLAUDE_CONFIG_DIR), ".claude.json") : path.join(homeOf(home, env), ".claude.json");
export const codexMcpPath = (home, env) => path.join(path.resolve(homeOf(home, env), env.CODEX_HOME || ".codex"), "config.toml");
+export const cursorMcpPath = (home, env) => path.join(homeOf(home, env), ".cursor", "mcp.json");
export const openCodeConfigPath = async (io, home, env) => {
const root = path.resolve(homeOf(home, env), env.OPENCODE_CONFIG_DIR || path.join(env.XDG_CONFIG_HOME || ".config", "opencode"));
const jsonc = path.join(root, "opencode.jsonc");
try { await io.lstat(jsonc); return { file: jsonc, jsonc: true }; }
- catch (error) { if (error?.code !== "ENOENT") throw error; return { file: path.join(root, "opencode.json"), jsonc: false }; }
+ catch (e) { if (e.code !== "ENOENT") throw e; return { file: path.join(root, "opencode.json"), jsonc: false }; }
};
-export const cursorMcpPath = (home, env) => path.join(homeOf(home, env), ".cursor", "mcp.json");
-export const geminiMcpPath = (home, env) => path.join(homeOf(home, env), ".gemini", "settings.json");
-// --- canonical translation ---------------------------------------------------
-// The shared store is the source of truth; each function maps it to the
-// provider's own shape. Secrets are carried (the editor needs them); status
-// polling must use redacted summaries instead.
-export function toClaudeEntry(server) {
- if (server.type === "local") {
- const entry = { command: server.command[0], args: server.command.slice(1) };
- if (Object.keys(server.env).length) entry.env = { ...server.env };
- return entry;
- }
- return { type: "http", url: server.url, ...(Object.keys(server.headers).length ? { headers: { ...server.headers } } : {}) };
+export function toClaudeEntry(s) {
+ return s.type === "local" ? { command: s.command[0], args: s.command.slice(1), ...(Object.keys(s.env).length ? { env: { ...s.env } } : {}) }
+ : { type: "http", url: s.url, ...(Object.keys(s.headers).length ? { headers: { ...s.headers } } : {}) };
}
export function fromClaudeEntry(name, entry) {
- if (!entry || typeof entry !== "object") return null;
- if (typeof entry.url === "string" && /^https?:\/\//.test(entry.url)) {
- return { name, type: "remote", url: entry.url, headers: { ...(entry.headers ?? {}) }, enabled: true };
- }
- if (typeof entry.command === "string" && entry.command) {
- return { name, type: "local", command: [entry.command, ...(Array.isArray(entry.args) ? entry.args : [])], env: { ...(entry.env ?? {}) }, enabled: true };
- }
+ if (!object(entry)) return null;
+ if (entry.type && !["http", "stdio"].includes(entry.type)) return null;
+ if (typeof entry.url === "string" && /^https?:\/\//.test(entry.url)) return { name, type: "remote", url: entry.url, headers: { ...(entry.headers ?? {}) }, enabled: true };
+ if (typeof entry.command === "string" && entry.command) return { name, type: "local", command: [entry.command, ...(Array.isArray(entry.args) ? entry.args : [])], env: { ...(entry.env ?? {}) }, enabled: true };
return null;
}
-export function toCodexEntry(server) {
- if (server.type === "local") {
- const entry = { command: server.command[0], args: server.command.slice(1) };
- if (Object.keys(server.env).length) entry.env = { ...server.env };
- if (server.enabled === false) entry.enabled = false;
- return entry;
- }
- const entry = { url: server.url };
- if (Object.keys(server.headers).length) entry.http_headers = { ...server.headers };
- if (server.enabled === false) entry.enabled = false;
+export function toCodexEntry(s) {
+ const entry = s.type === "local" ? { command: s.command[0], args: s.command.slice(1), ...(Object.keys(s.env).length ? { env: { ...s.env } } : {}) }
+ : { url: s.url, ...(Object.keys(s.headers).length ? { http_headers: { ...s.headers } } : {}) };
+ if (s.enabled === false) entry.enabled = false;
return entry;
}
export function fromCodexEntry(name, entry) {
- if (!entry || typeof entry !== "object") return null;
- if (typeof entry.url === "string" && entry.url) {
- return { name, type: "remote", url: entry.url, headers: { ...(entry.http_headers ?? {}) }, enabled: entry.enabled !== false };
- }
- if (typeof entry.command === "string" && entry.command) {
- return { name, type: "local", command: [entry.command, ...(Array.isArray(entry.args) ? entry.args : [])], env: { ...(entry.env ?? {}) }, enabled: entry.enabled !== false };
- }
+ if (!object(entry)) return null;
+ if (typeof entry.url === "string" && entry.url) return { name, type: "remote", url: entry.url, headers: { ...(entry.http_headers ?? {}) }, enabled: entry.enabled !== false };
+ if (typeof entry.command === "string" && entry.command) return { name, type: "local", command: [entry.command, ...(Array.isArray(entry.args) ? entry.args : [])], env: { ...(entry.env ?? {}) }, enabled: entry.enabled !== false };
return null;
}
-export function toOpenCodeEntry(server) {
- if (server.type === "local") {
- return { type: "local", command: [...server.command], ...(Object.keys(server.env).length ? { environment: { ...server.env } } : {}), enabled: server.enabled !== false };
- }
- return { type: "remote", url: server.url, ...(Object.keys(server.headers).length ? { headers: { ...server.headers } } : {}), enabled: server.enabled !== false };
+export function toOpenCodeEntry(s, v2 = false) {
+ const state = v2 ? { disabled: s.enabled === false } : { enabled: s.enabled !== false };
+ return s.type === "local" ? { type: "local", command: [...s.command], ...(Object.keys(s.env).length ? { environment: { ...s.env } } : {}), ...state }
+ : { type: "remote", url: s.url, ...(Object.keys(s.headers).length ? { headers: { ...s.headers } } : {}), ...state };
}
export function fromOpenCodeEntry(name, entry) {
- if (!entry || typeof entry !== "object") return null;
- if (entry.type === "remote" && typeof entry.url === "string") {
- return { name, type: "remote", url: entry.url, headers: { ...(entry.headers ?? {}) }, enabled: entry.enabled !== false };
- }
- if ((entry.type === "local" || entry.type === undefined) && Array.isArray(entry.command)) {
- return { name, type: "local", command: [...entry.command], env: { ...(entry.environment ?? entry.env ?? {}) }, enabled: entry.enabled !== false };
- }
+ if (!object(entry)) return null;
+ const enabled = entry.enabled !== false && entry.disabled !== true;
+ if (entry.type === "remote" && typeof entry.url === "string") return { name, type: "remote", url: entry.url, headers: { ...(entry.headers ?? {}) }, enabled };
+ if (entry.type === "local" && Array.isArray(entry.command)) return { name, type: "local", command: [...entry.command], env: { ...(entry.environment ?? {}) }, enabled };
return null;
}
-export function toCursorEntry(server) {
- return toClaudeEntry(server);
-}
-export function fromCursorEntry(name, entry) {
- return fromClaudeEntry(name, entry);
-}
-export function toGeminiEntry(server) {
- if (server.type === "local") {
- const entry = { command: server.command[0], args: server.command.slice(1) };
- if (Object.keys(server.env).length) entry.env = { ...server.env };
- return entry;
- }
- return { httpUrl: server.url, ...(Object.keys(server.headers).length ? { headers: { ...server.headers } } : {}) };
+export const toCursorEntry = toClaudeEntry;
+export const fromCursorEntry = fromClaudeEntry;
+export const isSupportedMcpProvider = (provider) => ["claude", "codex", "opencode", "cursor"].includes(provider);
+
+function jsonDocument(raw, strict) {
+ const errors = [];
+ const tree = parseTree(raw || "{}", errors, { allowTrailingComma: !strict, disallowComments: strict });
+ const check = (node) => {
+ if (node.type === "object") {
+ const keys = node.children.map((p) => p.children[0].value);
+ if (new Set(keys).size !== keys.length) throw problem("unsupported", "Duplicate keys in the MCP configuration. Repair them before changing servers.");
+ }
+ for (const child of node.children ?? []) check(child);
+ };
+ if (errors.length || tree?.type !== "object") throw problem("unsupported", "The MCP configuration is not a valid JSON object. Repair it before changing servers.");
+ check(tree);
+ return parse(raw || "{}", [], { allowTrailingComma: !strict, disallowComments: strict });
}
-export function fromGeminiEntry(name, entry) {
- if (!entry || typeof entry !== "object") return null;
- const url = typeof entry.httpUrl === "string" ? entry.httpUrl : typeof entry.url === "string" ? entry.url : null;
- if (url && /^https?:\/\//.test(url)) return { name, type: "remote", url, headers: { ...(entry.headers ?? {}) }, enabled: true };
- if (typeof entry.command === "string" && entry.command) {
- return { name, type: "local", command: [entry.command, ...(Array.isArray(entry.args) ? entry.args : [])], env: { ...(entry.env ?? {}) }, enabled: true };
- }
- return null;
+
+function containerFor(provider, value) {
+ const key = provider === "opencode" ? "mcp" : "mcpServers";
+ const root = value[key];
+ if (root !== undefined && !object(root)) throw problem("unsupported", "The MCP configuration has an unsupported server section.");
+ // V1 may contain a real server named 'servers'. Its transport distinguishes
+ // that entry from V2's server dictionary. Never rewrite V2 back to V1.
+ const v2 = provider === "opencode" && ((Object.hasOwn(root ?? {}, "servers") && !["local", "remote"].includes(root.servers?.type))
+ || (Object.hasOwn(root ?? {}, "timeout") && !["local", "remote"].includes(root.timeout?.type)));
+ if (v2 && root.servers !== undefined && !object(root.servers)) throw problem("unsupported", "The OpenCode V2 server dictionary must be an object.");
+ if (v2 && Object.keys(root).some((k) => !["servers", "timeout", "codemode", "protocol"].includes(k))) {
+ throw problem("unsupported", "OpenCode mixes V1 and V2 MCP entries. Repair its MCP section before changing servers.");
+ }
+ return { v2, keys: v2 ? [key, "servers"] : [key], entries: v2 ? root.servers ?? {} : root ?? {} };
}
-export const ADAPTERS = Object.freeze({
- claude: { file: claudeMcpPath, toEntry: toClaudeEntry, fromEntry: fromClaudeEntry, containerKey: "mcpServers" },
- cursor: { file: cursorMcpPath, toEntry: toCursorEntry, fromEntry: fromCursorEntry, containerKey: "mcpServers" },
- antigravity: { file: geminiMcpPath, toEntry: toGeminiEntry, fromEntry: fromGeminiEntry, containerKey: "mcpServers" },
-});
-export const isSupportedMcpProvider = (provider) => ["claude", "codex", "opencode", "cursor", "antigravity"].includes(provider);
+async function fileFor(io, provider, home, env) {
+ if (provider === "opencode") return (await openCodeConfigPath(io, home, env)).file;
+ if (provider === "codex") return codexMcpPath(home, env);
+ if (provider === "claude") return claudeMcpPath(home, env);
+ if (provider === "cursor") return cursorMcpPath(home, env);
+ throw problem("unsupported", "MCP management is not supported for this provider.");
+}
-// --- JSON container helpers (claude/cursor/gemini) ---------------------------
-const listJsonServers = (value, containerKey) => {
- const container = value?.[containerKey];
- if (container === undefined) return { servers: new Map(), present: false };
- if (!container || typeof container !== "object" || Array.isArray(container)) throw problem("unsupported", "The MCP configuration has an unsupported server section.");
- return { servers: new Map(Object.entries(container)), present: true };
-};
+// Unknown provider options must be shown as differing, rather than implying
+// that cwd/auth/tool-policy differences are represented by the shared entry.
+export function hasExtraMcpOptions(provider, entry) {
+ const keys = provider === "codex" ? ["command", "args", "env", "url", "http_headers", "enabled"]
+ : provider === "opencode" ? ["type", "command", "environment", "url", "headers", "enabled", "disabled"]
+ : ["type", "command", "args", "env", "url", "headers"];
+ return !object(entry) || Object.keys(entry).some((k) => !keys.includes(k));
+}
-/** Read one provider's own servers without secrets leaving redacted summaries. */
export async function readProviderMcp(io, provider, { home, env = {} }) {
- const resolvedHome = homeOf(home, env);
- if (provider === "codex") {
- const file = codexMcpPath(resolvedHome, env);
- let raw;
- try { raw = await readBounded(io, file); }
- catch (error) { if (error?.code === "ENOENT") return { provider, path: file, present: false, servers: [] }; throw error; }
- let parsed;
- try { parsed = parseMcpToml(raw); }
- catch (error) { throw problem(error?.code || "unsupported", "Codex config.toml MCP sections are unsupported: " + (error?.message || "parse error")); }
- return { provider, path: file, present: true, raw, servers: [...parsed.entries()].map(([name, entry]) => ({ name, entry, canonical: fromCodexEntry(name, entry) })) };
- }
- if (provider === "opencode" || provider === "antigravity") {
- // OpenCode uses JSONC; Gemini settings tolerate comments too. Parse with
- // the same vendored parser as skill policies so comments survive reads.
- const file = provider === "opencode" ? (await openCodeConfigPath(io, resolvedHome, env)).file : ADAPTERS.antigravity.file(resolvedHome, env);
- let raw;
- try { raw = await readBounded(io, file); }
- catch (error) { if (error?.code === "ENOENT") return { provider, path: file, present: false, servers: [] }; throw error; }
- let value;
- try {
- value = parse(raw, [], { allowTrailingComma: true, disallowComments: false });
- } catch { throw problem("unsupported", "The MCP configuration contains invalid JSON/JSONC. Repair it before changing MCP servers."); }
- if (!value || typeof value !== "object" || Array.isArray(value)) throw problem("unsupported", "The MCP configuration must be a JSON object.");
- if (provider === "opencode") {
- const container = value?.mcp;
- if (container !== undefined && (!container || typeof container !== "object" || Array.isArray(container))) {
- throw problem("unsupported", "The OpenCode MCP section has an unsupported shape.");
- }
- return { provider, path: file, present: true, raw, value,
- servers: Object.entries(container ?? {}).map(([name, entry]) => ({ name, entry, canonical: fromOpenCodeEntry(name, entry) })) };
- }
- const { servers } = listJsonServers(value, "mcpServers");
- return { provider, path: file, present: true, raw, value,
- servers: [...servers.entries()].map(([name, entry]) => ({ name, entry, canonical: fromGeminiEntry(name, entry) })) };
- }
- const adapter = ADAPTERS[provider];
- if (!adapter) throw problem("invalid", "Unknown MCP provider.");
- const file = adapter.file(resolvedHome, env);
- const read = await readJsonFile(io, file).catch((error) => {
- if (error?.code === "ENOENT") return { present: false, raw: null, value: {} };
- throw error;
- });
- const { servers } = listJsonServers(read.value, adapter.containerKey);
- return { provider, path: file, present: read.present, raw: read.raw, value: read.value,
- servers: [...servers.entries()].map(([name, entry]) => ({ name, entry, canonical: adapter.fromEntry(name, entry) })) };
+ const file = await fileFor(io, provider, home, env);
+ const snapshot = await readConfig(io, file);
+ let entries;
+ if (provider === "codex") entries = [...parseMcpToml(snapshot.content)];
+ else entries = Object.entries(containerFor(provider, jsonDocument(snapshot.content, provider !== "opencode")).entries);
+ const from = provider === "codex" ? fromCodexEntry : provider === "opencode" ? fromOpenCodeEntry : fromClaudeEntry;
+ return { provider, path: file, present: snapshot.exists, revision: snapshot.revision,
+ servers: entries.map(([name, entry]) => {
+ const canonical = from(name, entry);
+ const malformed = !object(entry)
+ || ['enabled', 'disabled'].some(k => entry[k] !== undefined && typeof entry[k] !== 'boolean')
+ || (entry.args !== undefined && (!Array.isArray(entry.args) || !entry.args.every(a => typeof a === 'string')));
+ return { name, entry, canonical: !malformed && canonical && validateMcpServer(canonical).ok ? canonical : null, extra: hasExtraMcpOptions(provider, entry) };
+ }) };
}
-/** Merge one translated entry into the provider file, preserving the rest. */
-export async function writeProviderMcpEntry(io, provider, { home, env = {} }, name, entryOrNull) {
- const resolvedHome = homeOf(home, env);
+export async function writeProviderMcpEntry(io, provider, { home, env = {} }, name, entry, { revision } = {}) {
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) throw problem("invalid", "Invalid MCP server name.");
- if (provider === "codex") {
- const file = codexMcpPath(resolvedHome, env);
- await io.mkdir(path.dirname(file), { recursive: true });
- let raw = "";
- try { raw = await readBounded(io, file); }
- catch (error) { if (error?.code !== "ENOENT") throw error; }
- const next = applyMcpToml(raw, name, entryOrNull);
- await io.writeFile(file + `.tmp.${process.pid}`, next, { encoding: "utf8", mode: 0o600 }).catch(async () => {
- await io.mkdir(path.dirname(file), { recursive: true });
- await io.writeFile(file + `.tmp.${process.pid}`, next, { encoding: "utf8", mode: 0o600 });
- });
- await io.chmod(file + `.tmp.${process.pid}`, 0o600);
- await io.rename(file + `.tmp.${process.pid}`, file);
- return { ok: true, path: file };
- }
- if (provider === "opencode") {
- const { file } = await openCodeConfigPath(io, resolvedHome, env);
- await io.mkdir(path.dirname(file), { recursive: true });
- let raw = "";
- try { raw = await readBounded(io, file); }
- catch (error) { if (error?.code !== "ENOENT") throw error; }
- if (!raw.trim()) raw = '{}\n';
- const value = jsoncKeys({ raw }, false);
- if (!value || typeof value !== "object" || Array.isArray(value)) throw problem("unsupported", "The OpenCode MCP configuration must be a JSON object.");
- if (value.mcp !== undefined && (!value.mcp || typeof value.mcp !== "object" || Array.isArray(value.mcp))) {
- throw problem("unsupported", "The OpenCode MCP section has an unsupported shape.");
+ const file = await fileFor(io, provider, home, env);
+ return changeConfig(io, file, revision, (raw) => {
+ if (provider === "codex") return applyMcpToml(raw, name, entry);
+ const value = jsonDocument(raw, provider !== "opencode");
+ const container = containerFor(provider, value);
+ let nextEntry = entry;
+ if (entry && container.v2) {
+ const { enabled, ...rest } = entry;
+ nextEntry = { ...rest, disabled: enabled === false };
}
- const formatting = { insertSpaces: true, tabSize: 2, eol: raw.includes("\r\n") ? "\r\n" : "\n" };
- let content = raw;
- if (entryOrNull) content = applyEdits(content, modify(content, ["mcp", name], entryOrNull, { formattingOptions: formatting }));
- else if (value.mcp && Object.hasOwn(value.mcp, name)) content = applyEdits(content, modify(content, ["mcp", name], undefined, { formattingOptions: formatting }));
- if (Buffer.byteLength(content, "utf8") > MAX_FILE_BYTES) throw problem("too_large", "The MCP configuration would exceed the 1 MiB limit.");
- const tmp = file + `.tmp.${process.pid}`;
- await io.writeFile(tmp, content, { encoding: "utf8", mode: 0o600 });
- await io.chmod(tmp, 0o600);
- await io.rename(tmp, file);
- return { ok: true, path: file };
- }
- const adapter = ADAPTERS[provider];
- if (!adapter) throw problem("invalid", "Unknown MCP provider.");
- const file = adapter.file(resolvedHome, env);
- await io.mkdir(path.dirname(file), { recursive: true });
- let raw = "";
- try { raw = await readBounded(io, file); }
- catch (error) { if (error?.code !== "ENOENT") throw error; }
- if (!raw.trim()) raw = '{}\n';
- if (provider === "antigravity") {
- // settings.json tolerates comments; preserve them like opencode does.
- const value = jsoncKeys({ raw }, false);
- if (!value || typeof value !== "object" || Array.isArray(value)) throw problem("unsupported", "The MCP configuration must be a JSON object.");
- if (value.mcpServers !== undefined && (!value.mcpServers || typeof value.mcpServers !== "object" || Array.isArray(value.mcpServers))) {
- throw problem("unsupported", "The MCP configuration has an unsupported server section.");
- }
- const formatting = { insertSpaces: true, tabSize: 2, eol: raw.includes("\r\n") ? "\r\n" : "\n" };
- let content = raw;
- if (entryOrNull) content = applyEdits(content, modify(content, ["mcpServers", name], entryOrNull, { formattingOptions: formatting }));
- else if (value.mcpServers && Object.hasOwn(value.mcpServers, name)) content = applyEdits(content, modify(content, ["mcpServers", name], undefined, { formattingOptions: formatting }));
- if (Buffer.byteLength(content, "utf8") > MAX_FILE_BYTES) throw problem("too_large", "The MCP configuration would exceed the 1 MiB limit.");
- const tmp = file + `.tmp.${process.pid}`;
- await io.writeFile(tmp, content, { encoding: "utf8", mode: 0o600 });
- await io.chmod(tmp, 0o600);
- await io.rename(tmp, file);
- return { ok: true, path: file };
- }
- let value;
- try { value = JSON.parse(raw); }
- catch { throw problem("unsupported", "The MCP configuration is not valid JSON. Repair it before changing MCP servers."); }
- if (!value || typeof value !== "object" || Array.isArray(value)) throw problem("unsupported", "The MCP configuration must be a JSON object.");
- if (value[adapter.containerKey] !== undefined && (!value[adapter.containerKey] || typeof value[adapter.containerKey] !== "object" || Array.isArray(value[adapter.containerKey]))) {
- throw problem("unsupported", "The MCP configuration has an unsupported server section.");
- }
- const next = { ...value, [adapter.containerKey]: { ...(value[adapter.containerKey] ?? {}) } };
- if (entryOrNull) next[adapter.containerKey][name] = entryOrNull;
- else delete next[adapter.containerKey][name];
- const content = JSON.stringify(next, null, 2) + "\n";
- if (Buffer.byteLength(content, "utf8") > MAX_FILE_BYTES) throw problem("too_large", "The MCP configuration would exceed the 1 MiB limit.");
- const tmp = file + `.tmp.${process.pid}`;
- await io.writeFile(tmp, content, { encoding: "utf8", mode: 0o600 });
- await io.chmod(tmp, 0o600);
- await io.rename(tmp, file);
- return { ok: true, path: file };
+ if (!entry && !Object.hasOwn(container.entries, name)) return raw;
+ return applyEdits(raw || "{}\n", modify(raw || "{}\n", [...container.keys, name], nextEntry ?? undefined,
+ { formattingOptions: { insertSpaces: true, tabSize: 2, eol: raw.includes("\r\n") ? "\r\n" : "\n" } }));
+ });
}
diff --git a/docker/setup/mcp/files.mjs b/docker/setup/mcp/files.mjs
new file mode 100644
index 0000000..5a1b625
--- /dev/null
+++ b/docker/setup/mcp/files.mjs
@@ -0,0 +1,48 @@
+// Share the native-settings queue with skill policy writers. Snapshots cover
+// content, resolved target, permissions and file identity, with bounded reads.
+import path from "node:path";
+import { randomBytes } from "node:crypto";
+import { configSnapshot, serializeState } from "../skills/state.mjs";
+
+const conflict = () => Object.assign(new Error("Provider settings changed. Refresh MCP servers before applying this change."), { code: "conflict" });
+export async function readConfig(io, file) {
+ const value = await configSnapshot(file, io, true);
+ if (!value.exists) {
+ let ancestor = path.dirname(file), suffix = path.basename(file);
+ for (;;) {
+ try { value.resolvedPath = path.join(await io.realpath(ancestor), suffix); break; }
+ catch (e) { if (e.code !== "ENOENT") throw e; suffix = path.join(path.basename(ancestor), suffix); ancestor = path.dirname(ancestor); }
+ }
+ }
+ return { ...value, content: value.exists ? value.content : "" };
+}
+
+export async function changeConfig(io, file, revision, change) {
+ await io.mkdir(path.dirname(file), { recursive: true });
+ const initial = await readConfig(io, file);
+ return serializeState(initial.resolvedPath, async () => {
+ const before = await readConfig(io, file);
+ if (initial.resolvedPath !== before.resolvedPath || (revision !== undefined && revision !== before.revision)) throw conflict();
+ const content = change(before.content);
+ if (content === before.content) return { ok: true, path: file, revision: before.revision };
+ if (Buffer.byteLength(content, "utf8") > 1024 * 1024) throw Object.assign(new Error("The MCP configuration would exceed the 1 MiB limit."), { code: "too_large" });
+ const suffix = `${process.pid}.${randomBytes(8).toString("hex")}`;
+ const tmp = `${before.resolvedPath}.tmp.${suffix}`;
+ const backupPath = before.exists ? `${before.resolvedPath}.t3-mcp-backup.${suffix}` : null;
+ try {
+ const mode = before.mode & 0o600;
+ await io.writeFile(tmp, content, { encoding: "utf8", mode, flag: "wx" });
+ await io.chmod(tmp, mode);
+ if ((await readConfig(io, file)).revision !== before.revision) throw conflict();
+ if (backupPath) await io.writeFile(backupPath, before.content, { encoding: "utf8", mode: 0o600, flag: "wx" });
+ if (before.exists) {
+ if ((await readConfig(io, file)).revision !== before.revision) throw conflict();
+ await io.rename(tmp, before.resolvedPath);
+ } else {
+ try { await io.link(tmp, before.resolvedPath); }
+ catch (e) { if (e.code === "EEXIST") throw conflict(); throw e; }
+ }
+ return { ok: true, path: file, backupPath, revision: (await readConfig(io, file)).revision };
+ } finally { await io.rm(tmp, { force: true }); }
+ });
+}
diff --git a/docker/setup/mcp/store.mjs b/docker/setup/mcp/store.mjs
index 321e2f8..23db7d2 100644
--- a/docker/setup/mcp/store.mjs
+++ b/docker/setup/mcp/store.mjs
@@ -8,14 +8,13 @@ export const MCP_PROVIDERS = Object.freeze({
codex: "Codex",
opencode: "OpenCode",
cursor: "Cursor",
- antigravity: "Antigravity",
});
-export const SUPPORTED_MCP_PROVIDERS = Object.freeze(["claude", "codex", "opencode", "cursor", "antigravity"]);
-// Grok Build and Muse Code MCP layouts are unverified against the pinned
-// CLIs (see docs/context-management.md). They stay out until verified.
+export const SUPPORTED_MCP_PROVIDERS = Object.freeze(Object.keys(MCP_PROVIDERS));
+// Keep native formats without a verified integration explicitly unavailable.
export const UNSUPPORTED_MCP_PROVIDERS = Object.freeze({
- grok: "Grok Build MCP layout is unverified against the pinned CLI; not managed here yet.",
- muse: "Muse Code MCP layout is unverified against the pinned CLI; not managed here yet.",
+ antigravity: "Antigravity uses isolated T3 profiles; shared MCP settings are not integrated yet.",
+ grok: "Grok MCP management is not implemented. Grok may also load Claude and Cursor MCP settings through its native compatibility imports.",
+ muse: "Muse MCP management is not implemented yet.",
});
const MAX_STORE_BYTES = 256 * 1024;
@@ -38,8 +37,8 @@ export function validateMcpServer(input) {
const out = { name, type, enabled };
if (type === "local") {
const command = Array.isArray(value.command) ? value.command : null;
- if (!command || command.length < 1 || command.length > 32 || !command.every((part) => typeof part === "string" && part.length >= 1 && part.length <= 1024)) {
- return { ok: false, error: "A local server needs a command of 1–32 entries, each 1–1024 characters." };
+ if (!command || command.length < 1 || command.length > 32 || !command[0] || !command.every((part) => typeof part === "string" && part.length <= 1024)) {
+ return { ok: false, error: "A local server needs an executable and at most 31 arguments, each up to 1024 characters." };
}
if (command.some((part) => part.includes("\0"))) return { ok: false, error: "Command entries must not contain NUL bytes." };
out.command = [...command];
@@ -54,11 +53,16 @@ export function validateMcpServer(input) {
out.env = Object.fromEntries(keys.sort().map((key) => [key, String(env[key])]));
} else {
const url = String(value.url ?? "").trim();
- if (!/^https?:\/\/.{1,2048}$/.test(url)) return { ok: false, error: "A remote server needs an http(s) URL of at most 2048 characters." };
+ try {
+ const parsed = new URL(url);
+ if (!["http:", "https:"].includes(parsed.protocol) || !parsed.hostname || url.length > 2048 || /[\r\n\0]/.test(url)) throw new Error();
+ if (parsed.username || parsed.password) return { ok: false, error: "Put remote credentials in Headers, not in the URL." };
+ } catch { return { ok: false, error: "A remote server needs a valid http(s) URL of at most 2048 characters." }; }
out.url = url;
const headers = value.headers === undefined ? {} : value.headers;
if (!headers || typeof headers !== "object" || Array.isArray(headers)) return { ok: false, error: "Expected headers to be an object." };
const keys = Object.keys(headers);
+ if (new Set(keys.map((key) => key.toLowerCase())).size !== keys.length) return { ok: false, error: "Header names must be unique regardless of letter case." };
if (keys.length > 64) return { ok: false, error: "A server holds at most 64 header entries." };
for (const key of keys) {
if (!/^[A-Za-z0-9-]+$/.test(key) || key.length > 128) return { ok: false, error: `Invalid header name: ${key}` };
@@ -74,11 +78,18 @@ export const mcpRevision = (servers) => createHash("sha256").update(JSON.stringi
async function readStoreFile(fs, file) {
let stat;
- try { stat = await fs.stat(file); }
+ try { stat = await fs.lstat(file); }
catch (error) { if (error?.code === "ENOENT") return { present: false }; throw error; }
if (!stat.isFile()) throw problem("invalid", "The shared MCP store must be a regular file.");
if (stat.size > MAX_STORE_BYTES) throw problem("too_large", "The shared MCP store exceeds the 256 KiB limit. Reduce it directly on disk.");
- const raw = await fs.readFile(file, "utf8");
+ const handle = await fs.open(file, "r");
+ let raw;
+ try {
+ const buf = Buffer.alloc(MAX_STORE_BYTES + 1); let bytes = 0;
+ while (bytes < buf.length) { const r = await handle.read(buf, bytes, buf.length - bytes, bytes); if (!r.bytesRead) break; bytes += r.bytesRead; }
+ if (bytes > MAX_STORE_BYTES) throw problem("too_large", "The shared MCP store exceeds the 256 KiB limit.");
+ raw = buf.toString("utf8", 0, bytes);
+ } finally { await handle.close(); }
let parsed;
try { parsed = JSON.parse(raw); }
catch { throw problem("invalid", "The shared MCP store is not valid JSON. Repair it directly on disk."); }
@@ -97,6 +108,7 @@ async function readStoreFile(fs, file) {
}
async function writeFileAtomic(fs, file, text) {
+ if (Buffer.byteLength(text, "utf8") > MAX_STORE_BYTES) throw problem("too_large", "The shared MCP store would exceed the 256 KiB limit. Reduce the server definition.");
await fs.mkdir(path.dirname(file), { recursive: true });
const tmp = `${file}.tmp.${process.pid}.${randomBytes(6).toString("hex")}`;
try {
@@ -109,7 +121,7 @@ async function writeFileAtomic(fs, file, text) {
}
const storeQueues = new Map();
-async function withStore(file, work) {
+export async function withStore(file, work) {
const key = path.resolve(file);
const previous = storeQueues.get(key) ?? Promise.resolve();
const current = previous.catch(() => {}).then(work);
@@ -143,6 +155,7 @@ export async function createMcpServer(fs, baseDir, input) {
return { ok: false, code: "conflict", error: "A shared MCP server with that name already exists." };
}
const servers = [...read.servers, checked.server];
+ if (servers.length > MAX_SERVERS) throw problem("too_large", "The shared MCP store holds at most 128 servers.");
await writeFileAtomic(fs, file, JSON.stringify({ version: 1, servers }, null, 2) + "\n");
return { ok: true, path: file, created: !read.present && read.servers.length === 0, revision: mcpRevision(servers), server: checked.server };
});
@@ -165,13 +178,14 @@ export async function saveMcpServer(fs, baseDir, name, revision, input) {
});
}
-export async function removeMcpServer(fs, baseDir, name, revision) {
+export async function removeMcpServer(fs, baseDir, name, revision, beforeRemove = async () => {}) {
const file = mcpStorePathFor(baseDir);
return withStore(file, async () => {
const read = await readMcpStore(fs, baseDir);
if (read.error) return { ok: false, error: read.error };
if (read.revision !== revision) return changedStore();
if (!read.servers.some((server) => server.name === name)) return { ok: true, path: file, deleted: false, revision: read.revision };
+ await beforeRemove();
const servers = read.servers.filter((server) => server.name !== name);
if (servers.length === 0) await fs.rm(file, { force: true });
else await writeFileAtomic(fs, file, JSON.stringify({ version: 1, servers }, null, 2) + "\n");
@@ -181,6 +195,9 @@ export async function removeMcpServer(fs, baseDir, name, revision) {
export function redactMcpServer(server) {
const { env, headers, ...rest } = server;
+ // Paths and query strings may contain signed credentials. Only the origin
+ // belongs in polling/list summaries; the full URL loads with the editor.
+ if (rest.url) { try { rest.url = new URL(rest.url).origin; } catch { rest.url = "(invalid URL)"; } }
return { ...rest,
envKeys: Object.keys(env ?? {}),
envCount: Object.keys(env ?? {}).length,
diff --git a/docker/setup/mcp/toml.mjs b/docker/setup/mcp/toml.mjs
index 346c11e..510dc23 100644
--- a/docker/setup/mcp/toml.mjs
+++ b/docker/setup/mcp/toml.mjs
@@ -1,161 +1,77 @@
-// Minimal TOML handling for Codex `[mcp_servers.*]` sections only. The rest
-// of config.toml is opaque text and is preserved byte-for-byte.
-const problem = (code, message) => Object.assign(new Error(message), { code });
+// Parse and patch the complete TOML syntax tree. The vendor keeps untouched
+// comments/formatting; never scan table boundaries with regular expressions.
+import { parse, patch, stringify, TomlDocument } from "../vendor/toml-patch.mjs";
+import { isDeepStrictEqual } from "node:util";
-const sectionHead = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/;
-const stripComment = (value) => {
- let out = "", quote = null;
- for (let i = 0; i < value.length; i++) {
- const char = value[i];
- if (quote) {
- if (char === "\\") { out += value.slice(i, i + 2); i++; continue; }
- if (char === quote) quote = null;
- out += char;
- } else if (char === '"' || char === "'") { quote = char; out += char; }
- else if (char === "#") break;
- else out += char;
- }
- return out.trim();
-};
-const unquote = (raw) => {
- const text = stripComment(raw).trim();
- if (/^"""/.test(text) || /^'''/.test(text)) throw problem("unsupported", "Multiline TOML strings are not supported in MCP sections.");
- if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
- const inner = text.slice(1, -1);
- if (text.startsWith("'")) {
- if (inner.includes("'")) throw problem("unsupported", "Unsupported TOML string in MCP sections.");
- return inner;
- }
- return inner.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
- .replace(/\\(.)/g, (_, char) => ({ n: "\n", t: "\t", r: "\r", '"': '"', "\\": "\\" }[char] ?? char));
- }
- if (text === "true") return true;
- if (text === "false") return false;
- if (/^[+-]?\d+$/.test(text)) return Number(text);
- if (/^\[.*\]$/.test(text)) {
- const inner = text.slice(1, -1).trim();
- if (!inner) return [];
- const parts = [];
- let current = "", quote = null, depth = 0;
- for (let i = 0; i < inner.length; i++) {
- const char = inner[i];
- if (quote) { current += char; if (char === "\\") { current += inner[++i] ?? ""; continue; } if (char === quote) quote = null; continue; }
- if (char === '"' || char === "'") { quote = char; current += char; continue; }
- if (char === "[") depth++;
- if (char === "]") depth--;
- if (char === "," && depth === 0) { parts.push(current.trim()); current = ""; continue; }
- current += char;
- }
- if (current.trim() || parts.length) parts.push(current.trim());
- return parts.filter((part) => part !== "").map(unquote);
- }
- if (/^\{.*\}$/.test(text)) {
- const inner = text.slice(1, -1).trim();
- if (!inner) return {};
- const out = {};
- for (const pair of inner.split(",").map((part) => part.trim()).filter(Boolean)) {
- const match = /^([^=]+)=(.*)$/.exec(pair);
- if (!match) throw problem("unsupported", "Unsupported TOML table in MCP sections.");
- out[unquote(match[1].trim())] = unquote(match[2].trim());
- }
- return out;
- }
- const bare = /^[A-Za-z0-9_.-]+$/.test(text) ? text : null;
- if (bare !== null) return bare;
- throw problem("unsupported", "Unsupported TOML value in MCP sections.");
-};
-
-const tomlString = (value) => JSON.stringify(String(value));
-const tomlValue = (value) => {
- if (typeof value === "string") return tomlString(value);
- if (typeof value === "boolean") return value ? "true" : "false";
- if (typeof value === "number" && Number.isInteger(value)) return String(value);
- if (Array.isArray(value)) return "[ " + value.map(tomlValue).join(", ") + " ]";
- if (value && typeof value === "object") {
- const entries = Object.entries(value).map(([key, entry]) => `${tomlString(key)} = ${tomlValue(entry)}`);
- return "{ " + entries.join(", ") + " }";
- }
- throw problem("invalid", "Unsupported MCP value for TOML output.");
-};
+const problem = () => Object.assign(new Error("Codex config.toml cannot be safely edited. Check its TOML syntax before changing MCP servers."), { code: "unsupported" });
+const object = (v) => v && typeof v === "object" && !Array.isArray(v);
+const plain = (v) => Array.isArray(v) ? v.map(plain)
+ : v && Object.getPrototypeOf(v) === null ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, plain(x)])) : v;
+function document(text) {
+ try {
+ const value = parse(String(text ?? ""));
+ if (value.mcp_servers !== undefined && !object(value.mcp_servers)) throw problem();
+ return plain(value);
+ } catch { throw problem(); } // Parser errors can contain credential values.
+}
-/** Parse only `[mcp_servers.]` tables; everything else stays opaque. */
export function parseMcpToml(text) {
- const lines = String(text ?? "").split("\n");
- const servers = new Map();
- let current = null;
- let currentName = null;
- for (const line of lines) {
- const head = sectionHead.exec(line);
- if (head) {
- const table = head[1].trim();
- const match = /^mcp_servers\.(.+)$/.exec(table);
- if (match) {
- currentName = match[1].trim().replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
- if (!currentName || servers.has(currentName)) throw problem("unsupported", `Duplicate Codex MCP server: ${currentName || "(unnamed)"}.`);
- current = {};
- servers.set(currentName, current);
- } else {
- current = null;
- currentName = null;
- }
- continue;
- }
- if (!current) continue;
- const trimmed = line.trim();
- if (!trimmed || trimmed.startsWith("#")) continue;
- const match = /^([^=]+)=(.*)$/.exec(line);
- if (!match) throw problem("unsupported", "Unsupported TOML line in Codex MCP sections.");
- current[unquote(match[1].trim())] = unquote(match[2]);
- }
- return servers;
+ return new Map(Object.entries(document(text).mcp_servers ?? {}));
}
-/** Replace/add/remove one `[mcp_servers.]` section, preserving the rest. */
export function applyMcpToml(text, name, entry) {
- const lines = String(text ?? "").split("\n");
- const out = [];
- let skipping = false;
- let replaced = false;
- const pushEntry = () => {
- if (!entry) return;
- out.push(`[mcp_servers.${name}]`);
- if (entry.command !== undefined) out.push(`command = ${tomlValue(entry.command)}`);
- if (entry.args !== undefined) out.push(`args = ${tomlValue(entry.args)}`);
- if (entry.env !== undefined) out.push(`env = ${tomlValue(entry.env)}`);
- if (entry.url !== undefined) out.push(`url = ${tomlValue(entry.url)}`);
- if (entry.http_headers !== undefined) out.push(`http_headers = ${tomlValue(entry.http_headers)}`);
- if (entry.enabled !== undefined) out.push(`enabled = ${tomlValue(entry.enabled)}`);
- if (entry.startup_timeout_ms !== undefined) out.push(`startup_timeout_ms = ${tomlValue(entry.startup_timeout_ms)}`);
- };
- for (const line of lines) {
- const head = sectionHead.exec(line);
- if (head) {
- if (skipping) { skipping = false; }
- const table = head[1].trim();
- const match = /^mcp_servers\.(.+)$/.exec(table);
- const target = match && match[1].trim().replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1") === name;
- if (target) {
- skipping = true;
- replaced = true;
- continue;
+ const value = document(text);
+ if (entry === null && !Object.hasOwn(value.mcp_servers ?? {}, name)) return text;
+ const servers = { ...(value.mcp_servers ?? {}) };
+ if (entry === null) delete servers[name];
+ else servers[name] = entry;
+ const desired = { ...value, mcp_servers: servers };
+ if (!Object.keys(servers).length) delete desired.mcp_servers;
+ try {
+ const tree = new TomlDocument(text).cst;
+ const starts = [0];
+ for (let at = text.indexOf("\n"); at !== -1; at = text.indexOf("\n", at + 1)) starts.push(at + 1);
+ const lineSpan = (node) => {
+ const start = starts[node.loc.start.line - 1];
+ const end = starts[node.loc.end.line] ?? text.length;
+ return [start, end];
+ };
+ const owns = (keys) => keys[0] === "mcp_servers" && keys[1] === name;
+ const edits = [];
+ let inlineRoot = false;
+ for (const node of tree) {
+ if (node.type === "KeyValue") {
+ if (owns(node.key.value)) edits.push([...lineSpan(node), ""]);
+ else if (node.key.value.length === 1 && node.key.value[0] === "mcp_servers") {
+ // Patch an inline root locally, never the surrounding document.
+ const [start, end] = lineSpan(node);
+ const updated = desired.mcp_servers ? { mcp_servers: desired.mcp_servers } : {};
+ edits.push([start, end, patch(text.slice(start, end), updated)]);
+ inlineRoot = true;
+ }
+ } else if (node.type === "Table" || node.type === "TableArray") {
+ const keys = node.key.item.value;
+ if (owns(keys)) {
+ // The parser attaches comments before the next table to this one.
+ // End at its last value, preserving those unrelated trailing notes.
+ const rows = node.items.filter((item) => item.type === "KeyValue");
+ edits.push([lineSpan(node.key)[0], lineSpan(rows.at(-1) ?? node.key)[1], ""]);
+ } else if (keys.length === 1 && keys[0] === "mcp_servers") {
+ for (const row of node.items) if (row.type === "KeyValue" && row.key.value[0] === name) edits.push([...lineSpan(row), ""]);
+ }
}
- out.push(line);
- continue;
}
- if (skipping) continue;
- out.push(line);
- }
- if (!replaced && entry) {
- if (out.length && out[out.length - 1].trim() !== "") out.push("");
- pushEntry();
- } else if (replaced && entry) {
- // Re-insert at end to keep output deterministic; the marker order does
- // not matter to Codex. Existing unrelated sections keep their bytes.
- while (out.length && out[out.length - 1].trim() === "") out.pop();
- out.push("");
- pushEntry();
- }
- let result = out.join("\n");
- if (!result.endsWith("\n")) result += "\n";
- return result;
+ let next = text;
+ for (const [start, end, replacement] of edits.sort((a, b) => b[0] - a[0])) next = next.slice(0, start) + replacement + next.slice(end);
+ if (entry !== null && !inlineRoot) {
+ const eol = text.includes("\r\n") ? "\r\n" : "\n";
+ // Remove only the known, generated parent marker, not user source.
+ const generated = stringify({ mcp_servers: { [name]: entry } }, { inlineTableStart: 3, newLine: eol }).replace(/^\[mcp_servers\]\r?\n(?:\r?\n)?/, "");
+ next += (next.endsWith("\n") || !next ? "" : eol) + eol + generated;
+ }
+ const actual = document(next);
+ if (actual.mcp_servers && !Object.keys(actual.mcp_servers).length) delete actual.mcp_servers;
+ if (!isDeepStrictEqual(actual, desired)) throw problem();
+ return next;
+ } catch { throw problem(); }
}
diff --git a/docker/setup/server.mjs b/docker/setup/server.mjs
index b5315a9..e9a741c 100644
--- a/docker/setup/server.mjs
+++ b/docker/setup/server.mjs
@@ -2704,7 +2704,8 @@ const server = createServer(async (req, res) => {
try {
const input = JSON.parse((await readBody(req, 6 * 64 * 1024 + 4096)) || "{}");
if (!input || typeof input !== "object" || Array.isArray(input)) return sendJson(res, 400, { ok: false, error: "Expected an MCP operation object." });
- const result = route === "/context/mcp/link" ? await mcpService.setLink(input.provider, input.name, input.linked, input.sharedRevision)
+ if (route === "/context/mcp/link" && typeof input.providerRevision !== "string") return sendJson(res, 400, { ok: false, code: "invalid", error: "Refresh MCP servers before changing provider settings." });
+ const result = route === "/context/mcp/link" ? await mcpService.setLink(input.provider, input.name, input.linked, input.sharedRevision, input.providerRevision)
: route === "/context/mcp/create" ? await mcpService.create(input.server)
: route === "/context/mcp/save" ? await mcpService.save(input.name, input.revision, input.server)
: await mcpService.remove(input.name, input.revision);
diff --git a/docker/setup/vendor/README.md b/docker/setup/vendor/README.md
index 966b78e..8fb394e 100644
--- a/docker/setup/vendor/README.md
+++ b/docker/setup/vendor/README.md
@@ -41,3 +41,28 @@ npm install --prefix /tmp/jsonc-build --no-audit --no-fund jsonc-parser@3.3.1 es
Bundle SHA-256: `bd0f3ce191c835fbd222afb0e0e416852d0ea1c917406bdd0a609a0b8655f5ef`.
Inputs are bounded to 1 MiB; invalid syntax, duplicate keys and unsupported
policy shapes disable controls rather than normalizing away existing settings.
+
+## TOML syntax tree
+
+`toml-patch.mjs` is the unmodified ESM distribution of
+`@decimalturn/toml-patch@3.1.2` (MIT; see `toml-patch.LICENSE`).
+Upstream: https://github.com/DecimalTurn/toml-patch
+
+```sh
+npm pack @decimalturn/toml-patch@3.1.2
+tar -xzf decimalturn-toml-patch-3.1.2.tgz
+cp package/dist/toml-patch.js toml-patch.mjs
+cp package/LICENSE toml-patch.LICENSE
+```
+
+Archive SHA-256: `aed417934ced4cd4d19da54eb286c16750ba9058ca2e47be4162e3c0e5449514`.
+Distribution SHA-256: `8872b6d74fd090a2e358b727ce68d055af802c9efa86d05a8a1d05cdba6ce869`.
+
+MCP editing validates the complete document and uses this pinned parser's CST
+locations to change only the selected server. Unrelated source, including
+comments attached to the end of a preceding table, stays in place. Inline-root
+edits are patched only inside that root assignment. The result is parsed again
+and checked against the intended data before publication. Nested tables,
+arrays of tables and multiline strings are covered by regression tests. Update
+the distribution, license, checksums and CST compatibility checks together.
+There is no runtime package installation and the bundle never reaches browsers.
diff --git a/docker/setup/vendor/toml-patch.LICENSE b/docker/setup/vendor/toml-patch.LICENSE
new file mode 100644
index 0000000..a8c55ea
--- /dev/null
+++ b/docker/setup/vendor/toml-patch.LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2019–2025 Tim Hall
+Copyright (c) 2025 Martin Leduc
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/docker/setup/vendor/toml-patch.mjs b/docker/setup/vendor/toml-patch.mjs
new file mode 100644
index 0000000..4cbaab6
--- /dev/null
+++ b/docker/setup/vendor/toml-patch.mjs
@@ -0,0 +1,56 @@
+//! @decimalturn/toml-patch v3.1.2 - https://github.com/DecimalTurn/toml-patch - @license: MIT
+function e(e){return e.type===`Document`}function t(e){return e.type===`Table`}function n(e){return e.type===`TableKey`}function r(e){return e.type===`TableArray`}function i(e){return e.type===`TableArrayKey`}function a(e){return e.type===`KeyValue`}function o(e){return e.type===`String`}function s(e){return e.type===`Float`}function c(e){return e.type===`DateTime`}function l(e){return e.type===`InlineArray`}function u(e){return e.type===`InlineItem`}function d(e){return e.type===`InlineTable`}function f(e){return e.type===`Comment`}function p(n){return e(n)||t(n)||r(n)||d(n)||l(n)}function m(e){return n(e)||i(e)||u(e)}function h(e){return a(e)||t(e)||r(e)||f(e)}let g,_;function v(e){if(g===e&&_!==void 0)return _;let t=C(e);return g=e,_=t,t}function y(){g=void 0,_=void 0}function b(e){return{lines:e.end.line-e.start.line+1,columns:e.end.column-e.start.column}}function x(e,t){let n=Array.isArray(e)?e:C(e);if(n.length===0)return{line:1,column:t};let r=0,i=n.length-1;for(;r>>1;n[e]({start:x(a,e),end:x(a,t)});for(;r=n||e.charCodeAt(r+1)!==10))throw new D(e,x(a,r),`Standalone CR; must be CRLF or LF`);if(t!==32&&t!==9&&t!==13){if(t===10)s(r);else if(t===91||t===93)yield{type:`Bracket`,raw:e[r],loc:o(r,r+1)};else if(t===123||t===125)yield{type:`Curly`,raw:e[r],loc:o(r,r+1)};else if(t===61)yield{type:`Equal`,raw:`=`,loc:o(r,r+1)};else if(t===44)yield{type:`Comma`,raw:`,`,loc:o(r,r+1)};else if(t===46)yield{type:`Dot`,raw:`.`,loc:o(r,r+1)};else if(t===35)yield c();else{let t=ne(e,r,`'`)||ne(e,r,`"`);t?yield l(t):yield u()}}r++}function s(t){a.push(t);let n=t>0&&e.charCodeAt(t-1)===13?`\r
+`:`
+`;i.reference===null?i.reference=n:i.reference!==n&&(i.mixed=!0)}function c(){let t=r;for(;r+1=6)throw new D(e,x(a,r),`Invalid multiline string: ${n} consecutive ${t} characters`);if(n===3){r+=2;break}r+=n-3,r+=2;break}let i=e.charCodeAt(r);if(i===13&&(r+1>=n||e.charCodeAt(r+1)!==10))throw new D(e,x(a,r),`Standalone CR in multiline string; must be CRLF or LF`);if((i<=31||i===127)&&i!==9&&i!==10&&i!==13){let n=t===`"`?`multiline basic strings`:`multiline literal strings`,o=`0x${i.toString(16).toUpperCase().padStart(2,`0`)}`,s=``;i===0?s=`Null`:i===127&&(s=`DEL`);let c=s?`${s} (${o}) not allowed in ${n}`:`Control char ${o} not allowed in ${n}`;throw new D(e,x(a,r),c)}i===10&&s(r),r++}if(r>=n){if(t===`"`){let t=0,i=n-1;for(;i>=0&&e[i]===`\\`;)t++,i--;if(t>0&&t%2!=0)throw new D(e,x(a,r),`Unterminated multiline ${c} (possible escape issue)`)}throw new D(e,x(a,r),`Unterminated multiline ${c}`)}return{type:`Literal`,raw:e.slice(i,r+1),loc:o(i,r+1)}}function u(){let t=e[r];if(!ee.test(t))throw new D(e,x(a,r),`Unexpected char "${t}"`);let i=r,s=t===`"`,c=t===`'`;for(;r=n);){let t=e.charCodeAt(r+1);if(!(s||c)&&(t===32||t===9||t===10||t===13||t===44||t===46||t===93||t===125||t===61||t===35))break;if(r++,!(s||c)){let t=e.charCodeAt(r);if(t<=31||t===127)throw new D(e,x(a,r),`Control char 0x${t.toString(16).toUpperCase().padStart(2,`0`)} not allowed in unquoted value`)}if(s||c){let t=e.charCodeAt(r);if((t<=31||t===127)&&t!==9){let n=s?`basic strings`:`literal strings`,i=`0x${t.toString(16).toUpperCase().padStart(2,`0`)}`,o=``;t===10?o=`Newline`:t===13?o=`Carriage return`:t===0?o=`Null`:t===127&&(o=`DEL`);let c=o?`${o} (${i}) not allowed in ${n}`:`Control char ${i} not allowed in ${n}`;throw new D(e,x(a,r),c)}}let i=e[r];if(i===`"`&&!c&&(s=!s),i===`'`&&!s&&(c=!c),r+1>=n)break;if(s&&i===`\\`){let t=e[r+1];(t===`"`||t===`\\`)&&r++}}if(s||c)throw new D(e,x(a,i),`Expected close of string with ${s?`"`:`'`}`);return{type:`Literal`,raw:e.slice(i,r+1),loc:o(i,r+1)}}}function ne(e,t,n){if(!n||e[t]!==n||e[t+1]!==n||e[t+2]!==n)return!1;if(n===`'`)return n;let r=0,i=t-1;for(;i>=0&&e[i]===`\\`;)r++,i--;return(r===0||r%2==0)&&n}const j=new Int8Array(128);j.fill(-1);for(let e=0;e<10;e++)j[48+e]=e;for(let e=0;e<6;e++)j[65+e]=10+e,j[97+e]=10+e;function M(e){let t=e.charCodeAt(0);return t<128&&j[t]!==-1}function re(e,t){return j[e.charCodeAt(t)]<<12|j[e.charCodeAt(t+1)]<<8|j[e.charCodeAt(t+2)]<<4|j[e.charCodeAt(t+3)]}function ie(e){return e.startsWith(`'''`)?oe(e.slice(3,e.length-3)):e.startsWith(`'`)?e.slice(1,e.length-1):e.startsWith(`"""`)?ae(oe(e.slice(3,e.length-3)),!0):e.startsWith(`"`)?ae(e.slice(1,e.length-1),!1):e}function ae(e,t){let n=e.length;if(!t&&e.indexOf(`\\`)===-1)return e;let r=[],i=0;for(let a=0;ai&&r.push(e.slice(i,a)),a++,a>=n)throw Error(`Trailing backslash`);let o=e[a];switch(o){case`b`:r.push(`\b`);break;case`t`:r.push(` `);break;case`n`:r.push(`
+`);break;case`f`:r.push(`\f`);break;case`r`:r.push(`\r`);break;case`"`:r.push(`"`);break;case`\\`:r.push(`\\`);break;case`e`:r.push(`\x1B`);break;case`u`:{if(a+4>n||!M(e[a+1])||!M(e[a+2])||!M(e[a+3])||!M(e[a+4]))throw Error(`Invalid Unicode escape: \\u${e.slice(a+1,a+5)}`);let t=re(e,a+1);if(t>=55296&&t<=57343)throw Error(`Invalid \\u${e.slice(a+1,a+5)}: surrogates not allowed`);r.push(String.fromCharCode(t)),a+=4;break}case`U`:{if(a+8>n)throw Error(`Invalid Unicode escape: \\U${e.slice(a+1,a+9)}`);for(let t=1;t<=8;t++)if(!M(e[a+t]))throw Error(`Invalid Unicode escape: \\U${e.slice(a+1,a+9)}`);let t=parseInt(e.slice(a+1,a+9),16);if(t>=55296&&t<=57343)throw Error(`Invalid \\U${e.slice(a+1,a+9)}: surrogates not allowed`);r.push(String.fromCodePoint(t)),a+=8;break}case`x`:{if(a+2>n||!M(e[a+1])||!M(e[a+2]))throw Error(`Invalid hex escape: \\x${e.slice(a+1,a+3)}`);let t=j[e.charCodeAt(a+1)]<<4|j[e.charCodeAt(a+2)];r.push(String.fromCharCode(t)),a+=2;break}default:if(t&&(o===`
+`||o===`\r`||o===` `||o===` `)){let t=o===`
+`||o===`\r`,r=a;for(;r0?`.`+String(e).padStart(3,`0`).replace(/0+$/,``):``}function me(e){if(e===`Z`)return 0;let t=e[0]===`+`?1:-1,[n,r]=e.slice(1).split(`:`);return t*(parseInt(n)*60+parseInt(r))}var he=class e{static IS_DATE_ONLY=/^\d{4}-\d{2}-\d{2}$/;static IS_TIME_ONLY=/^\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?$/;static IS_LOCAL_DATETIME_T=/^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?$/;static IS_LOCAL_DATETIME_SPACE=/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(?::\d{2})?(?:\.\d+)?$/;static IS_OFFSET_DATETIME_T=/^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;static IS_OFFSET_DATETIME_SPACE=/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;static IS_FULL_DATE=/(\d{4})-(\d+)-(\d+)/;static IS_FULL_TIME=/(\d+):(\d+)(?::(\d+))?/;static createDateWithOriginalFormat(t,n){if(e.IS_DATE_ONLY.test(n)){if(t.getUTCHours()!==0||t.getUTCMinutes()!==0||t.getUTCSeconds()!==0||t.getUTCMilliseconds()!==0){if(t instanceof ye)return t;let e=t.toISOString().replace(`Z`,``);return e=e.replace(/\.000$/,``),new ve(e,!1)}let e=t.toISOString().split(`T`)[0];return new ge(e)}if(e.IS_TIME_ONLY.test(n)){if(t instanceof _e)return t;{let e=N(t),r=pe(e.ms,n);return new _e(`${e.hours}:${e.minutes}:${e.seconds}${r}`,n)}}if(e.IS_LOCAL_DATETIME_T.test(n)){let e=N(t),r=pe(e.ms,n);return new ve(`${e.year}-${e.month}-${e.day}T${e.hours}:${e.minutes}:${e.seconds}${r}`,!1,n)}if(e.IS_LOCAL_DATETIME_SPACE.test(n)){let e=N(t),r=pe(e.ms,n);return new ve(`${e.year}-${e.month}-${e.day} ${e.hours}:${e.minutes}:${e.seconds}${r}`,!0,n)}if(e.IS_OFFSET_DATETIME_T.test(n)||e.IS_OFFSET_DATETIME_SPACE.test(n)){let r=n.match(/([+-]\d{2}:\d{2}|[Zz])$/),i=r?r[1]===`z`?`Z`:r[1]:`Z`,a=e.IS_OFFSET_DATETIME_SPACE.test(n),o=N(new Date(t.getTime()+me(i)*6e4)),s=a?` `:`T`,c=pe(o.ms,n,/\.(\d+)(?:[Zz]|[+-]\d{2}:\d{2})\s*$/);return new ye(`${o.year}-${o.month}-${o.day}${s}${o.hours}:${o.minutes}:${o.seconds}${c}${i}`,a)}return t}},ge=class extends Date{constructor(e){super(e)}toISOString(){let e=N(this);return`${e.year}-${e.month}-${e.day}`}},_e=class extends Date{originalFormat;constructor(e,t){let n=e;/:\d{2}:\d{2}/.test(e)||(n=e+`:00`),super(`0000-01-01T${n}Z`),this.originalFormat=t}toISOString(){let e=N(this);return`${e.hours}:${e.minutes}:${e.seconds}${pe(e.ms,this.originalFormat)}`}},ve=class extends Date{useSpaceSeparator=!1;originalFormat;constructor(e,t=!1,n){let r=e;/\d{2}:\d{2}:\d{2}/.test(e)||(r=e.replace(/(\d{2}:\d{2})([\s\-+TZ]|$)/,`$1:00$2`)),super(r.replace(` `,`T`)+`Z`),this.useSpaceSeparator=t,this.originalFormat=n||e}toISOString(){let e=N(this),t=this.useSpaceSeparator?` `:`T`;return`${e.year}-${e.month}-${e.day}${t}${e.hours}:${e.minutes}:${e.seconds}${pe(e.ms,this.originalFormat)}`}},ye=class extends Date{useSpaceSeparator=!1;originalOffset;originalFormat;constructor(e,t=!1){let n=e;/\d{2}:\d{2}:\d{2}/.test(e)||(n=e.replace(/(\d{2}:\d{2})([\s\-+TZ]|$)/,`$1:00$2`)),super(n.replace(` `,`T`)),this.useSpaceSeparator=t,this.originalFormat=e;let r=e.match(/([+-]\d{2}:\d{2}|[Zz])$/);r&&(this.originalOffset=r[1]===`z`?`Z`:r[1])}toISOString(){if(this.originalOffset){let e=N(new Date(this.getTime()+me(this.originalOffset)*6e4)),t=this.useSpaceSeparator?` `:`T`,n=pe(e.ms,this.originalFormat,/\.(\d+)(?:[Zz]|[+-]\d{2}:\d{2})\s*$/);return`${e.year}-${e.month}-${e.day}${t}${e.hours}:${e.minutes}:${e.seconds}${n}${this.originalOffset}`}let e=super.toISOString();return this.useSpaceSeparator?e.replace(`T`,` `):e}};const be=he,xe=`true`,Se=/e/i,Ce=/_/g,we=/^[+-]?inf$/,Te=/^[+-]?nan$/,Ee=/^[+-]?0x/i;function De(e){return e>=65&&e<=90||e>=97&&e<=122||e>=48&&e<=57||e===95||e===45}const Oe=/^[+-]?0o/i,ke=/^[+-]?0b/i;function Ae(e,t,n){for(let r=0;r23)throw new D(i,a,`"${r}": hour must be 00-23`)}if(t!==void 0){let e=parseInt(t,10);if(e<0||e>59)throw new D(i,a,`"${r}": minute must be 00-59`)}if(n!==void 0){let e=parseInt(n,10);if(e<0||e>60)throw new D(i,a,`"${r}": second must be 00-60`)}}function Re(e,t,n){if(Ee.test(e)||Oe.test(e)||ke.test(e))return;let r=e.replace(/^[+-]/,``).replace(/_/g,``);if(!/^[0-9]+$/.test(r))throw new D(t,n,`Invalid integer "${e}"`)}function ze(e,t,n,r,i,a,o){let s=r[0]+r[1].toUpperCase();if(RegExp(`^[+\\-]?`+s.replace(/([[\]])/g,`\\$1`)).test(e))throw new D(t,n,`${a} prefix must be lowercase "${r}"`);if(RegExp(`^[+\\-]?`+r+`_`,`i`).test(e))throw new D(t,n,`Underscore must be between digits`);let c=RegExp(`^[+\\-]?`+r,`i`),l=e.replace(c,``);if(!l||l===`_`||l.startsWith(`_`))throw new D(t,n,`Incomplete ${a.toLowerCase()} number`);let u=l.replace(/_/g,``);if(!i.test(u))throw new D(t,n,o??`Invalid ${a.toLowerCase()} digits`);if(/^[+-]/.test(e))throw new D(t,n,`${a} numbers cannot have a sign prefix`)}function*Be(e,t){let n=new se(te(e,t));try{for(;!n.next().done;){let t=$e(n,e);for(let e of t)yield e}}finally{y()}}function*Ve(e,t,n){for(let t of e)yield t;let r=new se(te(t));try{for(;!r.next().done;){let e=$e(r,t);for(let t of e)n&&Ue(t,n),yield t}}finally{y()}}function He(e,t){return{line:e.line+t.line-1,column:e.line===1?e.column+t.column:e.column}}function Ue(e,t){let n=[e];for(;n.length>0;){let e=n.pop();if(e.loc.start=He(e.loc.start,t),e.loc.end=He(e.loc.end,t),e.type===`KeyValue`){let t=e;n.push(t.value,t.key)}else if(e.type===`TableKey`||e.type===`TableArrayKey`||e.type===`InlineItem`)n.push(e.item);else if(e.type===`Table`||e.type===`TableArray`){let t=e;n.push(t.key);for(let e=t.items.length-1;e>=0;e--)n.push(t.items[e])}else if(e.type===`InlineTable`||e.type===`InlineArray`){let t=e.items;for(let e=t.length-1;e>=0;e--)n.push(t[e])}}}function We(e){return{type:`Comment`,loc:e.value.loc,raw:e.value.raw}}function Ge(e,t){let n=!e.peek().done&&e.peek().value.type===`Bracket`?`TableArray`:`Table`,r=n===`Table`;if(r&&e.value.raw!==`[`)throw new D(t,e.value.loc.start,`Expected table opening "[", found ${e.value.raw}`);if(!r){let n=e.peek();if(n.done)throw new D(t,e.value.loc.start,`Expected "[[" for Array of Tables, found end of input`);if(e.value.raw!==`[`||n.value.raw!==`[`)throw new D(t,e.value.loc.start,`Expected "[[", found ${e.value.raw+n.value.raw}`);let r=e.value,i=n.value;if(r.loc.end.line!==i.loc.start.line||r.loc.end.column!==i.loc.start.column)throw new D(t,r.loc.start,`"[[" brackets must be adjacent (no whitespace)`)}let i=r?{type:`TableKey`,loc:e.value.loc}:{type:`TableArrayKey`,loc:e.value.loc};if(e.next(),n===`TableArray`&&e.next(),e.done)throw new D(t,i.loc.start,`Expected table key, reached end of file`);if(e.value.type===`Bracket`&&e.value.raw===`]`)throw new D(t,e.value.loc.start,n===`TableArray`?`Array of Tables header [[]] requires a table name`:`Table header [] requires a table name`);let a=e.value.raw;je(a,t,e.value.loc.start),a.startsWith(`"`)||a.startsWith(`'`)||Ae(a,t,e.value.loc.start);let o;for(o=[Me(e.value.raw,t,e.value.loc.start)],i.item={type:`Key`,loc:e.value.loc,raw:e.value.raw,value:o};!e.peek().done&&e.peek().value.type===`Dot`;){e.next();let n=e.value;e.next();let r=e.value.raw;r.startsWith(`"`)||r.startsWith(`'`)||Ae(r,t,e.value.loc.start);let a=` `.repeat(n.loc.start.column-i.item.loc.end.column),o=` `.repeat(e.value.loc.start.column-n.loc.end.column);i.item.loc.end=e.value.loc.end,i.item.raw+=`${a}.${o}${e.value.raw}`;try{i.item.value.push(ie(e.value.raw))}catch(n){let r=n;throw new D(t,e.value.loc.start,r.message)}}if(e.next(),!e.done){let n=i.loc.start.line;if(e.value.loc.start.line!==n)throw new D(t,e.value.loc.start,r?`Table header must be single-line`:`Array of Tables header must be single-line`)}if(r&&(e.done||e.value.raw!==`]`))throw new D(t,e.done?i.item.loc.end:e.value.loc.start,`Expected table closing "]", found ${e.done?`end of file`:e.value.raw}`);if(!r&&(e.done||e.peek().done||e.value.raw!==`]`||e.peek().value.raw!==`]`))throw new D(t,e.done||e.peek().done?i.item.loc.end:e.value.loc.start,`Expected "]]" closing, found ${e.done||e.peek().done?`end of file`:e.value.raw+e.peek().value.raw}`);if(!r){let n=e.value,r=e.peek().value;if(n.loc.end.line!==r.loc.start.line||n.loc.end.column!==r.loc.start.column)throw new D(t,n.loc.start,`"]]" brackets must be adjacent (no whitespace)`)}if(r||e.next(),i.loc.end=e.value.loc.end,!e.peek().done){let n=e.peek().value;if(n.loc.start.line===i.loc.end.line&&n.type!==`Comment`)throw new D(t,n.loc.start,`Extra content after ${r?`table`:`Array of Tables`} header`)}let s=[];for(;!e.peek().done&&e.peek().value.type!==`Bracket`;){e.next();let n=$e(e,t);for(let e=0;e23)throw new D(t,n,`Offset "${e}": hour must be 00-23`);let c=parseInt(o,10);if(c<0||c>59)throw new D(t,n,`Offset "${e}": minute must be 00-59`)}if(!/^\d{4}-\d{2}-\d{2}(?:[Tt ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})?)?$/.test(e)&&!/^\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?$/.test(e))throw/^\d{4}-\d{2}-\d{2}T$/.test(e)?new D(t,n,`"${e}": 'T' requires time component`):/^\d{4}-\d{2}-\d{2}[a-su-zA-SU-Z]/.test(e)?new D(t,n,`Invalid date "${e}": unexpected character after date`):/^\d{4}-\d{2}-\d{2}\d{2}:\d{2}/.test(e)?new D(t,n,`"${e}": missing 'T' or space separator`):new D(t,n,`Invalid datetime "${e}"`);let i=e.match(/^(\d+)-/);if(i&&i[1].length!==4)throw new D(t,n,`"${e}": year must be 4 digits`);let a=e.match(/^(\d+)-(\d+)-(\d+)/);if(a){let[,,r,i]=a;if(r.length!==2)throw new D(t,n,`"${e}": month must be 2 digits`);if(i.length!==2)throw new D(t,n,`"${e}": day must be 2 digits`)}let o=e.match(/[T ](\d+):(\d+)(?::(\d+))?/);if(o){let[,r,i,a]=o;Ie(r,i,a,e,t,n)}let s=e.match(/^(\d+):(\d+)(?::(\d+))?/);if(s&&!a){let[,r,i,a]=s;Ie(r,i,a,e,t,n)}let c=e.match(/^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2}))?)?/),l=e.match(/^(\d{2}):(\d{2})(?::(\d{2}))?/);if(c){let[,r,i,a,o,s,l]=c,u=parseInt(i,10);if(u<1||u>12)throw new D(t,n,`"${e}": month must be 01-12`);let d=parseInt(a,10);if(d<1||d>31)throw new D(t,n,`"${e}": day must be 01-31`);if(d>Ye(parseInt(r,10),u))throw new D(t,n,`"${e}": day ${a} invalid for ${r}-${i}`);Le(o,s,l,e,t,n)}else if(l){let[,r,i,a]=l;Le(r,i,a,e,t,n)}}function Ze(e,t){let n=e.value.loc,r=e.value.raw,i;if(we.test(r))i=r.startsWith(`-`)?-1/0:1/0;else if(Te.test(r)){let e=new Float64Array(1),t=new DataView(e.buffer);r.startsWith(`-`)?(t.setUint32(0,0,!0),t.setUint32(4,4294443008,!0)):(t.setUint32(0,0,!0),t.setUint32(4,2146959360,!0)),i=e[0];let a=r.startsWith(`+`)?`+`:r.startsWith(`-`)?`-`:void 0;return{type:`Float`,loc:n,raw:r,value:i,nanSign:a}}else if(!e.peek().done&&e.peek().value.type===`Dot`){let a=n.start;{if(Se.test(r)&&!Ee.test(r))throw new D(t,n.start,`Float "${r}": dot after exponent`);let e=r;Pe(e,t,n.start,`the integer part of a float`);let i=e.replace(/^[+-]/,``);if(i===``||i===`_`)throw new D(t,n.start,`Float: digit required before dot`);Ne(e,t,n.start)}if(e.next(),e.peek().done||e.peek().value.type!==`Literal`)throw new D(t,e.value.loc.end,`Expected fraction value for Float`);e.next(),r+=`.${e.value.raw}`,n={start:a,end:e.value.loc.end};{let n=e.value.raw;if(!/^\d/.test(n))throw new D(t,e.value.loc.start,`Float: fraction must start with digit, found "${n}"`);Ne(n,t,e.value.loc.start,!1),Fe(n,r,t,e.value.loc.start)}i=Number(r.replace(Ce,``))}else{if(Ne(r,t,n.start),Fe(r,r,t,n.start),!e.peek().done&&e.peek().value.type===`Dot`)throw new D(t,e.peek().value.loc.start,`Float "${r}.": dot after exponent`);Pe(r,t,n.start,`the integer part of a float`),i=Number(r.replace(Ce,``))}if(Number.isNaN(i)&&!Te.test(r))throw new D(t,n.start,`Invalid float "${r}"`);return{type:`Float`,loc:n,raw:r,value:i}}function Qe(e,t){let n=e.value.raw,r=e.value.loc;if(/^\d{1,}-\d{1,}/.test(n)||/^\d{1,}:\d{1,}/.test(n)||/^\d{6}-\d{2}$/.test(n))throw new D(t,r.start,`Invalid integer "${n}"`);if(n===`-0`||n===`+0`)return{type:`Integer`,loc:r,raw:n,value:0};if(/^[+-]{2,}/.test(n))throw new D(t,r.start,`Double sign is not allowed in integers`);Pe(n,t,r.start,`decimal integers`),Ne(n,t,r.start),Re(n,t,r.start);let i=10;Ee.test(n)?(i=16,ze(n,t,r.start,`0x`,/^[0-9a-fA-F]+$/,`Hexadecimal`)):Oe.test(n)?(i=8,ze(n,t,r.start,`0o`,/^[0-7]+$/,`Octal`,`Invalid octal digits (must be 0-7)`)):ke.test(n)&&(i=2,ze(n,t,r.start,`0b`,/^[01]+$/,`Binary`,`Invalid binary digits (must be 0 or 1)`));let a=parseInt(n.replace(Ce,``).replace(Oe,``).replace(ke,``),i);if(Number.isNaN(a))throw new D(t,r.start,`Invalid integer "${n}"`);return{type:`Integer`,loc:r,raw:n,value:a}}function $e(e,t){if(e.value.type===`Comment`)return[We(e)];if(e.value.type===`Bracket`)return[Ge(e,t)];if(e.value.type===`Literal`)return et(e,t);throw e.value.type===`Equal`?new D(t,e.value.loc.start,`Missing key before '='`):new D(t,e.value.loc.start,`Unexpected token "${e.value.type}"`)}function et(e,t){let n=e.value.raw;if(n.endsWith(`:`))throw new D(t,{line:e.value.loc.start.line,column:e.value.loc.start.column+[...n].length-1},`Use '=' to separate keys and values, not ':'`);je(n,t,e.value.loc.start),n.startsWith(`"`)||n.startsWith(`'`)||Ae(n,t,e.value.loc.start);let r;r=[Me(e.value.raw,t,e.value.loc.start)];let i={type:`Key`,loc:e.value.loc,raw:e.value.raw,value:r};for(;!e.peek().done&&e.peek().value.type===`Dot`;){e.next();let n=e.value;e.next();let r=e.value.raw;je(r,t,e.value.loc.start),r.startsWith(`"`)||r.startsWith(`'`)||Ae(r,t,e.value.loc.start);let a=S(t,i.loc.end),o=S(t,n.loc.end),s=a.slice(i.loc.end.column,n.loc.start.column),c=o.slice(n.loc.end.column,e.value.loc.start.column);i.loc.end=e.value.loc.end,i.raw+=`${s}.${c}${e.value.raw}`,i.value.push(Me(e.value.raw,t,e.value.loc.start))}if(e.next(),!e.done&&e.value.loc.start.line!==i.loc.end.line)throw new D(t,e.value.loc.start,`"=" must be on same line as key`);if(e.done||e.value.type!==`Equal`)throw!e.done&&e.value.raw===`:`?new D(t,e.value.loc.start,`Use '=' to separate keys and values, not ':'`):new D(t,e.done?i.loc.end:e.value.loc.start,`Expected "=" for key-value`);let a=e.value.loc.start.column,o=e.value.loc.start.line;if(e.next(),e.done)throw new D(t,i.loc.start,`Expected value, reached EOF`);if(e.value.loc.start.line!==o)throw new D(t,e.value.loc.start,`Value must be on same line as '='`);if(e.done)throw new D(t,i.loc.start,`Expected value for key-value`);let s=tt(e,t),c=s[0];if(!e.peek().done){let n=e.peek().value;if(n.type===`Dot`&&n.loc.start.line===c.loc.end.line&&(c.type===`Float`||c.type===`Integer`))throw new D(t,n.loc.start,`Invalid number: multiple decimal points not allowed`);if((n.type===`Literal`||n.type===`Bracket`)&&n.loc.start.line===c.loc.end.line)throw new D(t,n.loc.start,`Key/value pairs must be separated by a newline`)}return s[0]={type:`KeyValue`,key:i,value:c,loc:{start:w(i.loc.start),end:w(c.loc.end)},equals:a},s}function tt(e,t){if(e.value.type===`Literal`){let n=e.value.raw;return n[0]===`"`||n[0]===`'`?[Ke(e,t)]:n===xe||n===`false`?[qe(e)]:/^\d/.test(n)&&(/^\d{1,}-\d{1,}/.test(n)||/^\d{1,}:\d{1,}/.test(n))?[Je(e,t)]:!e.peek().done&&e.peek().value.type===`Dot`||we.test(n)||Te.test(n)||Se.test(n)&&!Ee.test(n)?[Ze(e,t)]:[Qe(e,t)]}if(e.value.type===`Curly`){let[n,r]=rt(e,t);return[n,...r]}if(e.value.type===`Bracket`){let[n,r]=it(e,t);return[n,...r]}throw e.value.type===`Dot`?new D(t,e.value.loc.start,`Number cannot start with a dot`):new D(t,e.value.loc.start,`Unrecognized token type`)}function nt(e){for(let t=1;t=1)return e}}}function rt(e,t){if(e.value.raw!==`{`)throw new D(t,e.value.loc.start,`Expected "{" for inline table`);let n={type:`InlineTable`,loc:e.value.loc,items:[]},r=[];for(e.next();!e.done&&(e.value.type!==`Curly`||e.value.raw!==`}`);){if(e.value.type===`Comment`){r.push(We(e)),e.next();continue}if(e.value.type===`Comma`){let r=n.items[n.items.length-1];if(!r)throw new D(t,e.value.loc.start,`Leading comma in inline table`);if(r.comma)throw new D(t,e.value.loc.start,`Consecutive commas in inline table`);r.comma=!0,r.loc.end=e.value.loc.start,e.next();continue}let i=n.items[n.items.length-1];if(i&&!i.comma)throw new D(t,e.value.loc.start,`Missing comma between inline table items`);let a=$e(e,t),o=a[0];if(o.type===`KeyValue`){n.items.push({type:`InlineItem`,loc:T(o.loc),item:o,comma:!1});for(let e=1;e=-(2**53-1)&&e<=2**53-1}function ct(e){return typeof e==`bigint`}function lt(e){return typeof e==`number`&&(!st(e)||!isFinite(e)||Object.is(e,-0))}function ut(e){return typeof e==`boolean`}function dt(e){return Object.prototype.toString.call(e)===`[object Date]`}const ft=new Set([`Temporal.PlainDate`,`Temporal.PlainTime`,`Temporal.PlainDateTime`,`Temporal.ZonedDateTime`,`PlainDate`,`PlainTime`,`PlainDateTime`,`ZonedDateTime`]);function pt(e){var t;return typeof e==`object`&&!!e&&ft.has((t=e.constructor)==null?void 0:t.name)&&typeof e.equals==`function`}function mt(e){var t;let n=((t=e.constructor)==null?void 0:t.name)??``;if(n===`Temporal.ZonedDateTime`||n===`ZonedDateTime`){let t=e.toString(),n=t.match(/\[(.+)\]$/);if(n&&!/^[+-]\d{2}:\d{2}$/.test(n[1]))throw Error(`ZonedDateTime with IANA timezone "${t}" cannot be represented in TOML. TOML only supports offset-based timezones (+05:30, Z).`);return t.replace(/\[.*\]$/,``).replace(/(\+00:00)$/,`Z`)}let r=e.toString();if(/\[.*\]/.test(r))throw Error(`Temporal value contains unsupported annotation: "${r}". TOML only supports ISO 8601 calendar.`);return r}function F(e){return e&&typeof e==`object`&&!dt(e)&&!pt(e)&&!Array.isArray(e)}function ht(e){return e!=null&&typeof e[Symbol.iterator]==`function`}function gt(e){return e.startsWith(`"`)&&!e.startsWith(`"""`)}function _t(e){return e.startsWith(`"""`)}function vt(e){return e.startsWith(`'`)&&!e.startsWith(`'''`)}function yt(e){return e.startsWith(`'''`)}function bt(e,t){return t in e}function I(e,t){if(e.length!==t.length)return!1;for(let n=0;n0;){let e=r.pop();if(`text`in e){t.push(e.text),`leave`in e&&n.delete(e.leave);continue}let i=e.value;if(F(i)){if(n.has(i))throw TypeError(`Cannot stableStringify a circular structure`);n.add(i);let e=Object.keys(i).sort();t.push(`{`),r.push({leave:i,text:`}`});for(let t=e.length-1;t>=0;t--){let n=e[t];t=0;e--)e57343)){if(n<=56319){let n=e.charCodeAt(t+1);if(n>=56320&&n<=57343){t++;continue}}return{index:t,code:n}}}}function Et(e,t){if(wt!==void 0&&wt.call(e))return;let n=Tt(e);if(!n)return;let r=n.code.toString(16).toUpperCase().padStart(4,`0`);throw Error(`${t} contains a lone surrogate (U+${r}) at index ${n.index}. Unpaired surrogates are not Unicode scalar values and cannot be encoded as TOML.`)}function Dt(e,t){ht(e)?n(e,null):r(e,null);function n(e,t){for(let n of e)r(n,t)}function r(e,i){let a=t[e.type];switch(a&&typeof a==`function`&&a(e,i),a&&a.enter&&a.enter(e,i),e.type){case`Document`:n(e.items,e);break;case`Table`:r(e.key,e),n(e.items,e);break;case`TableKey`:r(e.item,e);break;case`TableArray`:r(e.key,e),n(e.items,e);break;case`TableArrayKey`:r(e.item,e);break;case`KeyValue`:r(e.key,e),r(e.value,e);break;case`InlineArray`:n(e.items,e);break;case`InlineItem`:r(e.item,e);break;case`InlineTable`:n(e.items,e);break;case`Key`:case`String`:case`Integer`:case`Float`:case`Boolean`:case`DateTime`:case`Comment`:break;default:throw Error(`Unrecognized node type "${e.type}"`)}a&&a.exit&&a.exit(e,i)}}function Ot(e){return new TextDecoder(`utf-8`,{fatal:!0,ignoreBOM:!0}).decode(e)}function kt(e){return e.charCodeAt(0)===65279}function At(e){return kt(e)?e.slice(1):e}const jt=`auto`,Mt=`auto`;function Nt(e){for(let t of e){let e=Lt(t);if(e!==null)return e}return!1}function Pt(e,t){for(let n of t){let t=Ft(n,e);if(t!==null)return t}return!0}function Ft(e,t){if(!e||typeof e!=`object`)return null;if((e.type===`InlineArray`||e.type===`InlineTable`)&&e.loc){let n=It(e.loc,t);if(n!==null)return n}if(e.items&&Array.isArray(e.items))for(let n of e.items){let e=Ft(n,t);if(e!==null)return e;if(n.item){let e=Ft(n.item,t);if(e!==null)return e}}for(let n of[`value`,`key`,`item`])if(e[n]){let r=Ft(e[n],t);if(r!==null)return r}return null}function It(e,t){if(!e||!e.start||!e.end)return null;let n=t.split(/\r?\n/),r=e.start.line-1,i=e.end.line-1,a=e.start.column,o=e.end.column,s=``;if(r===i){var c;s=((c=n[r])==null?void 0:c.substring(a,o+1))||``}else{n[r]&&(s+=n[r].substring(a));for(let e=r+1;e0:u?u[1].length>0:null}function Lt(e){if(!e||typeof e!=`object`)return null;if(e.type===`InlineArray`&&e.items&&Array.isArray(e.items)||e.type===`InlineTable`&&e.items&&Array.isArray(e.items))return Rt(e.items);if(e.type===`KeyValue`&&e.value)return Lt(e.value);if(e.items&&Array.isArray(e.items))for(let t of e.items){let e=Lt(t);if(e!==null)return e}return null}function Rt(e){if(e.length===0)return null;let t=e[e.length-1];return t&&typeof t==`object`&&`comma`in t?t.comma===!0:!1}function zt(e){let t=e.indexOf(`
+`);return t>0&&e[t-1]===`\r`?`\r
+`:`
+`}function Bt(e){let t=0,n=e.length;for(;n>0&&e[n-1]===`
+`;)t++,n--,n>0&&e[n-1]===`\r`&&n--;return t}function Vt(e){let t=e.split(/\r?\n/),n=0,r=0;for(let e of t)if(e.length!==0&&(e[0]===` `?n++:e[0]===` `&&r++,n+r>=5))break;return n>0&&n>=r}function Ht(e){let t=0;for(;t{var s,c,l;if(!e||typeof e!=`object`)return;if(t!==void 0&&a.has(e.type)&&((s=e.loc)==null||(s=s.start)==null?void 0:s.line)>t){let t=Ht(n[e.loc.start.line-1]??``);t>0&&r.push(t)}let u=i.has(e.type)&&((c=e.loc)==null||(c=c.end)==null?void 0:c.line)>((l=e.loc)==null||(l=l.start)==null?void 0:l.line)?e.loc.start.line:t;if(Array.isArray(e.items))for(let t of e.items)o(t,u);e.item&&o(e.item,u),e.value&&o(e.value,u)},s=Array.isArray(t)?t:Array.from(t??[]);for(let e of s)o(e);if(r.length>0)return Math.min(...r);let c=n.filter(e=>e.trim().length>0&&!e.trimStart().startsWith(`#`)).map(Ht).filter(e=>e>0);return c.length>0?Math.min(...c):2}const Wt=new Map([[`
+`,`
+`],[`\r
+`,`\r
+`],[`\\n`,`
+`],[`\\r\\n`,`\r
+`],[`LF`,`
+`],[`CRLF`,`\r
+`],[`UNIX`,`
+`],[`DOS`,`\r
+`]]);function Gt(e){let t=Wt.get(e)??Wt.get(e.toUpperCase());if(t===void 0)throw TypeError(`Invalid newLine value: expected LF or CRLF`);return t}function Kt(e){if(!e||typeof e!=`object`)return{};let t=e=>typeof e==`boolean`?null:`expected boolean, got ${typeof e}`,n=e=>e==null||typeof e==`boolean`||e===`auto`||e===`parent`||typeof e==`number`&&Number.isInteger(e)&&e>=0?null:`expected boolean, non-negative integer, "auto" or "parent", got ${typeof e}`,r={newLine:e=>typeof e==`string`?null:`expected string, got ${typeof e}`,trailingNewline:e=>typeof e==`boolean`||typeof e==`number`?null:`expected boolean or number, got ${typeof e}`,trailingComma:t,bracketSpacing:t,leadingBom:t,inlineTableStart:e=>e==null||typeof e==`number`&&Number.isInteger(e)&&e>=0?null:`expected non-negative integer or undefined, got ${typeof e}`,truncateZeroTimeInDates:t,useTabsForIndentation:t,indentWidth:e=>e==null||typeof e==`number`&&Number.isInteger(e)&&e>0?null:`expected positive integer or undefined, got ${typeof e}`,minimumDecimals:e=>e==null||typeof e==`number`&&Number.isInteger(e)&&e>=0?null:`expected non-negative integer or undefined, got ${typeof e}`,updateOrder:t,multilineTable:n,multilineArray:n},i={},a=[],o=[];for(let t in e){let n=Object.prototype.hasOwnProperty.call(r,t)?r[t]:void 0;if(n){let r=e[t],a=n(r);a?o.push(`${t} (${a})`):i[t]=r}else Object.prototype.hasOwnProperty.call(e,t)&&a.push(t)}if(a.length>0&&console.warn(`toml-patch: Ignoring unsupported format properties: ${a.join(`, `)}. Supported properties are: ${Object.keys(r).join(`, `)}`),o.length>0)throw TypeError(`Invalid types for format properties: ${o.join(`, `)}`);return`newLine`in i&&(i.newLine=Gt(i.newLine)),i}function L(e,t){if(e){if(e instanceof R)return e;{let n=Kt(e);return new R(n.newLine??t.newLine,n.trailingNewline??t.trailingNewline,n.trailingComma??t.trailingComma,n.bracketSpacing??t.bracketSpacing,`inlineTableStart`in n?n.inlineTableStart:t.inlineTableStart,n.truncateZeroTimeInDates??t.truncateZeroTimeInDates,n.useTabsForIndentation??t.useTabsForIndentation,n.minimumDecimals??t.minimumDecimals,n.leadingBom??t.leadingBom,n.updateOrder??t.updateOrder,n.indentWidth??t.indentWidth,n.multilineTable??t.multilineTable,n.multilineArray??t.multilineArray)}}return t}var R=class e{newLine;trailingNewline;trailingComma;bracketSpacing;leadingBom;inlineTableStart;truncateZeroTimeInDates;useTabsForIndentation;indentWidth;minimumDecimals;updateOrder;multilineTable;multilineArray;constructor(e,t,n,r,i,a,o,s,c,l,u,d,f){this.newLine=e==null?`
+`:Gt(e),this.trailingNewline=t??1,this.trailingComma=n??!1,this.bracketSpacing=r??!0,this.inlineTableStart=i??1,this.truncateZeroTimeInDates=a??!1,this.useTabsForIndentation=o??!1,this.indentWidth=this.useTabsForIndentation?1:u??2,this.minimumDecimals=s??0,this.leadingBom=c??!1,this.updateOrder=l??!1,this.multilineTable=d??`auto`,this.multilineArray=f??`auto`}static default(){return new e(`
+`,1,!1,!0,1,!1,!1,0,!1,!1,2,jt,Mt)}static autoDetectFormat(t){return e.autoDetectFormatWithCst(t)}static autoDetectFormatWithCst(t,n){let r=e.default();r.leadingBom=kt(t);let i=At(t),a=[];r.newLine=zt(i),r.trailingNewline=Bt(i);try{a=Array.isArray(n)?n:Array.from(n??Be(i)),r.trailingComma=Nt(a),r.bracketSpacing=Pt(i,a)}catch{r.trailingComma=!1,r.bracketSpacing=!0}return r.useTabsForIndentation=Vt(i),r.indentWidth=r.useTabsForIndentation?1:Ut(i,a),r.inlineTableStart=1,r.truncateZeroTimeInDates=!1,r.minimumDecimals=0,r.updateOrder=!1,r.multilineTable=jt,r.multilineArray=Mt,r}static autoDetectFormatWithAst(t,n){return e.autoDetectFormatWithCst(t,n)}};const qt=new WeakMap,Jt=new WeakMap,Yt=new WeakSet,Xt=new WeakMap;function Zt(e,t){return(e[t.line-1]??e[e.length-1])+t.column}function Qt(e,t){let n=$t(t);for(let t of e)n(t)}function $t(e){let n=[0];for(let t=0;t{let o=[i];for(;o.length>0;){let i=o.pop(),s=[Zt(n,i.loc.start),Zt(n,i.loc.end)];if(Object.defineProperty(i,"range",{configurable:!0,enumerable:!1,value:s}),qt.set(i,e),Xt.set(i,tn(i).slice()),a(i))o.push(i.value),o.push(i.key);else if(u(i))o.push(i.item);else if(t(i)||r(i)){o.push(i.key);let e=i.items;for(let t=e.length-1;t>=0;t--)o.push(e[t])}else if(p(i)){let e=i.items;for(let t=e.length-1;t>=0;t--)o.push(e[t])}}}}function z(e){return qt.get(e)}function en(e){let t=Xt.get(e);if(!t)return!1;let n=tn(e);return t.length===n.length&&t.every((e,t)=>e===n[t])}function tn(e){return a(e)?[e.key,e.value]:u(e)?[e.item]:t(e)||r(e)?[e.key,...e.items]:p(e)?e.items:[]}function nn(e){if(Yt.has(e))return;let t=[e];for(;t.length>0;){let e=t.pop();for(let n of tn(e))Jt.set(n,e),t.push(n)}Yt.add(e)}function rn(e){let t=[e];for(;t.length>0;){let e=t.pop();on(e);for(let n of tn(e))t.push(n)}}function an(e){rn(e)}function on(e){Object.defineProperty(e,"dirty",{configurable:!0,enumerable:!1,value:!0,writable:!0})}function sn(e,t,n){nn(e),n&&(Jt.set(n,t),rn(n));let r=t;for(;r;)on(r),r=Jt.get(r)}function cn(e){let t=Xt.get(e);return!!(t!=null&&t[0])&&t[0].loc.start.line>e.loc.start.line}const ln=new WeakMap,un=new WeakSet,dn=new WeakSet,fn=new WeakSet,pn=new WeakSet;function mn(e,t,n,r){let i=e===`array`?r.multilineArray:r.multilineTable;return typeof i==`boolean`?i:typeof i==`number`?t>=i:i===`parent`&&n}function hn(e,t){ln.set(e,t),t&&e.items.length===0&&(e.loc.end.line=e.loc.start.line+1,e.loc.end.column=e.loc.start.column+1)}function gn(e){return ln.get(e)}function _n(e){un.add(e)}function vn(e){return un.has(e)}function yn(e){dn.add(e)}function bn(e){return dn.has(e)}function xn(e){fn.add(e)}function Sn(e){return fn.has(e)}function Cn(e){pn.add(e)}function wn(e){return pn.has(e)}function Tn(e){return e.items.length?e.items.some((t,n)=>{let r=e.items[n-1];return t.loc.start.line>((r==null?void 0:r.loc.end.line)??e.loc.start.line)}):e.loc.end.line>e.loc.start.line}function En(e,t){let n=(e,r)=>{if(e===t)return r;if(a(e))return n(e.value,r);if(u(e))return n(e.item,r);if(l(e)||d(e)){for(let t of e.items){let e=n(t,r+1);if(e!==void 0)return e}return}if(p(e))for(let t of e.items){let e=n(t,r);if(e!==void 0)return e}};return n(e,0)}function Dn(e,t){let n=(e,r)=>{if(e===t)return r;if(a(e))return n(e.value,r);if(u(e))return n(e.item,r);if(l(e)||d(e)){for(let t of e.items){let r=n(t,e);if(r)return r}return}if(p(e))for(let t of e.items){let e=n(t,r);if(e)return e}};return n(e,void 0)}const On=new WeakSet,kn=new WeakSet,An=new WeakMap,jn=new WeakMap;function Mn(e,t){An.set(e,t)}function Nn(e,t){jn.set(e,t)}function Pn(e){kn.add(e)}const Fn=new WeakMap,In=new WeakSet,Ln=new WeakSet;function Rn(e){return Ln.has(e)}function zn(e){Ln.delete(e)}function Bn(e,t,n){rr(n,B(e),t)}function Vn(e){an(e),On.add(e)}const Hn=new WeakMap,Un=e=>(Hn.has(e)||Hn.set(e,new WeakMap),Hn.get(e));function Wn(e){return Hn.has(e)||Hn.set(e,new WeakMap),Hn.get(e)}const Gn=new WeakMap;function B(e){return Gn.has(e)||Gn.set(e,new WeakMap),Gn.get(e)}function Kn(e,t,n,r){if(p(t)){let e=t.items.indexOf(n);if(e<0)throw Error(`Item not found in parent for replace`);t.items.splice(e,1,r)}else if(a(t)&&d(t.value)&&!d(n)){let e=t.value.items.indexOf(n);if(e<0)throw Error(`Item not found in parent for replace`);t.value.items.splice(e,1,r)}else if(m(t))t.item=r;else if(a(t))t.key===n?t.key=r:t.value=r;else throw Error(`Unsupported parent type "${t.type}" for replace`);W(r,{lines:n.loc.start.line-r.loc.start.line,columns:n.loc.start.column-r.loc.start.column});let i=b(n.loc),o=b(r.loc);rr({lines:o.lines-i.lines,columns:o.columns-i.columns},B(e),r,n),kn.has(e)||sn(e,t,r),On.add(e)}function V(t,n,r,i,a,o,s,c=!1){if(!p(n))throw Error(`Unsupported parent type "${n.type}" for insert`);i=i!=null&&typeof i==`number`?i:n.items.length;let u,m;l(n)||d(n)?{shift:u,offset:m}=Zn(n,r,i,An.get(t)??2):a&&e(n)?{shift:u,offset:m}=Qn(n,r,i):{shift:u,offset:m}=Jn(t,n,r,i,s),W(r,u,{first_line_only:c});let h=n.items[i-1],g=h&&B(t).get(h);if(g&&(m.lines+=g.lines,m.columns+=g.columns,B(t).delete(h)),(d(n)||l(n))&&m.lines!==0&&(o||p(t))&&t!==n){let e=r.loc.start.line,i=o??t.items,a=qn(n,i);for(let t=0;ta.loc.end.line||r{t===e&&(r=!0)};if(Dt(t,{InlineArray:i,InlineTable:i}),r)return t}return e}function Jn(n,i,a,o,s){if(!h(a))throw Error(`Incompatible child type "${a.type}"`);let c=i.items[o-1],l=e(i)&&!i.items.length,u=e(i)&&!l&&c===void 0;i.items.splice(o,0,a);let d;if(c!==void 0){if(kn.has(n))d=c;else{let e=!1;for(let t=0;te||a.line===e&&a.column>t)&&(e=a.line,t=a.column,d=r)}}}}let p=d?{line:d.loc.end.line,column:f(d)?i.loc.start.column:d.loc.start.column}:w(i.loc.start),m=t(a)||r(a),g=0;s===void 0?l||u||(g=m?2:1):g=s,p.line+=g;let _={lines:p.line-a.loc.start.line,columns:p.column-a.loc.start.column},v=b(a.loc),y=!kn.has(n)&&c===void 0&&(t(i)||r(i)||e(i))&&Fn.has(i),x=y&&!In.has(i);y&&(Fn.delete(i),In.delete(i));let S=y&&!x,C=0;if(S){let e=t(i)||r(i)?B(n).get(i.key):Un(n).get(i);C=e?-e.lines:1}let T=x||S?g-2:g-1,E={lines:u?v.lines+1:v.lines+T,columns:v.columns};return{shift:{lines:_.lines+C,columns:_.columns},offset:E}}function Yn(e,t,n,r={}){let{useNewLine:i=!1,skipCommaSpace:a=2,skipBracketSpace:o=1,hasCommaHandling:s=!1,isLastElement:c=!1,hasSeparatingCommaBefore:u=!1,hasSeparatingCommaAfter:p=!1,hasTrailingComma:m=!1,indentWidth:h=2}=r,g=n>0?e.items[n-1]:void 0,_=g?{line:g.loc.end.line,column:i?f(g)?e.loc.start.column:g.loc.start.column:g.loc.end.column}:w(e.loc.start);if(!g&&i){let t=e.items.find((e,t)=>t>n&&!f(e));t?_.column=t.loc.start.column:e.loc.end.line>e.loc.start.line&&(_.column=(l(e)||d(e)?jn.get(e):void 0)??e.loc.end.column-1+h)}let v=0;if(i)v=1;else{let e=u||!s&&!!g;e&&s?_.column+=a:(e||s&&!g)&&(_.column+=o)}_.line+=v;let y={lines:_.line-t.loc.start.line,columns:_.column-t.loc.start.column},x=b(t.loc);if(!s)return{shift:y,offset:{lines:x.lines+(v-1),columns:x.columns}};let S=u&&m&&!p&&c,C=0;S&&(C=-1);let T=x.columns+(u||p?a:0)+(m?1+C:0);return i&&c&&g&&e.loc.end.line===g.loc.end.line&&(T=x.columns-(g.loc.end.column-g.loc.start.column)),{shift:y,offset:{lines:x.lines+(v-1),columns:T}}}function Xn(e){return fe(e)??2}function Zn(e,t,n,r){if(!u(t))throw Error(`Incompatible child type "${t.type}"`);let i=Xn(e),a=n==null?P(e.items):e.items[n-1],o=n==null||n===e.items.length,s=n==null?void 0:e.items[n];e.items.splice(n,0,t);let c=!!a,l=!o;return c&&(a.comma=!0),l&&(t.comma=!0),Yn(e,t,n,{useNewLine:nr(e,t)&&!(a&&s&&s.loc.start.line===a.loc.end.line&&s.loc.start.column>=a.loc.end.column)&&!(gn(e)!==!0&&d(e)&&!s&&a&&a.loc.end.line===e.loc.end.line),skipCommaSpace:i,hasCommaHandling:!0,isLastElement:o,hasSeparatingCommaBefore:c,hasSeparatingCommaAfter:l,hasTrailingComma:o&&t.comma===!0,indentWidth:r})}function Qn(e,t,n){let r=Yn(e,t,n,{useNewLine:!1,hasCommaHandling:!1});return e.items.splice(n,0,t),r}function H(n,i,a,o){if(!p(i))throw Error(`Unsupported parent type "${i.type}" for remove`);kn.has(n)||sn(n,i);let s=i.items.indexOf(a);if(s<0){if(s=i.items.findIndex(e=>m(e)&&e.item===a),s<0)throw Error(`Node not found in parent for removal`);a=i.items[s]}let c=i.items[s-1],h=i.items[s+1];i.items.splice(s,1);let g=b(a.loc);h&&f(h)&&h.loc.start.line===a.loc.end.line&&(g=b({start:a.loc.start,end:h.loc.end}),h=i.items[s+1],i.items.splice(s,1));let _=c&&u(c)||h&&u(h),v=c?B(n).get(c):void 0,y=c&&c.loc.end.line===a.loc.start.line+((v==null?void 0:v.lines)??0),x=h&&h.loc.start.line===a.loc.end.line,S=_&&(y||x),C={lines:-(g.lines-+!!S),columns:S?-g.columns:0};if((e(i)||t(i)||r(i))&&h&&!S&&h.loc.start.line>a.loc.end.line){let e=t(a)||r(a),o=t(h)||r(h),l=-1;for(let e=0;el&&(l=t)}let u=c?B(n).get(c):void 0,d=B(n).get(a),f=c?e?a.loc.start.line+((u==null?void 0:u.lines)??0)-l-1:0:o?h.loc.start.line+((d==null?void 0:d.lines)??0)-a.loc.end.line-1:0;f>0&&(C.lines-=f)}let w=(d(i)||l(i))&&i.loc.end.line>i.loc.start.line,T=w&&a.loc.start.line===i.loc.start.line;w&&!T&&c===void 0&&h===void 0&&a.loc.start.line>i.loc.start.line&&i.loc.end.line>a.loc.end.line&&Cn(i),c===void 0&&h===void 0&&(!w||T)&&(C.lines=0,C.columns=0,(d(i)||l(i))&&Ln.add(i),(t(i)||r(i)||e(i))&&Fn.set(i,!0)),(c!==void 0||h!==void 0)&&(t(i)||r(i)||e(i))&&In.add(i);let E=l(i)||d(i)?Xn(i):2;if(_&&y&&(C.columns-=E),_&&!c&&h&&(nr(i)||(C.columns-=E)),_&&c&&!h&&(c.comma=!!a.comma,y)){var D;let e=c?((D=B(n).get(c))==null?void 0:D.columns)??0:0;C.columns=c.loc.end.column-a.loc.end.column-e}let O,k;c?(O=c,k=B(n)):(t(i)||r(i))&&`key`in i?(O=i.key,k=B(n)):(O=i,k=Un(n));let ee=B(n),A=k.get(O);A&&(C.lines+=A.lines,C.columns+=A.columns);let te=ee.get(a);if(te&&(C.lines+=te.lines,S&&(C.columns+=te.columns)),k.set(O,C),On.add(n),w&&(o||p(n))&&n!==i){let e=a.loc.start.line,t=o??n.items,r=qn(i,t),s=[];for(let n=0;nr.loc.end.line||(o===e&&(o=0;e--)t.splice(s[e],1)}}function $n(e){let t=e.loc.start.line,n=e.loc.start.column;if(`key`in e){let r=e.key.loc.end;(r.line>t||r.line===t&&r.column>n)&&(t=r.line,n=r.column)}for(let r=0;rt||i.line===t&&i.column>n)&&(t=i.line,n=i.column)}e.loc.end={line:t,column:n}}function er(e,t,n=!0){if(!n||!t.items.length)return;rr({lines:0,columns:1},Un(e),t);let r=P(t.items);rr({lines:0,columns:1},B(e),r),On.add(e)}function tr(e,t,n=!1){if(!n||!t.items.length)return;let r=P(t.items);r.comma=!0,rr({lines:0,columns:1},B(e),r),On.add(e)}function U(e){if(!On.has(e))return;let t=Un(e),n=B(e),r=0,i={};function a(e){switch(e.type){case`Document`:{let t=e;o(t);for(let e=0;e{if(!o||e.loc.start.line===c){e.loc.start.column+=i;let t=(!o||s)&&e.loc.end.line!==e.loc.start.line&&((l(e)||d(e))&&gn(e)!==void 0&&!vn(e)||u(e)&&((l(e.item)||d(e.item))&&gn(e.item)!==void 0&&!vn(e.item)||a(e.item)&&(l(e.item.value)||d(e.item.value))&&gn(e.item.value)!==void 0&&!vn(e.item.value))||a(e)&&(l(e.value)||d(e.value))&&gn(e.value)!==void 0);(e.loc.end.line===e.loc.start.line||t)&&(e.loc.end.column+=i)}e.loc.start.line+=r,e.loc.end.line+=r};return Dt(e,{Table:p,TableKey:p,TableArray:p,TableArrayKey:p,KeyValue(e){let t=!o||e.loc.start.line===c;p(e),t&&(e.equals+=i)},Key:p,String:p,Integer:p,Float:p,Boolean:p,DateTime:p,InlineArray:p,InlineItem:p,InlineTable:p,Comment:p}),e}function nr(e,t){let n=gn(e);if(n===!0)return!0;if(n!==void 0||!e.items.length)return!1;let r=t?e.items.filter(e=>e!==t):e.items;return r.length?e.loc.end.line>e.loc.start.line&&r.every((e,t)=>t===0||e.loc.start.line>r[t-1].loc.start.line):e.loc.end.line>e.loc.start.line}function rr(e,t,n,r){let i=t.get(r||n);i&&(e.lines+=i.lines,e.columns+=i.columns),t.set(n,e)}function ir(e){let t=e.match(/\r\n|\n/);return(t==null?void 0:t[0])===`\r
+`?`\r
+`:`
+`}function ar(e,t){var n;let r=ir(e);e=e.replace(/\r\n/g,`
+`).replace(/\n/g,r);let i,a;e.startsWith(`"""\\${r}`)?(i=4+r.length,a=`"""\\${r}`):e.startsWith(`"""${r}`)?(i=3+r.length,a=`"""${r}`):(i=3,a=`"""`);let o=e.length-3,s=e.slice(i,o).split(r),c=s[s.length-1],l=s.length>1&&/^[\t ]*$/.test(c),u=l?c:``,d=(l?s.slice(0,-1):s).map(e=>{var t;if(/^[\t ]*$/.test(e))return{indent:e,content:``,trailingWs:``,hasBackslash:!1,isBlank:!0};let n=e,r=0;for(let e=n.length-1;e>=0&&n[e]===`\\`;e--)r++;let i=r%2==1;i&&(n=n.slice(0,-1));let a=((t=n.match(/^[\t ]*/))==null?void 0:t[0])??``,o=n.slice(a.length),s=o.match(/([\t ]+)$/),c=s?s[1]:``;return{indent:a,content:o.slice(0,o.length-c.length),trailingWs:c,hasBackslash:i,isBlank:!1}}),f=d.filter(e=>!e.isBlank),p=Math.max(...f.map(e=>e.content.length+e.trailingWs.length),1),m=f.filter(e=>e.hasBackslash),h=m[m.length-1]??f[f.length-1]??{indent:``,trailingWs:``,hasBackslash:!1,content:``,isBlank:!1},g=f[f.length-1]??h,_=[];{let e=[],t=0;for(let n of d)n.isBlank?e.push(n.indent):(t>0&&_.push(e),e=[],t++)}let v=f.some((e,t)=>!e.hasBackslash&&t0?((n=t.match(/^[\t ]*/))==null?void 0:n[0])??``:``,x=e=>{let t=e.match(/\S+| +/g)??[],n=[],r=0;for(;r0&&n[0]===` `)return null;let i=e[e.length-1];if(i.length>0&&i.length-i.trimEnd().length!==g.trailingWs.length)return null;for(let t=0;t0&&E.set(o,i);for(let e of a)S.push(e);t0&&(e[0]===` `||e.length-e.trimEnd().length!==g.trailingWs.length))return null;if(!v){let t=0,n=0;for(let r=0;r=t;r--){let t=f[r],a=t.content+t.trailingWs;if(a.length===0)break;let s=e.length-o-a.length;if(s>=n&&e.slice(s,s+a.length)===a)i++,o+=a.length;else break}if(i>0){let s=e.slice(n,e.length-o);if(s.length===0||s[0]!==` `){for(let e=0;e0)for(let t of _[e])C.push(t)}if(s.length>0){let e=x(s),n=e.join(``),r=/(?0&&!r){let e=t-1;if(e<_.length&&_[e].length>0)for(let t of _[e])C.push(t)}for(let n=0;n0&&/\s$/.test(i)?``:s.endsWith(` `)?` `:``;if(C.push(`${c}${i}${l}\\`),n0)for(let t of _[e])C.push(t)}}let a=f.length-i-1;if(!r&&a>=0&&a<_.length&&_[a].length>0)for(let e of _[a])C.push(e)}else if(t>0){let e=t-1;if(e<_.length&&_[e].length>0)for(let t of _[e])C.push(t)}let e=f.length-i;for(let t=e;te){let e=t-1;if(e<_.length&&_[e].length>0)for(let t of _[e])C.push(t)}C.push(r?`${n.indent}${n.content}${n.trailingWs}${n.hasBackslash&&n.content!==``?`\\`:``}`:`${n.indent}${n.content}${n.trailingWs}\\`)}return l&&!(g.hasBackslash&&g.content===``)?`${a}${C.join(r)}${r}${u}"""`:`${a}${C.join(r)}"""`}}let s=e.slice(n);if(t>0&&(n===e.length||s.length>0&&s[0]!==` `)){for(let e=0;e0)for(let t of _[e])C.push(t)}if(n===e.length){let e=f[t-1],n=g.hasBackslash&&e.content!==``?`\\`:``;C[C.length-1]=`${e.indent}${e.content}${g.trailingWs}${n}`}else{let e=x(s),n=e.join(``),r=/(?0)for(let e of _[i])C.push(e);for(let n=0;n0&&/\s$/.test(a)?``:` `,u=`\\`),C.push(`${c}${a}${l}${u}`),!i){let e=t+n;if(!r&&e<_.length&&_[e].length>0)for(let t of _[e])C.push(t)}}}return l&&!(g.hasBackslash&&g.content===``)?`${a}${C.join(r)}${r}${u}"""`:`${a}${C.join(r)}"""`}}let n=x(e);for(let e of n)S.push(e)}for(let e=0;e0?E.get(e)??``:i?y&&e===0?b:i.indent:r?g.indent:h.indent,o=S[e],s,c;if(r&&t?(s=g.trailingWs,c=g.hasBackslash&&g.content!==``?`\\`:``):r?(s=g.trailingWs,c=``):(s=o.length>0&&/\s$/.test(o)?``:` `,c=`\\`),C.push(`${a}${o}${s}${c}`),!t&&!n&&!D){let t=S.join(``);if(!/(?0)for(let t of _[e])C.push(t)}}return l&&!(g.hasBackslash&&g.content===``)?`${a}${C.join(r)}${r}${u}"""`:`${a}${C.join(r)}"""`}function or(e){switch(e){case`b`:return`\b`;case`t`:return` `;case`n`:return`
+`;case`f`:return`\f`;case`r`:return`\r`;case`"`:return`"`;case`\\`:return`\\`;default:return null}}function sr(e,t){return t===`singleline-basic`?e<=31||e===127:e>=0&&e<=7||e===11||e>=14&&e<=31||e===127}function cr(e,t){switch(e){case`\\`:return`\\\\`;case`\b`:return`\\b`;case` `:return`\\t`;case`\f`:return`\\f`;case`"`:return t===`singleline-basic`?`\\"`:`"`;case`
+`:return t===`singleline-basic`?`\\n`:`
+`;case`\r`:return t===`singleline-basic`?`\\r`:`\r`;default:{let n=e.charCodeAt(0);return sr(n,t)?`\\u${n.toString(16).padStart(4,`0`).toUpperCase()}`:e}}}function lr(e){let t=new Map;for(let n=0;n(Et(e,`Key ${JSON.stringify(e)}`),k.test(e)?e:_r(e))).join(`.`)}function yr(e){let t=vr(e);return{type:`Key`,loc:{start:E(),end:{line:1,column:t.length}},raw:t,value:e}}function br(e,t){return Et(e,`String value`),t?xr(e,t):Sr(e)}function xr(e,t){if(gt(t))return Sr(e,t);if(vt(t))return e.includes(`'`)?e.includes(`'''`)?Sr(e):Tr(e,`'''${t.slice(1,-1)}'''`):Cr(e);if(yt(t))return e.includes(`'''`)?wr(e,`"""${t.slice(3,-3)}"""`):Tr(e,t);if(_t(t))return wr(e,t);throw Error(`Existing raw string value is not valid: ${t}`)}function Sr(e,t){let n=``;return n=t?`"${dr(e,t,`singleline-basic`)}"`:_r(e),{type:`String`,loc:{start:E(),end:{line:1,column:n.length}},raw:n,value:e}}function Cr(e){let t=`'${e}'`,n;if(t.includes(`
+`)){let e=t.split(/\r\n|\n/),r=e[e.length-1];n={line:e.length,column:r.length}}else n={line:1,column:t.length};return{type:`String`,loc:{start:E(),end:n},raw:t,value:e}}function wr(e,t){let n=dr(e,t,`multiline-basic`),r=`"""`+(t.startsWith(`"""\r
+`)?`\r
+`:t.startsWith(`"""
+`)?`
+`:``)+n+`"""`,i=t.slice(3,-3);if(/(^|[^\\])(\\\\)*\\$/m.test(i)){let e=ar(t,n);e!==null&&(r=e)}let a=Er(r);return{type:`String`,loc:{start:E(),end:a},raw:r,value:e}}function Tr(e,t){let n=`'''`+(t.startsWith(`'''\r
+`)?`\r
+`:t.startsWith(`'''
+`)?`
+`:``)+e+`'''`,r=Er(n);return{type:`String`,loc:{start:E(),end:r},raw:n,value:e}}function Er(e){if(e.includes(`
+`)){let t=e.split(/\r\n|\n/),n=t[t.length-1];return{line:t.length,column:n.length}}return{line:1,column:e.length}}function Dr(e){let t=e.toString();return{type:`Integer`,loc:{start:E(),end:{line:1,column:t.length}},raw:t,value:e}}function Or(e,t=1){let n,r;if(e===1/0)n=`inf`;else if(e===-1/0)n=`-inf`;else if(Number.isNaN(e)){let t=new Float64Array([e]),i=!!(new DataView(t.buffer).getUint32(4,!0)&2147483648);r=i?`-`:void 0,n=i?`-nan`:`nan`}else if(Object.is(e,-0))n=`-0.`+`0`.repeat(Math.max(t,1));else{n=e.toString();let r=n.search(/[eE]/);if(r!==-1){let e=n.slice(0,r),i=n.slice(r),a=e.indexOf(`.`);if(a===-1)n=`${e}.${`0`.repeat(Math.max(t,1))}${i}`;else{let r=e.length-a-1;n=r{if(!a(e))return!1;let n=d(e.value),r=l(e.value)&&e.value.items.length>0&&e.value.items.every(e=>d(e.item));if(n||r){let n=Gr(e.key.value);return t.inlineTableStart===void 0||n{H(e,e,n),d(n.value)?n.value.items.length===0?V(e,e,G(n.key.value)):V(e,e,Lr(n,t.bracketSpacing)):Rr(n,t.bracketSpacing).forEach(t=>{V(e,e,t)})}),U(e),e)}function Lr(e,t){let n=G(e.key.value);Pn(n);for(let t of e.value.items)V(n,n,t.item,void 0,void 0,void 0,void 0,!0);return U(n),Hr(n,1,t),n}function Rr(e,n){let i=fr();Pn(i);for(let t of e.value.items){let n=mr(e.key.value);V(i,i,n);for(let e of t.item.items)V(i,n,e.item,void 0,void 0,void 0,void 0,!0)}U(i);for(let e of i.items)(t(e)||r(e))&&Hr(e,1,n);return i.items}function zr(e){let t=gn(e);return t===void 0?Tn(e):t}function Br(e){return Math.max(e,2)}function Vr(e,t,n,r=!0){let i=zr(e),o=!1;if(i){let i=t+n;for(let t of e.items)W(t,{lines:0,columns:i-t.loc.start.column}),u(t)&&(l(t.item)||d(t.item)?Vr(t.item,t.item.loc.start.column,Br(n),r)&&(t.loc.end={...t.item.loc.end},o=!0):a(t.item)&&(l(t.item.value)||d(t.item.value))&&Vr(t.item.value,t.item.loc.start.column,n,r)&&(t.item.loc.end={...t.item.value.loc.end},t.loc.end={...t.item.loc.end},o=!0));e.loc.end.column=t+1,o=!0}for(let t of e.items)if(u(t)){if(l(t.item)||d(t.item))Vr(t.item,t.item.loc.start.column,Br(n),r)&&(t.loc.end={...t.item.loc.end},o=!0);else if(a(t.item)&&(l(t.item.value)||d(t.item.value))){let e=t.item.value,i=t.item.loc.start.column,a=zr(e);Vr(e,i,n,r)&&(a&&(e.loc.end.column=i+1),t.item.loc.end={...e.loc.end},t.loc.end={...t.item.loc.end},o=!0)}}let s=fe(e)??2;for(let t=1;t0){let t=e.items[e.items.length-1];e.loc.end={line:t.loc.end.line,column:t.loc.end.column+ +!!t.comma+(r?2:1)}}return o}function Hr(e,t,n=!0){let r=a(e)?[e]:e.items;for(let e of r)a(e)&&(l(e.value)||d(e.value))&&Vr(e.value,e.loc.start.column,t,n)}function Ur(e,t){let n=(e,r)=>{if(r&&(delete e.range,gn(e)===!0||Tn(e))){let n=e.loc.start.column+t;for(let t of e.items)W(t,{lines:0,columns:n-t.loc.start.column});e.loc.end.column=e.loc.start.column+1}for(let t of e.items)u(t)&&(l(t.item)||d(t.item)?n(t.item,l(e)&&d(t.item)):a(t.item)&&(l(t.item.value)||d(t.item.value))&&n(t.item.value,!1))};n(e,!1)}function Wr(e){if(e.items.length>0){let t=e.items[e.items.length-1];e.loc.end.line=t.loc.end.line,e.loc.end.column=t.loc.end.column}else e.loc.end.line=e.key.loc.end.line,e.loc.end.column=e.key.loc.end.column}function Gr(e){return Math.max(0,e.length-1)}function Kr(e,t){if(t.inlineTableStart===void 0||t.inlineTableStart===0)return e;let n=[];for(let i of e.items)if(a(i)&&d(i.value)){if(Gr(i.key.value)0&&U(e)}function Jr(e){let t=0,n=0;for(let r of e.items)n===0&&r.loc.start.line>1?t=1-r.loc.start.line:r.loc.start.line+t>n+2&&(t+=n+2-(r.loc.start.line+t)),W(r,{lines:t,columns:0}),n=r.loc.end.line;return e}function Yr(e){return e.items.length>0&&e.loc.end.line-e.loc.start.line+1>e.items.length}function Xr(e){return l(e)||d(e)}function Zr(e,t,n){if(!u(t)||!Xr(t.item)||!Yr(e))return;let r=e.items.find(e=>Xr(e.item)&&e.item.type===t.item.type&&Yr(e.item));if(!r||!Xr(r.item))return;let i=t.item,a=r.item,o=a.items.length>0?a.items[0].loc.start.column-a.loc.start.column:n,s=a.items.length>0?a.items[0].loc.start.line-a.loc.start.line:1,c=a.items.length>0?a.loc.end.line-a.items[a.items.length-1].loc.end.line:1,l=i.loc.start.line,d=i.loc.start.column,f=l+s;for(let e of i.items)W(e,{lines:f-e.loc.start.line,columns:d+o-e.loc.start.column}),f=e.loc.end.line+1;i.loc.end={line:f-1+c,column:a.loc.end.column},_n(i),t.loc={start:w(i.loc.start),end:w(i.loc.end)}}function Qr(e,t){let n=e=>{let r=!1;for(let i of e.items){if(!u(i)||!Xr(i.item))continue;let a=i.item;if(d(a)&&Tn(a)){delete i.range,delete a.range;let n=a.loc.start.column+Math.max(t,2);for(let e of a.items)W(e,{lines:0,columns:n-e.loc.start.column});a.loc.end.column=a.loc.start.column+1,yn(a),_n(a),r=!0,xn(e),i.comma=!0,i.loc.end={line:a.loc.end.line,column:a.loc.end.column},e.loc.end={line:a.loc.end.line,column:a.loc.end.column+(i.comma?2:1)}}l(a)&&n(a)&&(delete a.range,delete i.range,i.loc.end={line:a.loc.end.line,column:a.loc.end.column},r=!0,xn(e))}if(r){for(let t=1;t1&&Kr(i,t),t.multilineTable!==`auto`||t.multilineArray!==`auto`)for(let e of i.items)(e.type===`Table`||e.type===`TableArray`||a(e)&&(l(e.value)||d(e.value)))&&Hr(e,t.indentWidth,t.bracketSpacing);return Jr(i)}function*$r(e,t,n=0,r=!1){for(let i of Object.keys(e)){let a=e[i];if(a===void 0)continue;let o=ri(a);o!==void 0&&(yield gr([i],ei(o,t,n,r),n>0&&!r))}}function ei(e,t,n,r){let i=t.minimumDecimals??0;if(e===null)throw Error(`"null" values are not supported`);if(e===void 0)throw Error(`"undefined" values are not supported inside arrays`);return ot(e)?br(e):ct(e)?Dr(e):st(e)?i>0?Or(e,i):Dr(e):lt(e)?Or(e,Math.max(i,1)):ut(e)?kr(e):pt(e)?jr(e,t.truncateZeroTimeInDates):dt(e)?Ar(e,t.truncateZeroTimeInDates):Array.isArray(e)?ti(e,t,n,r):ni(e,t,n,r)}function ti(e,t,n,r){let i=Mr(),a=!(e.length===0&&n>0)&&mn(`array`,n,r,t);hn(i,a),Mn(i,t.indentWidth),Pn(i);for(let r of e){let e=ei(r,t,n+1,a),o=K(e);!a&&(e.type===`InlineArray`||e.type===`InlineTable`)&&gn(e)===!0&&_n(e),Zr(i,o,t.indentWidth),V(i,i,o)}return er(i,i,t.bracketSpacing),tr(i,i,t.trailingComma),U(i),i}function ni(e,t,n,r){if(e=ri(e),!F(e))return ei(e,t,n,r);let i=Nr(),a=!(Object.keys(e).length===0&&n>0)&&mn(`table`,n,r,t);hn(i,a),Mn(i,t.indentWidth),Pn(i);for(let r of $r(e,t,n+1,a))V(i,i,K(r));return er(i,i,t.bracketSpacing),tr(i,i,t.trailingComma),U(i),i}function ri(e){return!e||dt(e)||pt(e)?e:typeof e.toJSON==`function`?e.toJSON():e}const ii=/(\r\n|\n)/g;function ai(e,t){let n=[],r=e=>{let t={line:e.loc.end.line,column:e.loc.end.column-1},n=e.items[e.items.length-1];return n&&(n.loc.end.line>t.line||n.loc.end.line===t.line&&n.loc.end.column>t.column)&&(t.line=n.loc.end.line,t.column=n.loc.end.column+ +!!n.comma),t};function i(e){switch(e.type){case`Document`:for(let t=0;t{if(e.type===`String`&&e.loc.end.line>e.loc.start.line)for(let n=e.loc.start.line+1;n<=e.loc.end.line;n++)t.add(n);if(p(e))for(let t of e.items)r(t);a(e)&&r(e.value),u(e)&&r(e.item)};if(ht(e))for(let t of e)r(t);else r(e);for(let e=0;e{if(e.type===`Comment`&&c.push(e),a(e))_(e.value);else if(u(e))_(e.item);else if(e.type===`Table`||e.type===`TableArray`)for(let t of e.items)_(t);else if(p(e))for(let t of e.items)_(t)};for(let e of i)_(e);let v=new WeakSet,y=e=>{for(let n of e)if(a(n)){let e=n.value;(d(e)||l(e))&&v.add(e)}else(t(n)||r(n))&&y(n.items)};y(i);let b=e=>(n.useTabsForIndentation?` `:` `).repeat(e),x=e=>{if(e.length===0)return;o.push(e);let t=e.split(/\r\n|\n/);t.length===1?g+=e.length:(h+=t.length-1,g=t[t.length-1].length)},S=(e,t)=>{e>h?(x(n.newLine.repeat(e-h)),t>0&&x(b(t))):e===h&&t>g?x(g===0?b(t):` `.repeat(t-g)):o.length>0&&(e0&&x(b(t)))},C=e=>{switch(e.type){case`Key`:return e.raw;case`String`:return e.raw;case`Integer`:return e.raw;case`Float`:return e.raw;case`Boolean`:return e.value.toString();case`DateTime`:return e.raw;case`Comment`:return e.raw;default:return}},w=e=>{if(a(e))return[e.key,e.value];if(u(e)||m(e))return[e.item];if(e.type===`Table`||e.type===`TableArray`){let t=e;return[t.key,...t.items]}return p(e)?e.items:[]},T=e=>{let t=[e];for(;t.length>0;){let e=t.pop();if(e.dirty)return!1;let n=C(e);if(n!==void 0){let t=z(e);if(!t||!e.range||t.slice(e.range[0],e.range[1])!==n)return!1}else if(!en(e))return!1;t.push(...w(e))}return!0},E=e=>e.type!==`Document`&&e.type!==`InlineItem`&&e.type!==`TableKey`&&e.type!==`TableArrayKey`&&(C(e)!==void 0||en(e)),D=(e,t,r=!1)=>{t.line>e.line?(x(n.newLine.repeat(t.line-e.line)),t.column>0&&x(r?` `.repeat(t.column):b(t.column))):t.line===e.line&&t.column>e.column&&x(` `.repeat(t.column-e.column))},O=(e,t,n)=>{var r,i;let a=z(e);if(!a||!e.range||l(e)&&Sn(e)&&(!n||t&&!t.range||n&&!n.range)||t&&z(t)!==a||n&&z(n)!==a)return;let o=(t==null||(r=t.range)==null?void 0:r[1])??e.range[0]+1,s=(n==null||(i=n.range)==null?void 0:i[0])??e.range[1]-1;if(o===void 0||s===void 0||s{let n=z(t);return n!==void 0&&z(e)===n&&t.range!==void 0&&e.range!==void 0&&e.range[0]>=t.range[0]&&e.range[1]<=t.range[1]},ee=e=>{let t=d(e)&&bn(e),r;for(let n of e.items){let i=r?{line:r.loc.end.line,column:r.loc.end.column+ +!!r.comma}:{line:e.loc.start.line,column:e.loc.start.column+1},a=c.filter(t=>!f.has(t)&&k(t,e)&&t.loc.start.line>=i.line&&t.loc.end.line<=n.loc.start.line&&(t.loc.start.line>i.line||t.loc.start.column>=i.column)&&(t.loc.end.line=e.loc.start.line&&t.loc.end.line<=e.loc.end.line).sort((e,t)=>e.loc.start.line-t.loc.start.line||e.loc.start.column-t.loc.start.column);if(a.length>0){for(let e of a)D(i,e.loc.start),x(e.raw),f.add(e),i=e.loc.end;D(i,n.loc.start)}else{let a=O(e,r,n);if(a!==void 0)x(a);else{let a=z(n)!==z(e);if(r&&a&&n.loc.start.line===r.loc.end.line){let t=fe(e)??2,n=Math.max(0,t-1);n>0&&x(` `.repeat(n))}else D(i,n.loc.start,t)}}A(n,!1);for(let t of c){if(f.has(t)||!k(t,e)||t.loc.start.line!==n.loc.end.line||t.loc.start.columne.loc.end.line)continue;let r=n.loc.end.column+ +!!n.comma;t.loc.start.column>r&&x(` `.repeat(t.loc.start.column-r)),x(t.raw),f.add(t)}r=n}let i=r?{line:r.loc.end.line,column:r.loc.end.column+ +!!r.comma}:{line:e.loc.start.line,column:e.loc.start.column+1},a=c.filter(t=>!f.has(t)&&k(t,e)&&t.loc.start.line>=i.line&&t.loc.end.line<=e.loc.end.line).sort((e,t)=>e.loc.start.line-t.loc.start.line||e.loc.start.column-t.loc.start.column);if(a.length>0){for(let e of a)D(i,e.loc.start),x(e.raw),f.add(e),i=e.loc.end;D(i,{line:e.loc.end.line,column:e.loc.end.column-1})}else{let a=O(e,r,void 0);if(a!==void 0)x(a);else if(r)t&&r.comma&&x(` `),D(i,{line:e.loc.end.line,column:e.loc.end.column-1},t);else{let t=z(e);if(t&&e.range){let r=t.slice(e.range[0],e.range[1]),i=Math.max(r.lastIndexOf(`
+`),r.lastIndexOf(`\r`)),a=v.has(e)||wn(e);if(i!==-1&&a&&cn(e)){var o;let e=((o=r.slice(i+1,-1).match(/^[\t ]*/))==null?void 0:o[0])??``;x(n.newLine+e)}}}}},A=(e,t=!0)=>{if(e.type===`Comment`&&f.has(e))return;let n=z(e);if(!(n!==void 0&&e.range&&s.some(t=>t.source===n&&t.start<=e.range[0]&&t.end>=e.range[1]))){if(E(e)&&T(e)&&e.range&&n!==void 0){let r=n.slice(e.range[0],e.range[1]),i=C(e);if(i===void 0||i===r){t&&S(e.loc.start.line,e.loc.start.column),x(r),s.push({source:n,start:e.range[0],end:e.range[1]});return}}switch(e.type){case`Document`:for(let t of e.items)A(t);break;case`Table`:{let n=e;A(n.key,t);for(let e of n.items)A(e);break}case`TableKey`:{let n=e;t&&S(n.loc.start.line,n.loc.start.column),x(`[${n.item.raw}]`);break}case`TableArray`:{let n=e;A(n.key,t);for(let e of n.items)A(e);break}case`TableArrayKey`:{let n=e;t&&S(n.loc.start.line,n.loc.start.column),x(`[[${n.item.raw}]]`);break}case`KeyValue`:{let n=e;t&&S(n.loc.start.line,n.loc.start.column),A(n.key,!1),D(n.key.loc.end,{line:n.loc.start.line,column:n.equals}),x(`=`),D({line:n.loc.start.line,column:n.equals+1},n.value.loc.start),A(n.value,!1);break}case`Key`:t&&S(e.loc.start.line,e.loc.start.column),x(e.raw);break;case`String`:t&&S(e.loc.start.line,e.loc.start.column),x(e.raw);break;case`Integer`:t&&S(e.loc.start.line,e.loc.start.column),x(e.raw);break;case`Float`:t&&S(e.loc.start.line,e.loc.start.column),x(e.raw);break;case`Boolean`:t&&S(e.loc.start.line,e.loc.start.column),x(e.value.toString());break;case`DateTime`:t&&S(e.loc.start.line,e.loc.start.column),x(e.raw);break;case`InlineArray`:{let n=e;t&&S(n.loc.start.line,n.loc.start.column),x(`[`),ee(n),x(`]`);break}case`InlineTable`:{let n=e;t&&S(n.loc.start.line,n.loc.start.column),x(`{`),ee(n),x(`}`);break}case`InlineItem`:{let t=e;if(A(t.item,!1),t.comma){let e=z(t);if(e&&t.range&&t.item.range&&z(t.item)===e){let n=e.slice(t.item.range[1],t.range[1]);/^[\t ]*$/.test(n)&&x(n)}x(`,`)}break}case`Comment`:t&&S(e.loc.start.line,e.loc.start.column),x(e.raw);break;default:throw Error(`toTOMLCursor: Unrecognized node type: ${String(e.type)}`)}}};for(let e of i)A(e);return o.join(``).replace(/\r\n|\n/g,n.newLine)+n.newLine.repeat(n.trailingNewline)}function si(e,t){let n=Array.isArray(e)?e:ht(e)?Array.from(e):[e],r=[],i=1,a=0,o=e=>(t.useTabsForIndentation?` `:` `).repeat(e),s=e=>{if(e.length===0)return;r.push(e);let t=0,n=-1,o=e.indexOf(`
+`);for(;o!==-1;)t++,n=o,o=e.indexOf(`
+`,o+1);if(t===0){a+=e.length;return}i+=t,a=e.length-n-1},c=(e,n)=>{e>i?(s(t.newLine.repeat(e-i)),n>0&&s(o(n))):e===i&&n>a?s(a===0?o(n):` `.repeat(n-a)):r.length>0&&(e0&&s(o(n)))},l=e=>{switch(e.type){case`Key`:return e.raw;case`String`:return e.raw;case`Integer`:return e.raw;case`Float`:return e.raw;case`Boolean`:return e.value.toString();case`DateTime`:return e.raw;case`Comment`:return e.raw;default:return``}},u=[],d=(e,t)=>{switch(e.type){case`Document`:{let t=e.items;for(let e=t.length-1;e>=0;e--)u.push([0,t[e],!0]);break}case`Table`:{let n=e,r=n.items;for(let e=r.length-1;e>=0;e--)u.push([0,r[e],!0]);u.push([0,n.key,t]);break}case`TableKey`:{let n=e;u.push([1,`[${n.item.raw}]`]),t&&u.push([3,n.loc.start.line,n.loc.start.column]);break}case`TableArray`:{let n=e,r=n.items;for(let e=r.length-1;e>=0;e--)u.push([0,r[e],!0]);u.push([0,n.key,t]);break}case`TableArrayKey`:{let n=e;u.push([1,`[[${n.item.raw}]]`]),t&&u.push([3,n.loc.start.line,n.loc.start.column]);break}case`KeyValue`:{let n=e;u.push([0,n.value,!1]),u.push([2,n.loc.start.line,n.equals+1,n.value.loc.start.line,n.value.loc.start.column,!1]),u.push([1,`=`]),u.push([2,n.key.loc.end.line,n.key.loc.end.column,n.loc.start.line,n.equals,!1]),u.push([0,n.key,!1]),t&&u.push([3,n.loc.start.line,n.loc.start.column]);break}case`Key`:case`String`:case`Integer`:case`Float`:case`Boolean`:case`DateTime`:case`Comment`:u.push([1,l(e)]),t&&u.push([3,e.loc.start.line,e.loc.start.column]);break;case`InlineArray`:{let n=e,r=n.items;if(u.push([1,`]`]),r.length>0){let e=r[r.length-1];u.push([2,e.loc.end.line,e.loc.end.column+ +!!e.comma,n.loc.end.line,n.loc.end.column-1,!1])}for(let e=r.length-1;e>=0;e--){let t=r[e],i=e>0?r[e-1]:void 0;u.push([0,t,!1]),u.push(i?[2,i.loc.end.line,i.loc.end.column+ +!!i.comma,t.loc.start.line,t.loc.start.column,!1]:[2,n.loc.start.line,n.loc.start.column+1,t.loc.start.line,t.loc.start.column,!1])}u.push([1,`[`]),t&&u.push([3,n.loc.start.line,n.loc.start.column]);break}case`InlineTable`:{let n=e,r=n.items,i=bn(n);if(u.push([1,`}`]),r.length>0){let e=r[r.length-1];i&&e.comma&&u.push([1,` `]),u.push([2,e.loc.end.line,e.loc.end.column+ +!!e.comma,n.loc.end.line,n.loc.end.column-1,i])}for(let e=r.length-1;e>=0;e--){let t=r[e],a=e>0?r[e-1]:void 0;u.push([0,t,!1]),u.push(a?[2,a.loc.end.line,a.loc.end.column+ +!!a.comma,t.loc.start.line,t.loc.start.column,i]:[2,n.loc.start.line,n.loc.start.column+1,t.loc.start.line,t.loc.start.column,i])}u.push([1,`{`]),t&&u.push([3,n.loc.start.line,n.loc.start.column]);break}case`InlineItem`:{let t=e;t.comma&&u.push([1,`,`]),u.push([0,t.item,!1]);break}default:throw Error(`toTOMLSequential: Unrecognized node type: ${String(e.type)}`)}};for(let e=n.length-1;e>=0;e--)u.push([0,n[e],!0]);for(;u.length>0;){let e=u.pop();switch(e[0]){case 0:d(e[1],e[2]);break;case 1:s(e[1]);break;case 2:{let n=e[1],r=e[2],i=e[3],a=e[4],c=e[5];i>n?(s(t.newLine.repeat(i-n)),a>0&&s(c?` `.repeat(a):o(a))):i===n&&a>r&&s(` `.repeat(a-r));break}case 3:c(e[1],e[2])}}return r.join(``).replace(/\r\n|\n/g,t.newLine)+t.newLine.repeat(t.trailingNewline)}function ci(e,t,n){if(t.start.line===t.end.line){let r=ui(e,t.start.line),i=r.substring(0,t.start.column),a=i.lengthe!==`
+`&&e!==`\r
+`),i=t.end.line-t.start.line+1;if(r.length!==i)throw Error(`Mismatch between location and raw string, expected ${i} lines for "${n}"`);for(let n=t.start.line;n<=t.end.line;n++){let i=ui(e,n),a=n===t.start.line,o=n===t.end.line,s=``;if(a){let e=i.substring(0,t.start.column);s=e.length=BigInt(-(2**53-1))&&i<=BigInt(2**53-1)?Number(i):i}function fi(e,t,n){for(let r of e.items){let e=r.item;if(e.value.type===`InlineTable`){let r=t.concat(e.key.value);n.add(xi(r)),fi(e.value,r,n)}}}function pi(e){var t,n,r,i,a,o;if((t=e.constructor)!=null&&(t=t.name)!=null&&(n=t.startsWith)!=null&&n.call(t,`Temporal.`)||((r=e.constructor)==null?void 0:r.name)===`PlainDate`||((i=e.constructor)==null?void 0:i.name)===`PlainTime`||((a=e.constructor)==null?void 0:a.name)===`PlainDateTime`||((o=e.constructor)==null?void 0:o.name)===`ZonedDateTime`)return e;let s=globalThis.Temporal;if(e instanceof ge)return s.PlainDate.from(e.toISOString());if(e instanceof _e)return s.PlainTime.from(e.toISOString());if(e instanceof ve)return s.PlainDateTime.from(e.toISOString().replace(` `,`T`));if(e instanceof ye){let t=e.toISOString().replace(` `,`T`),n=t.match(/([+-]\d{2}:\d{2}|Z)$/),r=n?n[1]:`Z`,i=t.replace(/([+-]\d{2}:\d{2}|Z)$/,``),a=r===`Z`?`+00:00`:r;return s.ZonedDateTime.from(`${i}${r}[${a}]`)}return s.PlainDateTime.from(e.toISOString().replace(`Z`,``))}function J(e,t=``,n={}){let{integersAsBigInt:r=`asNeeded`,temporal:i=!1}=n,a=at(),o=new Set,s=new Set,c=new Set,l=new Set,u=new Set,d=a,f=[],p={tables:o,table_arrays:s,defined:c,implicit_tables:l,inline_tables:u};for(let t of e)switch(t.type){case`Table`:m(t);for(let e of t.items)e.type===`KeyValue`&&g(e);break;case`TableArray`:h(t);for(let e of t.items)e.type===`KeyValue`&&g(e);break;case`KeyValue`:g(t)}return a;function m(e){let n=e.key.item.value;try{hi(a,[],n,e.type,p)}catch(n){let r=n;throw new D(t,e.key.loc.start,r.message)}let r=xi(n);o.add(r),c.add(r),d=gi(a,n),f=n}function h(e){let n=e.key.item.value;try{hi(a,[],n,e.type,p)}catch(n){let r=n;throw new D(t,e.key.loc.start,r.message)}let r=xi(n);s.add(r),c.add(r),d=_i(a,n),f=n}function g(e){let n=e.key.value;try{hi(d,f,n,e.type,p)}catch(n){let r=n;throw new D(t,e.key.loc.start,r.message)}if(n.length>1)for(let e=1;e1?gi(d,n.slice(0,-1)):d;o[P(n)]=a,c.add(xi(f.concat(n)))}}function mi(e,t={}){let{integersAsBigInt:n=`asNeeded`,temporal:r=!1}=t;switch(e.type){case`InlineTable`:let t=at(),i=new Set,a=new Map;return e.items.forEach(({item:e})=>{let o=e.key.value,s=mi(e.value,{integersAsBigInt:n,temporal:r}),c=bi(o),l=xi(o);if(i.has(l))throw Error(`Duplicate key "${c}" in inline table`);for(let e=1;e1?gi(t,o.slice(0,-1)):t;u[P(o)]=s}),t;case`InlineArray`:return e.items.map(e=>mi(e.item,{integersAsBigInt:n,temporal:r}));case`DateTime`:if(r){if(globalThis.Temporal===void 0)throw Error(`Temporal API is not available in this runtime. Set temporal: false or use a runtime with Temporal support.`);return pi(e.value)}return e.value;case`String`:return e.value;case`Integer`:return di(e.raw,n);case`Float`:case`Boolean`:return e.value;default:throw Error(`Unrecognized value type "${e.type}"`)}}function hi(e,t,n,r,i){let a=t.length>0?[...t]:[],o=Array(n.length),s=Array(n.length);for(let e=0;e1)for(let e=1;e1)for(let e=1;e1)for(let e=1;e<=n.length;e++){let t=o[e-1];if(i.tables.has(s[e-1]))throw Error(`Cannot extend explicit table ${t} with dotted keys`)}let u=0;for(let t of n){if(!bt(e,t))return;if(yi(e[t]))throw Error(`Value already defined for ${o[u]}`);let r=o[u];if(Array.isArray(e[t])&&!i.table_arrays.has(s[u]))throw Error(`Cannot add to static array at ${r}`);let a=u++(e[t]||(e[t]=at()),Array.isArray(e[t])?P(e[t]):e[t]),e)}function yi(e){return typeof e!=`object`&&!dt(e)}function bi(e){return e.join(`.`)}function xi(e){return JSON.stringify(e)}function Si(e){return e.type===`Add`}function Ci(e){return e.type===`Edit`}function Y(e){return e.type===`Remove`}function wi(e){return e.type===`Move`}function Ti(e){return e.type===`Rename`}function Ei(e,t,n=[],r={}){if(e===t||xt(e,t))return[];if(typeof e==`number`&&typeof t==`number`&&Number.isNaN(e)&&Number.isNaN(t)){let n=new Float64Array([e]),r=new Float64Array([t]),i=new DataView(n.buffer),a=new DataView(r.buffer);if((i.getUint32(4,!0)&2147483648)==(a.getUint32(4,!0)&2147483648))return[]}return Array.isArray(e)&&Array.isArray(t)?ji(e,t,n,r):F(e)&&F(t)?Di(e,t,n,r):[{type:`Edit`,path:n}]}function Di(e,t,n=[],r={}){let i=[],a=Object.keys(e),o=a.map(t=>St(e[t])),s=Object.keys(t),c=s.map(e=>St(t[e])),l=new Map(s.map((e,t)=>[e,c[t]])),u=new Set(a),d=new Set(s),f=(e,t,n)=>{let r=new Map;return e.forEach((e,i)=>{if(n.has(e))return;let a=r.get(t[i]);a?a.push(e):r.set(t[i],[e])}),r},p=f(a,o,d),m=f(s,c,u),h=new Map,g=new Set,_=new Map;for(let[e,t]of p){let n=m.get(e);if(!n)continue;let r=Math.min(t.length,n.length);for(let e=0;e{let c=n.concat(a);if(d.has(a)){let n=e[a],u=t[a],d=typeof n==`number`&&!Number.isFinite(n)||typeof u==`number`&&!Number.isFinite(u);(r.updateOrder||d||o[s]!==l.get(a))&&Ct(i,Ei(e[a],t[a],c,r))}else h.has(a)?i.push({type:`Rename`,path:n,from:a,to:h.get(a)}):i.push({type:`Remove`,path:c})}),s.forEach(e=>{if(!u.has(e)&&!g.has(e)){let t={type:`Add`,path:n.concat(e)};if(r.orderSource!==void 0){let i=r.orderSource;for(let e of n)i=i==null?void 0:i[e];if(F(i)){let n=Object.keys(i),r=n.indexOf(e);for(let e=r+1;et.has(e))}}let y=i.filter(e=>Si(e)&&e.path.length===n.length+1||Ti(e)&&e.path.length===n.length);if(r.orderSource!==void 0&&y.length>1){let e=[...y].sort((e,t)=>{let n=Si(e)?e.path[e.path.length-1]:Ti(e)?e.to:``,r=Si(t)?t.path[t.path.length-1]:Ti(t)?t.to:``;return v.indexOf(n)-v.indexOf(r)}),t=0;i=i.map(n=>y.includes(n)?e[t++]:n)}if(r.updateOrder){let e=[];for(let t of a)if(d.has(t))e.includes(t)||e.push(t);else if(h.has(t)){let n=h.get(t);e.includes(n)||e.push(n)}for(let t of s)e.includes(t)||e.push(t);s.forEach((t,r)=>{if(e[r]===t)return;let a=e.indexOf(t);i.push({type:`Move`,path:n,from:a,to:r,key:t}),e.splice(a,1),e.splice(r,0,t)})}return i}function Oi(e){if(typeof e==`string`)return e.includes(`
+`);if(Array.isArray(e))return e.length>0;if(typeof e==`object`&&e&&!(e instanceof Date)){let t=Object.values(e);return t.some(e=>typeof e==`object`&&!!e)||t.some(Oi)}return!1}function ki(e){return typeof e==`object`&&!!e&&!Array.isArray(e)&&!(e instanceof Date)}function Ai(e){return e.length>4&&e.some(e=>typeof e==`string`)&&e.some(ki)}function ji(e,t,n=[],r={}){let i=[],a=e.map(St),o=t.map(St),s=e.some(Oi)||t.some(Oi)||Ai(e)||Ai(t),c=e.slice(),l=0,u=!1,d=e=>{let t={type:`Remove`,path:n.concat(e)};return(!u||l>0)&&Object.defineProperty(t,"coordinate",{value:`source`}),t};for(let e=0;e=a.length;if(!p&&a[e]===f)continue;let m=p?-1:a.indexOf(f,e+1);if(m>-1){let r=a[e],p=0;for(let t=e;th;if(s&&(o.indexOf(a[e])===-1||g)){i.push(d(e+l)),a.splice(e,1),c.splice(e,1),l++,e--;continue}if(l>0&&g){i.push(d(e+l)),a.splice(e,1),c.splice(e,1),l++,e--;continue}if(s&&l>0){i.push({type:`Add`,path:n.concat(e)}),u=!0,a.splice(e,0,f),c.splice(e,0,t[e]),l--;continue}i.push({type:`Move`,path:n,from:m,to:e});let _=a.splice(m,1);a.splice(e,0,..._);let v=c.splice(m,1);c.splice(e,0,...v);continue}let h=!1;if(!p){let t=a[e],n=0;for(let r=e;rr}if(!p&&h){Ct(i,Ei(c[e],t[e],n.concat(e),r)),a[e]=f,c[e]=t[e];continue}i.push({type:`Add`,path:n.concat(e)}),u=!0,a.splice(e,0,f),c.splice(e,0,t[e]),l>0&&l--}for(let e=o.length;e{try{let f=[];if(a(l))f=l.key.value;else if(t(l))f=l.key.item.value;else if(r(l)){f=l.key.item.value;let e=St(f);o[e]||(o[e]=0);let t=o[e]++;f=f.concat(t)}else u(l)&&a(l.item)?f=l.item.key.value:u(l)&&(f=[d]);if(f.length&&I(f,i.slice(0,f.length))){let t=i.slice(f.length);if(e(n)&&r(l)&&t.length>0){let e=l.key.item.value,i=n,a=i.items.findIndex((t,n)=>n>d&&r(t)&&I(t.key.item.value,e)),o=a===-1?i.items.length:a;s=Mi(l,t,e,i.items.slice(d+1,o))}else s=u(l)&&a(l.item)?i.length===f.length?l:X(l.item.value,t):u(l)&&i.length>f.length?X(l.item,t):X(l,t);return!0}return(a(l)||u(l)&&a(l.item))&&f.length>i.length&&I(f.slice(0,i.length),i)&&(c||=l),!1}catch{return!1}}),!s&&c&&(s=c),!s)throw Error(`Node not found at ${i.join(`.`)}`);return s}function Mi(e,n,i,a){try{return X(e,n)}catch{}let o={};for(let e of a){let a=[];if(t(e)){let t=e.key.item.value;t.length>i.length&&I(t.slice(0,i.length),i)&&(a=t.slice(i.length))}else if(r(e)){let t=e.key.item.value;if(t.length>i.length&&I(t.slice(0,i.length),i)){let e=t.slice(i.length),n=St(e);o[n]||(o[n]=0);let r=o[n]++;a=e.concat(r)}}if(a.length&&I(a,n.slice(0,a.length)))try{return X(e,n.slice(a.length))}catch{}}throw Error(`Node not found at (aot scope) ${n.join(`.`)}`)}function Z(e,t){try{return X(e,t)}catch{}}function Ni(e,t){let n=t,r;for(;n.length&&!r;)n=n.slice(0,-1),r=Z(e,n);if(!r)throw Error(`Parent not found for ${t.join(`.`)}`);return r}const Pi=String.raw`(?:[\w-]+|"[^"]*"|'[^']*')`,Fi=new RegExp(String.raw`^#\s*${Pi}(?:\s*\.\s*${Pi})*\s*=`),Ii=String.raw`(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|"""[\s\S]*?"""|'''[\s\S]*?'''|[^\s#]+)`,Li=new RegExp(String.raw`^#\s*${Pi}(?:\s*\.\s*${Pi})*\s*=\s*${Ii}\s*$`),Ri=new RegExp(String.raw`^#\s*\[\[?\s*${Pi}(?:\s*\.\s*${Pi})*\s*\]\]?\s*$`);function zi(e){return Li.test(e.raw)||Ri.test(e.raw)}function Bi(e){let t=e.raw.indexOf(`=`);if(t<0)return!1;let n=!1,r=!1;for(let i=t+1;i=0&&(n=n.substring(0,r)),(n.startsWith(`"`)&&n.endsWith(`"`)||n.startsWith(`'`)&&n.endsWith(`'`))&&(n=n.slice(1,-1)),n||void 0}function Ui(e){return(d(e)||l(e))&&e.loc.end.line>e.loc.start.line}function Wi(e){if(a(e))return e.key.value[0];if(t(e)||r(e))return e.key.item.value[0];if(u(e)&&a(e.item))return e.item.key.value[0]}function Gi(e,t,n,r=Wi){let i=[],a=t,o,s=[],c=()=>{s.length&&(i.push({kind:`pinned`,items:s,startLine:s[0].loc.start.line,endLine:P(s).loc.end.line}),s=[])};for(let t of e){if(f(t)){if(t.loc.start.line<=a){o?(o.items.push(t),o.endLine=Math.max(o.endLine,t.loc.end.line)):i.push({kind:`pinned`,items:[t],startLine:t.loc.start.line,endLine:t.loc.end.line});continue}let e=s.length?P(s).loc.end.line:void 0;s.length&&t.loc.start.line===e+1||c(),s.push(t);continue}let e=[];if(s.length){if(P(s).loc.end.line+1===t.loc.start.line&&n(t)){let n=r(t);if(n!==void 0){let e=-1;for(let t=s.length-1;t>=0;t--){let r=Vi(s[t])?Hi(s[t]):void 0;if(r!==void 0&&r!==n){e=t;break}}if(e>=0){let t=s.splice(0,e+1);i.push({kind:`pinned`,items:t,startLine:t[0].loc.start.line,endLine:P(t).loc.end.line})}}e=s,s=[]}else c()}let l=[...e,t],u={kind:`member`,member:t,key:r(t),items:l,startLine:l[0].loc.start.line,endLine:t.loc.end.line};i.push(u),o=u,a=t.loc.end.line}return c(),i}function Ki(t,n=()=>!0,r=Wi){let i=e(t)?0:t.key.loc.end.line;return Gi(t.items,i,n,r)}function qi(e,t){let n=t.filter(t=>f(t)&&(t.loc.start.line>e.loc.start.line||t.loc.start.line===e.loc.start.line&&t.loc.start.column>=e.loc.start.column)&&(t.loc.end.linee.loc.start.line-t.loc.start.line||e.loc.start.column-t.loc.start.column),e.loc.start.line,()=>!0)}function Ji(e,t){function n(e){if(a(e))return n(e.value);if(p(e)){for(let n of e.items)if(u(n)&&n.item===t)return n;for(let t of e.items){let e=n(u(t)?t.item:t);if(e)return e}}}return n(e)}function Yi(e,n){function i(e,t){if(e===n)return t;if(a(e))return i(e.value,t);if(d(e)||l(e))for(let r of e.items){if(r===n)return t;let e=i(r.item,t);if(e)return e}}function o(e){for(let s of e.items){if(s===n)return e;if(a(s)){let t=i(s.value,e);if(t)return t}else if(t(s)||r(s)){let e=o(s);if(e)return e}}}return o(e)}function Xi(e){for(let n=0;n0?i.items[e-1]:void 0;if(o&&(t(o)||r(o))){let e=Zi(o,a);if(e)for(let t of e)H(n,o,t)}}if(e(i)||t(i)||r(i)){let e=Ki(i).find(e=>e.member===a);if(e){let t=e.items.indexOf(a);for(let r=0;r<=t;r++){let t=e.items[r];i.items.includes(t)&&H(n,i,t)}for(let n=t+1;ne.member===a);if(t){let r=t.items.filter(e=>e!==a);for(let t of r){let n=e.items.indexOf(t);n<0||e.items.splice(n,1)}r.length&&(a.loc.start=t.items[0].loc.start,a.loc.end=P(t.items).loc.end),H(n,i,a,e.items),U(n);return}}}H(n,i,a)}function Qi(t,n,r,i){let s=!1;if(Ui(n)&&e(t)){let e=Yi(t,n);if(e){let c=qi(n,e.items);{let e=n.items,r=1;for(let n=1;n=3){s=!0;break}}else r=1}let f=[],m,h,g,_=[],v=(e,t)=>{if(!p(e))return;let n=e.items;for(let e=0;et;if(i&&_.push({node:r,start:w(r.loc.start),end:w(r.loc.end)}),r.loc.end.line>t&&r.loc.end.line>r.loc.start.line&&_.push({node:r,start:w(r.loc.start),end:w(r.loc.end),endOnly:!0}),u(r)){let e=r.item;if(i){let t=a(e)?e:void 0;_.push({node:e,start:w(e.loc.start),end:w(e.loc.end),equals:t?t.equals:void 0}),t&&(_.push({node:t.key,start:w(t.key.loc.start),end:w(t.key.loc.end)}),_.push({node:t.value,start:w(t.value.loc.start),end:w(t.value.loc.end)}))}e.loc.end.line>t&&e.loc.end.line>e.loc.start.line&&_.push({node:e,start:w(e.loc.start),end:w(e.loc.end),endOnly:!0}),v(e,t)}else p(r)&&v(r,t)}};for(let t of c){if(t.kind!==`member`||!t.member)continue;let n=t.items.filter(e=>e!==t.member);if(t.member===r&&(m=w(r.loc.start),h=w(r.loc.end),u(r)&&(g=w(r.item.loc.end)),v(u(r)?r.item:r,m.line),n.length&&(r.loc.start=w(t.items[0].loc.start),r.loc.end=w(P(t.items).loc.end))),n.length){f.push({owner:t.member,ownerOriginalStart:w(t.member.loc.start),comments:n});for(let t of n){let n=e.items.indexOf(t);n>=0&&e.items.splice(n,1)}}}let y=n.items.indexOf(r),b=y===n.items.length-1,x=y>0?n.items[y-1]:void 0,S=i===0&&y>0&&r.loc.end.line>r.loc.start.line&&x!==void 0&&x.loc.start.line===r.loc.start.line;H(t,n,r,e.items);let C=!1;if(b&&x!==void 0&&r.loc.end.line>r.loc.start.line&&!nr(n)&&B(t).get(x)&&(B(t).delete(x),C=!0),U(t),V(t,n,r,i,void 0,e.items),C){let e=B(t).get(r);e&&(e.lines=0,e.columns=0)}U(t),m&&h&&r.loc.end.line>r.loc.start.line&&(r.loc.end.column=h.column,u(r)&&g&&r.item.loc.end.line>r.item.loc.start.line&&(r.item.loc.end.column=g.column));for(let{node:e,start:t,end:n,endOnly:r,equals:i}of _){if(r){e.loc.end.column=n.column;continue}e.loc.start.column=t.column,e.loc.end.line===e.loc.start.line&&(e.loc.end.column=n.column),i!==void 0&&a(e)&&(e.equals=i)}for(let{owner:t,ownerOriginalStart:n,comments:i}of f){let a;t===r?(a={lines:r.loc.start.line-n.line,columns:r.loc.start.column-n.column},r.loc.start={line:m.line+a.lines,column:m.column+a.columns},r.loc.end={line:h.line+a.lines,column:h.column+a.columns}):a={lines:t.loc.start.line-n.line,columns:t.loc.start.column-n.column};for(let t of i){W(t,a);let n=e.items.findIndex(e=>e.loc.start.line>t.loc.start.line);n===-1?e.items.push(t):e.items.splice(n,0,t)}}let T=n;if(s||S){let e=(t,n)=>{let r;for(let n of t.items){if(!r){r={line:n.loc.end.line,column:n.loc.end.column+2};continue}let t={lines:r.line-n.loc.start.line,columns:r.column-n.loc.start.column};if(t.lines!==0||t.columns!==0){W(n,t),n.loc.end.line>n.loc.start.line&&(n.loc.end.column-=t.columns,u(n)&&n.item.loc.end.line>n.item.loc.start.line&&(n.item.loc.end.column-=t.columns));let r=u(n)?a(n.item)?n.item.value:n.item:n;(d(r)||l(r))&&r.loc.end.line>r.loc.start.line&&e(r,n)}r={line:n.loc.end.line,column:n.loc.end.column+2}}r&&(t.loc.end={line:r.line,column:r.column-1},n&&n.loc.end.line===t.loc.end.line&&(n.loc.end.column=t.loc.end.column))},n;for(let t of T.items){if(t===r){n={line:r.loc.end.line,column:r.loc.end.column+2};continue}if(!n)continue;let i={lines:n.line-t.loc.start.line,columns:n.column-t.loc.start.column};if(i.lines!==0||i.columns!==0){W(t,i);let n=u(t)?a(t.item)?t.item.value:t.item:t;(d(n)||l(n))&&n.loc.end.line>n.loc.start.line&&e(n,u(t)?t:void 0)}let s=t.loc.end.line,c=u(t)?t.item:t;o(c)&&c.loc.end.line>c.loc.start.line&&(s=c.loc.start.line+c.raw.split(`
+`).length-1,c.loc.end.line=s,t.loc.end.line=s),n={line:s,column:t.loc.end.column+2}}if(n){let e=w(T.loc.end);T.loc.end={line:n.line,column:n.column-1};let r=Ji(t,T);r&&(r.loc.end={line:T.loc.end.line,column:T.loc.end.column});let i={lines:T.loc.end.line-e.line,columns:T.loc.end.column-e.column};(i.lines!==0||i.columns!==0)&&Bn(t,r??T,i)}}return}}let c=n,f=c.items,m=f[0],h=f[f.length-1],g=m.loc.start.column-c.loc.start.column-1,_=c.loc.end.column-1-h.loc.end.column;H(t,n,r),V(t,n,r,i),U(t);let v=c.items,y=v[0],b=y.loc.start.column-c.loc.start.column-1;b!==g&&W(y,{lines:0,columns:g-b});let x=v[v.length-1];c.loc.end.column-1-x.loc.end.column!==_&&(c.loc.end.column=x.loc.end.column+1+_)}function $i(e){let n=[];for(let i of e){if(i.kind===`pinned`){n.push({kind:`pinned`,isSection:!1,movable:!1,groups:[i],startLine:i.startLine,endLine:i.endLine,items:i.items});continue}let e=t(i.member)||r(i.member),a=n[n.length-1];if(a&&a.kind===`member`&&i.key!==void 0&&a.key===i.key){a.groups.push(i),a.endLine=i.endLine,a.items=a.items.concat(i.items);continue}n.push({kind:`member`,key:i.key,isSection:e,movable:i.key!==void 0,groups:[i],startLine:i.startLine,endLine:i.endLine,items:[...i.items]})}let i=new Map;for(let e of n)e.kind===`member`&&e.key!==void 0&&i.set(e.key,(i.get(e.key)??0)+1);for(let e of n)e.kind===`member`&&e.key!==void 0&&i.get(e.key)>1&&(e.movable=!1);return n}function ea(e,t){let n=[...e];for(let e of t){if(e.key===void 0)continue;let t=n.indexOf(e.key);if(t===-1)continue;let r=Math.max(0,Math.min(e.to,n.length-1));t!==r&&(n.splice(t,1),n.splice(r,0,e.key))}return n}function ta(e){return[...e.path,e.key].join(`.`)}function na(e,n,i,o){if(n.some(e=>typeof e!=`string`))return;let s=[...n,i],c=[{container:e,prefix:[]}];for(let n of e.items)(t(n)||r(n))&&c.push({container:n,prefix:n.key.item.value});for(let e of c)for(let t of e.container.items){if(!a(t)||t.key.value.length<2||o.has(t))continue;let n=[...e.prefix,...t.key.value];if(n.length===s.length&&n.every((e,t)=>e===s[t]))return{container:e.container,dottedPrefix:t.key.value.slice(0,-1)}}}function ra(n,i,o,s){if(i.length===0)return{container:n};let c;try{c=Z(n,i)}catch{return}if(!c)return;if(a(c)&&(c=c.value),c&&m(c)&&(c=c.item),c&&(e(c)||t(c)||r(c)))return{container:c};if(o===void 0)return;let l=na(n,i,o,s);if(l)return l;let u=-1;for(let e=i.length-1;e>=0;e--)if(typeof i[e]==`number`){u=e;break}if(u<0)return;let d=i.slice(u+1);if(d.length===0||d.some(e=>typeof e!=`string`))return;let f=Z(n,i.slice(0,u+1));if(!f||!r(f))return;let p=[...d,o];return f.items.some(e=>a(e)&&e.key.value.length===p.length&&e.key.value.every((e,t)=>e===p[t]))?{container:f,dottedPrefix:d}:void 0}function ia(e,t,n){if(t.key===void 0)return!1;let r;try{r=Z(e,t.path.concat(t.key))}catch{return!1}return r!==void 0&&u(r)&&!n.has(r)}function aa(t,n,r,i,o){let s=$i(Ki(t,e=>r.has(e),o?e=>{if(!a(e))return;let t=e.key.value;if(t.length===o.length+1){for(let e=0;ee.kind===`member`&&e.key!==void 0).map(e=>e.key));for(let e of n)e.key!==void 0&&l.has(e.key)&&!c.has(e.key)&&i.push(`"${ta(e)}" -- its entries are not contiguous in the document (e.g. the TOML spec's own "valid but discouraged" out-of-order table pattern), so reordering was skipped for it`);if(c.size===0)return;let u=o?s.filter(e=>e.kind===`member`&&e.movable).map(e=>e.key):s.filter(e=>e.kind===`member`).map((e,t)=>e.movable?e.key:`\u0000anchor:${t}`),d=n.filter(e=>e.key!==void 0&&c.has(e.key));if(d.length===0)return;let f=ea(u,d).filter(e=>c.has(e)),p=f.filter(e=>!c.get(e).isSection),m=f.filter(e=>c.get(e).isSection),h=!1;for(let e of f)if(c.get(e).isSection)h=!0;else if(h){i.push(`the requested order asked for a root key-value to be positioned after a section header, which TOML cannot represent -- each group (root key-values, section blocks) was reordered to match its own requested relative order instead`);break}let g=0,_=0,v=s.map(e=>{if(e.kind!==`member`||!e.movable)return e;let t=e.isSection?m[_++]:p[g++];return c.get(t)});if(v.every((e,t)=>e===s[t]))return;let y=e(t)?1:t.key.loc.end.line,b=s.map((e,t)=>t===0?e.startLine-y:e.startLine-s[t-1].endLine-1),x=y;for(let e=0;ee.items),C=t.items;C.splice(0,C.length,...S),$n(t)}function oa(e,t,n){if(t.length===0)return;Xi(e);let r=[],i=new Map;for(let o of t){var a;if(o.key===void 0)continue;let t=ra(e,o.path,o.key,n);if(!t){if(ia(e,o,n))continue;r.push(`"${ta(o)}" -- unsupported location for reordering (e.g. a dotted-key implicit table, or an interior of an inline table/array-of-tables entry)`);continue}let s=((a=t.dottedPrefix)==null?void 0:a.join(`\0`))??``,c=i.get(t.container);c||(c=new Map,i.set(t.container,c));let l=c.get(s);l?l.moves.push(o):c.set(s,{...t,moves:[o]})}for(let e of i.values())for(let t of e.values())aa(t.container,t.moves,n,r,t.dottedPrefix);if(r.length>0){let e=[...new Set(r)];console.warn(`toml-patch: updateOrder could not honor the requested position for ${e.length===1?`1 entry`:`${e.length} entries`}, left unchanged:\n`+e.map(e=>` - ${e}`).join(`
+`))}$n(e)}const sa=new WeakMap,ca=new WeakMap;function la(e,t){let n=e[t].loc.end.line;for(let r=t+1;rn)return}}function ua(e,t,n){for(let r=t+n;r>=0&&r0,a=n{(e>n||e===n&&t>i)&&(n=e,i=t)};(t(e)||r(e))&&a(e.key.loc.end.line,e.key.loc.end.column);for(let t of e.items)a(t.loc.end.line,t.loc.end.column);e.loc.end.line=n,e.loc.end.column=i}function ba(e,t,n){let r=n.newLine,i=t.match(/(?:\r\n|\n)+$/u),a=i?i[0]:``,o=a?t.slice(0,-a.length):t,s=o.length?o.split(r):[],c=va(e),l=(e,t)=>{let n=e.lastIndexOf(t.raw);return n>=0?n:t.loc.start.column},u=_a(e).filter(e=>{let t=s[e.loc.start.line-1]||``,n=l(t,e),r=t.slice(0,n);return r.trim().length>0&&/\s{2,}$/u.test(r)}).sort((e,t)=>e.loc.start.line-t.loc.start.line),d=[],f=[];for(let e of u){let t=f[f.length-1];!t||e.loc.start.line===t.loc.start.line+1?f.push(e):(f.length>0&&d.push(f),f=[e])}f.length>0&&d.push(f);for(let e of d){if(e.length<2)continue;let t=ca.get(e[0])??e[0].loc.start.column,n=new Map;for(let t of e){let e=ca.get(t)??t.loc.start.column;n.set(e,(n.get(e)||0)+1)}let r=1;for(let[e,i]of n)i>r&&(r=i,t=e);let i=Math.max(...e.map(e=>{let t=s[e.loc.start.line-1]||``,n=l(t,e);return t.slice(0,n).trimEnd().length})),a=Math.min(...e.map(e=>{let t=s[e.loc.start.line-1]||``,n=l(t,e),r=t.slice(0,n).trimEnd();return Math.max(1,n-r.length)})),o=e.filter(e=>sa.has(e)),u=e.some(e=>!c.has(e.loc.start.line));if(o.length>0&&u){let t=Math.min(...e.map(e=>{if(sa.has(e))return sa.get(e)||1;let t=s[e.loc.start.line-1]||``,n=l(t,e),r=t.slice(0,n).trimEnd();return Math.max(1,n-r.length)}));a=Math.min(a,t)}if(to.leadingBom?`${e}`:e)(Da(a,t,o,n instanceof R||n!=null&&Object.prototype.hasOwnProperty.call(n,`indentWidth`),n instanceof R||n!=null&&Object.prototype.hasOwnProperty.call(n,`trailingComma`)).tomlString)}function Sa(e,t,n){let r=e.slice(n),i=0,a=Math.min(r.length,t.length);for(let e=1;e<=a;e++)I(r.slice(r.length-e),t.slice(0,e))&&(i=e);return i}function Ca(e,t,n){if(n===void 0)return;let r=t.key.value;if(r.length<2)return;let i=r.slice(0,-1);for(let t=0;t{t.add(e)};return Dt(e,{Document:n,Table:n,TableKey:n,TableArray:n,TableArrayKey:n,KeyValue:n,Key:n,String:n,Integer:n,Float:n,Boolean:n,DateTime:n,InlineArray:n,InlineItem:n,InlineTable:n,Comment:n}),t}function Ta(e,t,n,r){if(!F(t))return t;let i=J(q(e,L({...n,inlineTableStart:0},n)).items,``,{temporal:r});if(!F(i))return t;let a={};for(let e of Object.keys(i))Object.prototype.hasOwnProperty.call(t,e)&&(a[e]=t[e]);for(let e of Object.keys(t))Object.prototype.hasOwnProperty.call(a,e)||(a[e]=t[e]);return a}function Ea(e){let n=e.items;for(let e=0;e0&&f(a[o-1]);)o--;if(os||t.line===s&&t.column>c)&&(s=t.line,c=t.column)}let l=J(a,``,{temporal:o}),u={type:`Document`,loc:{start:{line:1,column:0},end:{line:s,column:c}},items:a};Mn(u,n.indentWidth);let d=n.inlineTableStart!=null&&n.inlineTableStart>1?1:n.inlineTableStart,f=L({...n,inlineTableStart:d},n),p=q(t,f),m=J(p.items,``,{temporal:o}),h=n.updateOrder?Ta(t,m,f,o):m;if(!n.updateOrder&&Oa(l,h))return{tomlString:ai(a,n),document:u};let g=ja(Ma(u,h,Ei(l,h,[],{updateOrder:n.updateOrder,orderSource:t})));if(g.length===0)return{tomlString:ai(a,n),document:u};let _=za(u,p,g,n,o,wa(u),t,r,i);return{tomlString:ba(_,oi(_.items,n),n),document:_}}function Oa(e,t){let n=[[e,t]];for(;n.length>0;){let[e,t]=n.pop();if(typeof e==`number`&&typeof t==`number`&&(!Number.isFinite(e)||!Number.isFinite(t))||typeof e==`number`&&typeof t==`number`&&Number.isNaN(e)&&Number.isNaN(t))return!1;if(Object.is(e,t))continue;if(e instanceof Date&&t instanceof Date){if(e.getTime()!==t.getTime())return!1;continue}if(!e||!t||typeof e!=`object`||typeof t!=`object`||Array.isArray(e)!==Array.isArray(t))return!1;if(!Array.isArray(e)){let n=Object.getPrototypeOf(e),r=Object.getPrototypeOf(t);if(n!==Object.prototype&&n!==null||r!==Object.prototype&&r!==null)return!1}let r=Object.keys(e),i=Object.keys(t);if(r.length!==i.length)return!1;for(let i of r){if(!Object.prototype.hasOwnProperty.call(t,i))return!1;n.push([e[i],t[i]])}}return!0}function ka(e,t=new WeakSet){return typeof e!=`object`||!e?!1:pt(e)?!0:!t.has(e)&&(t.add(e),Object.values(e).some(e=>ka(e,t)))}function Aa(e){if(Array.isArray(e)){let t=!1,n=[];for(let r=0;rr){e.splice(a,1),e.splice(t,0,o),t=-1;break}a++}}}return e}function Ma(e,t,n){let i=new Set,o=[],s=new Map;for(let e of n){if(!Y(e)&&!Si(e))continue;let t=e.path.slice(0,-1);if(t.length===0)continue;let n=JSON.stringify(t),r=s.get(n);r||(r={path:t,removes:[],adds:[]},s.set(n,r)),(Y(e)?r.removes:r.adds).push(e)}for(let t of s.values())t.removes.length!==0&&t.adds.length!==0&&(Z(e,t.path)||(t.removes.forEach(e=>i.add(e)),t.adds.forEach(e=>i.add(e)),o.push({type:`Edit`,path:t.path})));for(let t of n){if(i.has(t)||!Y(t)&&!Si(t))continue;let r=t.path.slice(0,-1);if(r.length===0||typeof P(r)!=`number`)continue;let s=Z(e,r);if(!s||!u(s)||!d(s.item))continue;let c=n.filter(e=>!i.has(e)&&(Y(e)||Si(e))&&I(e.path.slice(0,-1),r));if(c.length===0||c.some(e=>e.path.length!==r.length+1))continue;let l=new Set(s.item.items.filter(u).map(e=>{let t=a(e.item)?e.item:void 0;return(t==null?void 0:t.key.value.length)===1?t.key.value[0]:void 0}).filter(e=>e!==void 0)),f=new Set(c.filter(Y).map(e=>P(e.path)));if(c.some(Y)&&c.some(Si)&&l.size===f.size&&![...l].some(e=>!f.has(e))){for(let e of c)i.add(e);o.push({type:`Edit`,path:r})}}let c=new Map;for(let t of n){if(i.has(t)||!Ci(t)&&!Si(t)&&!Y(t)||typeof P(t.path)!=`number`)continue;let n=t.path.slice(0,-1);if(n.length===0)continue;let a=Z(e,n.concat(0));a&&r(a)&&c.set(JSON.stringify(n),n)}for(let e of c.values()){let r=t;for(let t of e)r=r==null?void 0:r[t];if(!Array.isArray(r)||r.every(e=>F(e)))continue;let a=n.filter(t=>!i.has(t)&&(Ci(t)||Si(t)||Y(t))&&t.path.length>e.length&&I(t.path.slice(0,e.length),e));if(!(a.length<2)){for(let e of a)i.add(e);o.push({type:`Edit`,path:e})}}if(i.size===0)return n;let l=n.filter(e=>!i.has(e));return l.push(...o),l}function Na(e,t){return t.map(t=>k.test(t)?t:`"${dr(t,e,`singleline-basic`)}"`).join(`.`)}function Pa(e){let t=[],n,r=!1;for(let i=0;i0&&(e[o-1]===` `||e[o-1]===` `);)o--;let s=i+1;for(;sr===0?t:`${e}${n[Math.min(r-1,n.length-1)]}${t}`,``),e.loc.end.column=e.loc.start.column+e.raw.length)}function La(e,t){let n;return Dt(e,{KeyValue:e=>{!n&&e.key.value.length>t.length&&I(e.key.value.slice(0,t.length),t)&&Pa(e.key.raw).length>0&&(n=e)}}),n}function Ra(e,t){if(o(e)&&o(t)){let n=br(t.value,e.raw);t.raw=n.raw,t.loc=n.loc}if(c(e)&&c(t)){let n=e.raw,r=t.value;if(pt(r)){let e=mt(r);if(n.includes(` `)&&e.includes(`T`)&&(e=e.replace(`T`,` `)),/(?:\+00:00|-00:00)/.test(n)&&e.endsWith(`Z`)){let t=n.match(/([+-]00:00)/)[1];e=e.replace(/Z$/,t)}t.raw=e,t.loc.end.column=t.loc.start.column+t.raw.length}else if(!(he.IS_TIME_ONLY.test(n)&&r instanceof Date&&r.getUTCFullYear()>1)){let e=he.createDateWithOriginalFormat(r,n);t.value=e,t.raw=e.toISOString(),t.loc.end.column=t.loc.start.column+t.raw.length}}if(s(e)&&s(t)&&Number.isNaN(e.value)&&Number.isNaN(t.value)){let n=e,r=t;n.nanSign&&(r.nanSign=`+`,r.raw=`+nan`)}if(l(e)&&l(t)){let n=Pr(e);if(t.items.length>0){let e=t.items[t.items.length-1];e.comma!==n&&(e.comma=n,n&&(t.loc.end.column+=1))}}if(d(e)&&d(t)){let n=Fr(e);if(t.items.length>0){let e=t.items[t.items.length-1];e.comma!==n&&(e.comma=n,n&&(t.loc.end.column+=1))}}}function za(n,i,s,c,h=!1,g=new WeakSet,_=void 0,v=!1,y=!0){let x=new Map,S=new Set;function C(e){let t=n.items,r=t.indexOf(e);if(r>0){let n=t[r-1];f(n)&&n.loc.end.line+1===e.loc.start.line&&S.add(e)}}let w=new WeakMap;function T(e){let i=w.get(e);if(i!==void 0)return i;let o={};function s(n,i){if(n===e)return i;if(a(n))return s(n.value,i);if(u(n))return s(n.item,i);if(p(n))for(let e=0;et.length||!I(n.key.value,t.slice(t.length-r))}function D(e,i){let o=e.key.item.value.concat(i);return e.items.some(e=>a(e)&&e.key.value.length>i.length&&I(e.key.value.slice(0,i.length),i))?!0:n.items.some(e=>{if(!t(e)&&!r(e))return!1;let n=e.key.item.value;return n.length>o.length&&I(n.slice(0,o.length),o)})}function O(e,t){let n=[],r=!1;for(let i of e.items){if(!a(i)||!d(i.value)||!D(t,i.key.value)){n.push(i);continue}let e=[];for(let t of i.value.items){if(!u(t)||!a(t.item))continue;let n=J([t.item],``,{temporal:h});for(let e of t.item.key.value)n=n==null?void 0:n[e];let r=eo(n,c);r!==void 0&&e.push(gr(i.key.value.concat(t.item.key.value),r))}e.length>0?(n.push(...e),r=!0):n.push(i)}if(!r)return e;let i=mr(e.key.item.value);for(let e of n)V(i,i,e);return U(i),i}function ee(e,t){function n(e){if(a(e))return n(e.value);if(u(e))return n(e.item);if(!p(e))return;let r=e.items;for(let n of r)if(n===t||u(n)&&n.item===t)return e;for(let e of r){let t=n(u(e)?e.item:e);if(t)return t}}return n(e)}function A(e,t,r,i){let a=t.concat(e.key.value),o=e.key.raw;e.key.value=a,e.key.raw=a.map(e=>k.test(e)?e:JSON.stringify(e).replace(/\x7f/g,`\\u007f`)).join(`.`);let s=e.key.raw.length-o.length;e.key.loc.end.column=e.key.loc.start.column+e.key.raw.length,e.equals+=s,W(e.value,{lines:0,columns:s},{first_line_only:!0}),e.loc.end.line===e.loc.start.line&&(e.loc.end.column+=s),Kn(n,i,r,e),Ua(n,e)}function te(e,t){let n=e.key.item,r=n.raw,i=t.concat(n.value),a=i.map(e=>k.test(e)?e:JSON.stringify(e).replace(/\x7f/g,`\\u007f`)).join(`.`),o=a.length-r.length;n.value=i,n.raw=a,n.loc.end.column=n.loc.start.column+a.length,e.key.loc.end.line===e.key.loc.start.line&&(e.key.loc.end.column+=o)}function ne(e,t,r,i){te(e,t),Kn(n,i,r,e)}function j(e,t,r){let i=Z(n,e);if(i&&!E(i,e))return null;let a=e.length;for(;a>0;){let t=Z(n,e.slice(0,a));if(t&&!E(t,e.slice(0,a)))break;a--}let o=e.slice(a);if(o.length===0||!o.every(e=>typeof e==`string`))return null;if(_!==void 0){let e=_;for(let t of r)e=e==null?void 0:e[t];if(e!==void 0){let n=eo(e,c);if(n!==void 0)return gr([...o,...t.key.value],n)}}let s=t.key,l=[...o,...s.value].map(e=>k.test(e)?e:JSON.stringify(e).replace(/\x7f/g,`\\u007f`)).join(`.`),u=l.length-s.raw.length;return s.raw=l,s.value=[...o,...s.value],s.loc.end.column=s.loc.start.column+l.length,t.equals+=u,W(t.value,{lines:0,columns:u},{first_line_only:!0}),t.loc.end.line===t.loc.start.line&&(t.loc.end.column+=u),t}let M=new Set,re=new Set,ie=new WeakMap;function ae(e,t){return ie.get(e)??t}let oe=[],se=new Set,ce=[];function le(e,t,r){if(!l(e)&&!d(e)||!u(r)||d(e)&&!a(r.item))return r;let i=_;for(let e of t)i=i==null?void 0:i[e];if(!Array.isArray(i)&&!F(i))return r;let o=En(n,e)??0,s=nr(e)||e.items.length===0&&e.loc.end.line>e.loc.start.line,f=v?c.indentWidth:l(e)&&e.items.length===0?Math.max(c.indentWidth,4):c.indentWidth;l(e)&&e.items.length===0&&!c.useTabsForIndentation&&Nn(e,Math.max(f,4));let p=Array.isArray(i)?`array`:`table`,m=e.items.map(t=>d(e)&&a(t.item)?t.item.value:t.item).find(e=>p===`array`?l(e):d(e)),h=m&&p===`array`?Pr(m):m&&p===`table`?Fr(m):!1,g=m,b=g&&g.items.length>0?g.items[0].loc.start.column-g.loc.start.column>1:void 0,x=mn(p,o+1,s,c),S=p===`array`?`multilineArray`:`multilineTable`,C=L({...c,inlineTableStart:0,[S]:x,trailingComma:y?c.trailingComma:h,bracketSpacing:b??c.bracketSpacing,indentWidth:f},c),w=q({tmp:i},C,o+1,s).items[0];if(!w||!a(w))return r;let T=w.value;if(x&&(l(T)||d(T))){let e=T.loc.start.column;T.loc.start.column=0,T.loc.end.line===T.loc.start.line?T.loc.end.column-=e:mn(l(T)?`array`:`table`,o+1,s,C)&&(T.loc.end.column=1)}if(d(e)){let e=K(gr(r.item.key.value,T));return e.comma=r.comma,e}let E=K(T);return e.items.length===0&&l(e)&&d(T)&&(E.comma=!0),E}function ue(e,t,r){let i=u(t)?t.item:t;if(!l(i)&&!d(i))return t;let o=_;for(let e of r)o=o==null?void 0:o[e];if(!Array.isArray(o)&&!F(o))return t;let s=Dn(n,e),f=s?En(n,s)??0:-1,p=s?nr(s)||s.items.length===0&&s.loc.end.line>s.loc.start.line:!1,m=Array.isArray(o)?`array`:`table`,h=m===`array`?c.multilineArray:c.multilineTable,g=mn(m,f+1,p,c),v=u(e)?e.item:e,b=l(v)||d(v)?v:void 0,x=l(v)?`array`:d(v)?`table`:void 0,S=b!==void 0&&x!==m&&Tn(b)&&h===`auto`;if(!(g||S))return t;let C=m===`array`?`multilineArray`:`multilineTable`,w=L({...c,inlineTableStart:0,[C]:!0,trailingComma:y?c.trailingComma:!1},c),T=q({tmp:o},w,f+1,p).items[0];if(!T||!a(T))return t;let E=T.value;if(l(E)||d(E)){let t=u(e)?e.item.loc.start.column:e.loc.start.column;E.loc.end.line===E.loc.start.line?E.loc.end.column+=t-E.loc.start.column:E.loc.end.column=1,E.loc.start.column=t}if(u(t)){let e=K(E);return e.comma=t.comma,e}return E}s.forEach(s=>{if(Si(s)){var v;let o=X(i,s.path),m=s.path.slice(0,-1),g=P(s.path),_=r(o);if(st(g)&&!_&&!st(P(m))){let e=Z(n,m.concat(0));e&&r(e)&&(_=!0)}if(_&&st(g)&&!st(P(m))){let e=Z(n,m),t=e&&a(e)?e.value:e;if(t&&l(t)){let e=J(i.items,``,{temporal:h});for(let t of s.path)e=e==null?void 0:e[t];let r=L({...c,inlineTableStart:0},c),l=e===void 0?void 0:q({tmp:e},r),f=l==null?void 0:l.items[0];if(f&&a(f)&&(o=K(f.value),_=!1,u(o)&&d(o.item)&&o.item.items.length>0)){let e=o.item;if(!(e.items[0].loc.start.column-e.loc.start.column>1)){let r=t.items.map(e=>e.item).find(e=>d(e)&&e.items.length>0);r&&r.items[0].loc.start.column-r.loc.start.column>1&&er(n,e,!0)}}}}if(_&&!r(o)){let e=m.filter(e=>typeof e==`string`),n=J(i.items,``,{temporal:h});for(let e of s.path)n=n==null?void 0:n[e];if(n!==void 0){let i=mr(e),a=q(n,c);for(let e of a.items)V(i,i,e,void 0);let s=n=>{if(p(n))for(let i of n.items){if(!t(i)&&!r(i))continue;let n=i.key,a=n.item,o=e.concat(a.value);a.value=o,a.raw=yr(o).raw,a.loc.start.column=n.loc.start.column+1,a.loc.end.column=a.loc.start.column+a.raw.length,n.loc.end.column=a.loc.start.column+a.raw.length+1,s(i)}};s(i),U(i),o=i}}let y;if(t(o))y=n;else if(_){y=n;let e=n,i=Z(e,m.concat(g-1)),a=Z(e,m.concat(g));if(a)g=e.items.indexOf(a);else if(i){if(g=e.items.indexOf(i)+1,r(i)){let n=i.key.item.value,a=g-1;for(let i=g;in.length&&I(s.slice(0,n.length),n)&&(a=i)}g=a+1,r(o)&&(o=O(o,i))}}else g=e.items.length}else{y=n;for(let e=s.path.length-1;e>=0;e--){let t=Z(n,s.path.slice(0,e));if(t&&!E(t,s.path.slice(0,e))){y=t;break}}a(y)?y=y.value:u(y)&&a(y.item)?y=y.item.value:(u(y)&&d(y.item)||u(y)&&l(y.item))&&(y=y.item)}if((l(y)||d(y))&&(o=le(y,s.path,o),Zr(y,o,c.indentWidth)),l(y)){let e=Z(n,m),t=Z(n,m.slice(0,-1));if(e&&a(e)&&t){let n=ga(y,o,g);n!==0&&ma(t,e,n)}}let b=(l(y)||d(y))&&y.loc.end.line>y.loc.start.line;b&&re.has(y)&&U(n);let x=l(y)||d(y)?(v=Yi(n,y))==null?void 0:v.items:void 0;if(r(y)||l(y)||e(y)){if(l(y)){let e=ae(y,Pr(y));if(u(o)){let t=y.items.length===0&&d(o.item)&&nr(o.item);o.comma=t||e;let n=o,r=d(n.item)?n.item:void 0,l=r&&r.items.length>0?y.items.map(e=>e.item).find(e=>e!==n.item&&d(e)):void 0;if(r&&l){let e=Fr(l);if(e!==Fr(r)){let t=J(i.items,``,{temporal:h});for(let e of s.path)t=t==null?void 0:t[e];let r=L({...c,inlineTableStart:0,trailingComma:e},c),l=t===void 0?void 0:q({tmp:t},r).items[0];if(l&&a(l)){let e=K(l.value);e.comma=n.comma,o=e}}}}}if(c.inlineTableStart!==void 0&&c.inlineTableStart>0&&e(y)&&t(o)){let e=Qa(o,n,c);V(n,y,o,g);for(let t of e)V(n,n,t,void 0)}else{let i=o;u(o)&&(r(y)||e(y))&&(i=o.item);let c=g;if(e(y)&&a(i)){let e=y.items.findIndex(e=>t(e)||r(e));e!==-1&&(c=e)}let d=!1;if((r(y)||e(y))&&a(i)){let e=j(m,i,s.path);e&&(M.has(y)&&U(n),i=e,d=!0)}if(l(y)&&y.items.length===0&&U(n),c===0&&l(y)&&Wn(n).has(y)&&U(n),c===0&&(e(y)||t(y)||r(y))&&Wn(n).has(y)&&U(n),a(i)&&i.key.value.length>1){let e=La(n,i.key.value.slice(0,-1));if(e){let t=i.equals;Ia(i.key,e.key.raw),i.equals=i.key.loc.end.column+e.equals-e.key.loc.end.column;let n=i.equals-t;W(i.value,{lines:0,columns:n},{first_line_only:!0}),i.loc.end.line===i.loc.start.line&&(i.loc.end.column+=n)}}V(n,y,i,c,void 0,x),d&&M.add(y)}}else if(d(y)){y.items.length===0&&(U(n),Rn(y)&&y.loc.end.line>y.loc.start.line&&(y.loc.end.line=y.loc.start.line));let e=ae(y,Fr(y));if(a(o)){let t=K(o);t.comma=e;let r=Ca(y,o,s.before);V(n,y,t,r,void 0,x)}else if(u(o)&&a(o.item)){let t=o.item,r=j(m,t,s.path);r&&(M.has(y)&&U(n),t=r);let i=K(t);i.comma=e;let a=Ca(y,t,s.before);V(n,y,i,a,void 0,x),r&&M.add(y)}else V(n,y,o,void 0,void 0,x)}else if(c.inlineTableStart!==void 0&&c.inlineTableStart>0&&a(o)&&d(o.value)&&t(y))Gr([...y.key.item.value,...o.key.value])0&&y.items.every(f)?2:void 0;V(n,y,i,void 0,void 0,void 0,l),c&&M.add(y)}b&&re.add(y)}else if(Ci(s)){let f=Z(n,s.path),m;try{m=X(i,s.path)}catch{m=void 0}if(!f||!m){Ga(n,i,s,c,h,g,S);return}if(a(f)&&t(m)){let e=m.key.item.value;if($(n,e).some(e=>e!==f)){Ga(n,i,s,c,h,g,S);return}}if(u(f)&&(t(m)||r(m))){let e=Z(n,s.path.slice(0,-1)),t=e&&a(e)?e.value:e;if(t&&l(t)){let e=_;for(let t of s.path)e=e==null?void 0:e[t];if(e!==void 0){let t=L({...c,inlineTableStart:0},c),n=q({tmp:e},t),r=n==null?void 0:n.items[0];r&&a(r)&&(m=K(r.value))}}}let v,y=Z(n,s.path.slice(0,-1));y&&a(y)&&E(y,s.path.slice(0,-1))&&(y=ee(n,y));let x=to(n,s.path);if(a(f)&&a(m)){let i=!1;if(f.key.value.length>1){let o=T(f),c=Sa(s.path,f.key.value,o?o.length-f.key.value.length:0);if(c>0&&c=0;i--){let o=e[i];if(o===f)continue;let s=a(o)?o.key.value:t(o)||r(o)?o.key.item.value:void 0;s&&s.length>=u.length&&I(s.slice(0,u.length),u)&&Q(n,y,o)}}}}if(!i&&a(m)&&y&&(t(y)||e(y)||r(y))&&ro(n,y,f.key.value,f),Ra(f.value,m.value),y&&pa(y,f,f.value,m.value),v=f,f=f.value,m=m.value,i&&_!==void 0){let e=_;for(let t of s.path)e=e==null?void 0:e[t];if(e!==void 0){let t=eo(e,c);t!==void 0&&(m=t,Ra(f,m))}}}else if(a(f)&&u(m)&&a(m.item)){let i=!1;if(f.key.value.length>1){let o=T(f),c=Sa(s.path,f.key.value,o?o.length-f.key.value.length:0);if(c>0&&c=0;i--){let o=e[i];if(o===f)continue;let s=a(o)?o.key.value:t(o)||r(o)?o.key.item.value:void 0;s&&s.length>=u.length&&I(s.slice(0,u.length),u)&&Q(n,y,o)}}}}if(!i&&a(m.item)&&y&&(t(y)||e(y)||r(y))&&ro(n,y,f.key.value,f),v=f,f=f.value,m=m.item.value,i&&_!==void 0){let e=_;for(let t of s.path)e=e==null?void 0:e[t];if(e!==void 0){let t=eo(e,c);t!==void 0&&(m=t)}}}else if(u(f)&&a(f.item)&&a(m)){let e=f.item,i=!1;if(e.key.value.length>1){let o=T(e),c=Sa(s.path,e.key.value,o?o.length-e.key.value.length:0);if(c>0&&c=0;i--){let o=e[i];if(o===f)continue;let s=a(o)?o.key.value:u(o)&&a(o.item)?o.item.key.value:t(o)||r(o)?o.key.item.value:void 0;s&&s.length>=d.length&&I(s.slice(0,d.length),d)&&Q(n,m,o)}}}}if(Ra(e.value,m.value),v=e,f=e.value,m=m.value,i&&_!==void 0){let e=_;for(let t of s.path)e=e==null?void 0:e[t];if(e!==void 0){let t=eo(e,c);t!==void 0&&(m=t,Ra(f,m))}}}else if(u(f)&&u(m)&&a(f.item)&&a(m.item)){let e=!1,i=f.item;if(i.key.value.length>1){let o=T(i),c=Sa(s.path,i.key.value,o?o.length-i.key.value.length:0);if(c>0&&c=0;i--){let o=e[i];if(o===f)continue;let s=a(o)?o.key.value:u(o)&&a(o.item)?o.item.key.value:t(o)||r(o)?o.key.item.value:void 0;s&&s.length>=d.length&&I(s.slice(0,d.length),d)&&Q(n,m,o)}}}}if(Ra(i.value,m.item.value),v=i,f=i.value,m=m.item.value,e&&_!==void 0){let e=_;for(let t of s.path)e=e==null?void 0:e[t];if(e!==void 0){let t=eo(e,c);t!==void 0&&(m=t,Ra(f,m))}}}else if(t(f)){let o=J(i.items,``,{temporal:h});for(let e of s.path)o=o==null?void 0:o[e];if(o!==void 0){let i=f.key.item.value,l=i.slice(-1),u=i.slice(0,-1),d=ee(n,f)??Ni(n,s.path);if(e(d)){let e=$(n,i).filter(e=>e!==f&&(t(e)||r(e)));for(let t of e)Q(n,d,t)}let p=q({[l[0]]:o},c),m=p.items[0];if(u.length>0){if(t(m)||r(m))ne(m,u,f,d),g.add(m);else if($(n,u).some(a))A(m,u,f,d),g.add(m);else{let e=n.items.find(e=>e!==f&&(t(e)||r(e))&&I(e.key.item.value,u));if(e)V(n,e,m,void 0),Q(n,d,f),g.add(m);else{let e=G(u);S.add(e),V(n,e,m,0),Kn(n,d,f,e),g.add(e)}}}else Kn(n,d,f,m),g.add(m),Ua(n,m);if(p.items.length>1)for(let e=1;e0&&te(t,u);let r=e===1?m:p.items[e-1];V(n,d,t,d.items.indexOf(r)+1),g.add(t)}return}v=Ni(n,s.path)}else if(r(f)){let o=J(i.items,``,{temporal:h}),l=f.key.item.value,u=o,d=typeof s.path[s.path.length-1]==`number`?s.path.slice(0,-1):s.path;for(let e of d)u=u==null?void 0:u[e];if(u!==void 0){let i=l.slice(-1),o=l.slice(0,-1),d=ee(n,f)??Ni(n,s.path),p=q({[i[0]]:u},c).items[0];if(Array.isArray(u)&&!u.every(e=>F(e))){let e=$(n,l).filter(e=>e!==f&&(t(e)||r(e)));for(let t of e)Q(n,n,t)}if(o.length>0){if(t(p)||r(p))ne(p,o,f,d),g.add(p);else{let r=e(d)?d.items.find(e=>t(e)&&I(e.key.item.value,o)):void 0;if(r){let e=r.items.find(e=>a(e)&&I(e.key.value,p.key.value));e?Kn(n,r,e,p):V(n,r,p,void 0),Q(n,d,f),g.add(r);return}if($(n,o).some(a))A(p,o,f,d),g.add(p);else{let e=G(o);S.add(e),V(n,e,p,0),Kn(n,d,f,e),g.add(e)}}}else Kn(n,d,f,p),g.add(p),Ua(n,p);return}v=Ni(n,s.path)}else if(v=Ni(n,s.path),a(v)){let e=X(n,s.path.slice(0,-1));a(e)&&l(e.value)&&(v=e.value)}else u(v)&&a(v.item)?v=v.item.value:(u(v)&&d(v.item)||u(v)&&l(v.item))&&(v=v.item);if(u(f)&&u(m)&&(o(f.item)&&o(m.item)&&(Ra(f.item,m.item),m.loc={start:{...m.item.loc.start},end:{...m.item.loc.end}}),m.comma=f.comma),x){let e=b(f.loc),t=b(m.loc).columns-e.columns;t!==0&&ha(x.container,x.row,t)}m=ue(f,m,s.path),Kn(n,v,f,m),l(m)&&se.add(m),(l(m)||d(m))&&(U(n),Ur(m,c.indentWidth),ce.push({owner:v,container:m})),e(v)&&a(f)&&(t(m)||r(m))&&Wa(n,m)}else if(Y(s)){let o=Z(n,s.path);if(o){let f=Ni(n,s.path);if(f===o&&(f=Ni(n,s.path.slice(0,-1))),f===o){let e=Yi(n,o);e&&(f=e)}a(f)&&(f=f.value),u(f)&&a(f.item)&&(f=f.item.value),u(f)&&d(f.item)&&(f=f.item),u(f)&&l(f.item)&&(f=f.item),(!p(f)||!f.items.includes(o))&&(f=ee(n,o)??(p(f)?n:Yi(n,o)??n));let h=!1;if(s.path.length>1&&t(o)&&_!==void 0){let e=s.path.slice(0,-1),t=o.key.item.value.slice(0,-1);if($(n,t).filter(e=>e!==o).length===0){let r=_;for(let t of e)r=r==null?void 0:r[t];if(F(r)&&Object.keys(r).length===0){let e=o;for(;e.items.length>0;)H(n,e,P(e.items));U(n);let r=e.key,i=m(r)?r.item:r;i.value=t,i.raw=Na(i.raw,i.value),i.loc.end.column=i.loc.start.column+i.raw.length,r.loc.end.column=r.loc.start.column+i.raw.length+2,e.loc.end.line=r.loc.end.line,e.loc.end.column=r.loc.end.column,C(e),h=!0}}}let g=e(f)&&p(f)?f.items.indexOf(o):-1,v=p(f)&&!e(f)?f.items.indexOf(o):-1,y=u(o)?o.comma:void 0;u(o)&&(l(f)||d(f))&&Nn(f,o.loc.start.column),u(o)&&(l(f)||d(f))&&v===f.items.length-1&&y!==void 0&&ie.set(f,y);let b=u(o)&&(l(f)||d(f))&&o.loc.end.line===f.loc.end.line?f.loc.end.column-o.loc.end.column:void 0,w=T(u(o)&&a(o.item)?o.item:o),E=a(o)?o:u(o)&&a(o.item)?o.item:void 0;if(E&&w&&p(f)){let e=w.length-E.key.value.length,i=s.path.slice(e);if(i.length>0&&i.lengthi.length&&I(e.slice(0,i.length),i)&&s.push(n)}for(let e of s)Q(n,f,e)}}if(E&&w&&p(f)){let e=E.key.value,i=f.items,s=[];for(let n of i){if(n===o)continue;let i=a(n)?n.key.value:u(n)&&a(n.item)?n.item.key.value:t(n)||r(n)?n.key.item.value:void 0;i&&i.length>e.length&&I(i.slice(0,e.length),e)&&s.push(n)}for(let e of s)Q(n,f,e)}if((t(o)||r(o))&&p(f)){let e=o.key.item.value,i=f.items,s=[];for(let n of i){if(n===o)continue;let i=a(n)?n.key.value:t(n)||r(n)?n.key.item.value:void 0;i&&i.length>e.length&&I(i.slice(0,e.length),e)&&s.push(n)}for(let e of s)Q(n,f,e)}if(h||Q(n,f,o),u(o)&&(l(f)||d(f))&&o.loc.end.line===f.loc.end.line){let e=f.items,t=f.loc.end.line===f.loc.start.line?e.every(e=>e.loc.end.line===f.loc.start.line):e.length>0&&P(e).loc.end.line===f.loc.end.line;if(e.length>0&&v===e.length&&t){let t=b??f.loc.end.column-o.loc.end.column;U(n);let r=P(e).loc.end.column+t;f.loc.end.column!==r&&(f.loc.end.column=r);let i,s=e=>{if(a(e))return e.value===f?e:s(e.value);if(u(e))return s(e.item);if(p(e))for(let t of e.items){let e=s(u(t)?t.item:t);if(e)return e}};if(i=s(n),i&&i.loc.end.line===f.loc.end.line){i.loc.end.column=f.loc.end.column;let e=t=>{if(a(t))return e(t.value);if(u(t))return t.item===i?t:e(t.item);if(p(t))for(let n of t.items){let t=e(n);if(t)return t}},t=e(n);if(t){let e=f.loc.end.column-t.loc.end.column;t.loc.end.column=f.loc.end.column,e!==0&&(Bn(n,t,{lines:0,columns:e}),Vn(n),U(n))}}}}if(!h&&s.path.length>1&&(t(o)||r(o))&&_!==void 0){let e=s.path.slice(0,-1),t=o.key.item.value.slice(0,-1);if($(n,t).length===0){let r=_;for(let t of e)r=r==null?void 0:r[t];if(F(r)&&Object.keys(r).length===0){let e=G(t);S.add(e);let r=g>=0?Math.min(g,n.items.length):n.items.length;U(n),V(n,n,e,r)}}}if(!h&&s.path.length>1&&_!==void 0){let i=a(o)?o:u(o)&&a(o.item)?o.item:void 0;if(i&&i.key.value.length>1){let o=s.path.slice(0,-1),l=e(f)||t(f)||r(f)||d(f)?f:n;if(p(l)){let s;if(t(l)||r(l)){let e=l.key.item.value,t=0,n=0;for(let r=0;r=o.length||o[t]!==e[r])break;n++,t++}for(;t=o.length||o[t]!==e[n])break;n++,t++}for(;ttypeof e==`string`);let f=l.items.filter(e=>{let t=a(e)?e.key.value:u(e)&&a(e.item)?e.item.key.value:void 0;return t?I(t.slice(0,s.length),s):!1}),p=r(l),m=[];p&&f.length===0&&(m=$(n,l.key.item.value.concat(s)));let h=s.length===0&&(t(l)||r(l)||d(l));if(f.length===0&&m.length===0&&!h){let r=_;for(let e of o)r=r==null?void 0:r[e];if(F(r)&&Object.keys(r).length===0){if(d(l)){let e=s.map(e=>k.test(e)?e:JSON.stringify(e).replace(/\x7f/g,`\\u007f`)).join(`.`),t=Array.from(Be(`${e} = {}`))[0];if(a(t)){let e=K(t);e.comma=y??!1,U(n),Rn(l)&&l.loc.end.line>l.loc.start.line&&(l.loc.end.line=l.loc.start.line),V(n,l,e,v>=0?v:void 0),c.bracketSpacing&&W(e,{lines:0,columns:1});let r=l.items[v+1],i=B(n).get(e);r&&i&&r.loc.start.line===e.loc.end.line&&(i.columns=e.loc.end.column+2-r.loc.start.column)}return}let r=G(p||t(l)?l.key.item.value.concat(s):s);S.add(r);let i=g>=0?g:n.items.length;if(p){let e=n.items.indexOf(l);i=e>=0?e+1:n.items.length}else if(t(l)){let e=n.items.indexOf(l);i=e>=0?e+1:n.items.length}else if(e(l)){let e=Va(n);i=e>=0?e:n.items.length}U(n),V(n,n,r,i)}}}}}if(r(o)){let e=o.key.item.value,t=s.path.slice(0,-1);!Z(n,t.concat(0))&&Z(i,t)&&x.set(e.join(`.`),e)}}else{let e=()=>Z(n,s.path.concat(0))||$(n,s.path).filter(r)[0],a=e();if(a){let o=n.items.indexOf(a),c=!1;if(s.path.length>1&&_!==void 0){let e=s.path.slice(0,-1);if($(n,e).filter(e=>!(r(e)&&I(e.key.item.value,s.path))).length===0){let t=_;for(let n of e)t=t==null?void 0:t[n];if(F(t)&&Object.keys(t).length===0){let t=a,r=t.key;for(;t.items.length>0;)Q(n,t,P(t.items));U(n),t.type=`Table`,r.type=`TableKey`;let i=m(r)?r.item:r;i.value=e,i.raw=Na(i.raw,i.value),i.loc.end.column=i.loc.start.column+i.raw.length,r.loc.end.column=r.loc.start.column+i.raw.length+2,t.loc.end.line=r.loc.end.line,t.loc.end.column=r.loc.end.column,C(t),c=!0}}}let l;for(;l=e();)Q(n,n,l);for(let e of $(n,s.path).filter(t))Q(n,n,e);if(Z(i,s.path)){let e=s.path;x.set(e.join(`.`),e)}if(!c&&s.path.length>1&&_!==void 0){let e=s.path.slice(0,-1);if($(n,e).length===0){let t=_;for(let n of e)t=t==null?void 0:t[n];if(F(t)&&Object.keys(t).length===0){let t=G(e);S.add(t);let r=o>=0?Math.min(o,n.items.length):n.items.length;U(n),V(n,n,t,r)}}}}else{let e=no(n,s.path),t=$(n,e);if(t.length>0){let r=n.items.indexOf(t[0]);for(let e of t)Q(n,n,e);if(e.length>1&&_!==void 0){let t=e.slice(0,-1);if($(n,t).length===0){let e=_;for(let t of s.path.slice(0,-1))e=e==null?void 0:e[t];if(F(e)&&Object.keys(e).length===0){let e=G(t);S.add(e);let i=r>=0?Math.min(r,n.items.length):n.items.length;U(n),V(n,n,e,i)}}}}else X(n,s.path)}}}else if(wi(s)){if(s.key!==void 0){oe.push(s);return}let e=Z(n,s.path);if(e){if(m(e)&&(e=e.item),a(e)&&(e=e.value),s.from>=e.items.length)return;let t=e.items[s.from];Qi(n,e,t,s.to)}else{Ea(n);let e=X(n,s.path.concat(s.from)),i=Ki(n).find(t=>t.member===e),a=i?[...i.items]:[e],o=a.map(e=>({startLine:e.loc.start.line,endLine:e.loc.end.line})),c=n.items.indexOf(i?i.items[0]:e),l=o[o.length-1].endLine,u;for(let e=0;el){u=t.loc.start.line-l;break}}if(u===void 0&&c>0){let e=n.items[c-1];u=o[0].startLine-e.loc.end.line}let d=i?i.items.indexOf(e):0;for(let e=0;e<=d;e++)n.items.includes(a[e])&&H(n,n,a[e]);for(let e=d+1;e=0&&n.items.splice(t,1)}let p=Z(n,s.path.concat(s.to)),m,h=!1,g=i&&i.items[0]!==e&&f(i.items[0]);if(p){let e=Ki(n).find(e=>e.member===p);h=!!(e&&e.items[0]!==p&&f(e.items[0])),m=h&&g?n.items.indexOf(e.items[0]):n.items.indexOf(p)}else m=n.items.length;let _=m>0?n.items[m-1].loc.end.line:0,v=m0&&m0){let n=e===0?_:o[e-1].endLine,a=e===0&&v!==void 0?v-_:o[e].startLine-n;a!==(t(i)||r(i)?2:1)&&(s=a)}V(n,n,i,b,void 0,void 0,s),b++}if(u!==void 0){U(n);let e=a[a.length-1],t=n.items.indexOf(e)+1;if(t0&&(o.equals+=t,W(o.value,{lines:0,columns:t},{first_line_only:!0}),Bn(n,o.value,{lines:0,columns:t}));return}throw Error(`Cannot rename "${d.raw}" to "${f.raw}": the replacement key has ${f.value.length} segment(s) where the existing key has ${d.value.length}, so one cannot be substituted for the other.`)}f.raw=Na(d.raw,f.value),Ia(f,d.raw),f.loc.end.column=f.loc.start.column+f.raw.length,Kn(n,m(l)?l:o,d,f)}}),U(n);for(let e=1;e0&&W(i,{lines:-e,columns:0})}}let de=n.items;for(let e=0;e1){let n=1-t;for(let t=e+1;t{Rn(e)&&(fe=!0)},InlineArray:e=>{Rn(e)&&(fe=!0)}}),fe&&U(n);let N=!1,pe=[];if(Dt(n,{InlineTable:e=>{Rn(e)&&(pe.push(e),Ka(e),zn(e),er(n,e,c.bracketSpacing),N=!0)},InlineArray:e=>{Rn(e)&&(Ka(e),zn(e),er(n,e,c.bracketSpacing),N=!0)}}),N){U(n);for(let e of pe)qa(n,e);Ja(n)}if(Xa(n),Za(n,x,c),oa(n,oe,g),oe.length>0&&Vn(n),se.size>0){U(n);for(let e of se)Qr(e,c.indentWidth)}if(ce.length>0){U(n);for(let{owner:e,container:t}of ce)Ba(t),Vr(t,e.loc.start.column,c.indentWidth,c.bracketSpacing)}return n}function Ba(e){if(delete e.range,p(e))for(let t of e.items)Ba(t);m(e)&&Ba(e.item),a(e)&&(Ba(e.key),Ba(e.value))}function Va(e){return e.items.findIndex(e=>t(e)||r(e))}function Ha(e){let t=Va(e);return t===-1?void 0:t}function Ua(e,t){let n=e.items.indexOf(t);if(n<0)return;let r=Va(e);r===-1||r>n||(H(e,e,t),U(e),V(e,e,t,r))}function Wa(e,n){let i=e.items.indexOf(n);if(i<0)return;let o=e.items.findIndex((e,n)=>n>i&&(t(e)||r(e)));if(!e.items.some((e,t)=>t>i&&a(e)&&(o===-1||ttypeof e==`number`);if(n>0&&i.path.slice(n+1).every(e=>typeof e==`string`)){let s=Z(e,i.path.slice(0,n+1));if(s&&r(s)){let r=s.key.item.value,c=i.path.slice(n+1),l=$(e,r.concat(c));for(let t of l)Q(e,e,t);let u=s.items.filter(e=>{let t=a(e)?e.key.value:void 0;return t&&t.length>c.length&&I(t.slice(0,c.length),c)});for(let t of u)Q(e,s,t);let d=f;for(let e=i.path.length-1;e>n;e--)d={[i.path[e]]:d};let p=ai(q(d,o).items,o),m=Array.from(Be(p)),h=t=>{let n=s.items.find(e=>a(e)&&I(e.key.value,t.key.value));n?Kn(e,s,n,t):V(e,s,t,void 0)};for(let e of m){if(a(e)){h(e);continue}if(t(e)){let t=e.key.item.value;for(let n of e.items){if(!a(n))continue;let e=n.key.raw,r=t.concat(n.key.value);n.key.value=r,n.key.raw=yr(r).raw;let i=n.key.raw.length-e.length;n.key.loc.end.column=n.key.loc.start.column+n.key.raw.length,n.equals+=i,W(n.value,{lines:0,columns:i},{first_line_only:!0}),n.loc.end.line===n.loc.start.line&&(n.loc.end.column+=i),h(n)}}}return}}}let p=f;for(let e=i.path.length-1;e>=0;e--)p={[i.path[e]]:p};let m=$(e,i.path);for(let t of m)H(e,e,t);let h=ai(q(p,o).items,o),g=Array.from(Be(h)),_=Ha(e);_!==void 0&&U(e);let v;for(let n=0;nt(e)&&I(e.key.item.value,i.key.item.value)):void 0;if(o){for(let t of i.items)V(e,o,t,void 0);v||=o;continue}if(a(i)&&d(i.value)){let o=i.key.value,s=e.items.find(e=>t(e)&&I(e.key.item.value,o));if(s){for(let t of i.value.items)V(e,s,u(t)?t.item:t,void 0);v||=s;continue}if($(e,o).filter(e=>a(e)||t(e)||r(e)).length>0){for(let t of i.value.items){let r=u(t)?t.item:t;if(a(r)){let e=r.key.raw,t=o.concat(r.key.value);r.key.value=t,r.key.raw=t.map(e=>k.test(e)?e:JSON.stringify(e).replace(/\x7f/g,`\\u007f`)).join(`.`);let n=r.key.raw.length-e.length;r.key.loc.end.column=r.key.loc.start.column+r.key.raw.length,r.equals+=n,W(r.value,{lines:0,columns:n},{first_line_only:!0}),r.loc.end.line===r.loc.start.line&&(r.loc.end.column+=n)}V(e,e,r,n===0?_:void 0),v||=r}continue}}if(t(i)){let t=i.key.item.value;if($(e,t).filter(e=>a(e)).length>0){for(let r of i.items){if(a(r)){let e=r.key.raw,n=t.concat(r.key.value);r.key.value=n,r.key.raw=n.map(e=>k.test(e)?e:JSON.stringify(e).replace(/\x7f/g,`\\u007f`)).join(`.`);let i=r.key.raw.length-e.length;r.key.loc.end.column=r.key.loc.start.column+r.key.raw.length,r.equals+=i,W(r.value,{lines:0,columns:i},{first_line_only:!0}),r.loc.end.line===r.loc.start.line&&(r.loc.end.column+=i)}V(e,e,r,n===0?_:void 0),v||=r}continue}}n>0&&v&&t(v)&&a(i)?V(e,v,i,void 0):V(e,e,i,n===0?_:void 0),v||=i}let y=v;t(y)&&l.add(y),c.add(y)}function Ka(e){if(e.items.length>0){let t=e.items[e.items.length-1];e.loc.end.column=t.loc.end.column+1}else e.loc.end.line=e.loc.start.line,e.loc.end.column=e.loc.start.column+2}function qa(e,t){let n=[],r=e=>{if(n.push(e),e===t)return!0;if(a(e)){if(r(e.value))return!0}else if(u(e)){if(r(e.item))return!0}else if(p(e)){for(let t of e.items)if(r(t))return!0}return n.pop(),!1};if(!r(e))return;let i=n.findLast(u);if(!i)return;let o=t.loc.end.line-i.loc.end.line;if(o===0)return;let s=n.findLastIndex((e,t)=>t=0?c.items[m+1]:void 0;if(m<0||!h)return;let g=h.loc.start.column-f.loc.end.column,_=Ya(f)+g-h.loc.start.column;for(let e=m+1;et.loc.end.line&&(e.loc.end.line+=o);for(let e=n.length-1;e>=0;e--){let r=n[e];a(r)?(r.loc.end={...r.value.loc.end},r.value!==t&&d(r.value)&&(r.loc.end.column+=_)):u(r)&&(r.loc.end={...r.item.loc.end})}}function Ja(e){Dt(e,{InlineTable:{exit:e=>{if(e.items.length===0||e.loc.end.line!==e.loc.start.line)return;let t=e.items[e.items.length-1],n=Ya(t),r=e.loc.end.column,i=n+(r-t.loc.end.column);i!==r&&(e.loc.end.column=i)}},InlineArray:{exit:e=>{if(e.items.length===0||e.loc.end.line!==e.loc.start.line)return;let t=e.items[e.items.length-1],n=Ya(t),r=e.loc.end.column,i=n+(r-t.loc.end.column);i!==r&&(e.loc.end.column=i)}}})}function Ya(e){if(!u(e))return e.loc.end.column;if(a(e.item)){let t=e.item.value;if(d(t)||l(t))return t.loc.end.column}return d(e.item)||l(e.item)?e.item.loc.end.column:e.loc.end.column}function Xa(e){Dt(e,{KeyValue:t=>{let n=t.value;if(!l(n)||n.items.length>0)return;let r=Ni(e,[t.key.value[0]]);if(!r||!p(r))return;let i=t.loc.start.line,a=t.loc.end.line,o=r.items;for(let e=o.length-1;e>=0;e--){let t=o[e];if(!f(t))continue;let n=t.loc.start.line;n>=i&&n<=a&&o.splice(e,1)}}})}function Za(e,n,i){if(n.size!==0){for(let a of n.values()){if(a.length>1){let n=Z(e,a.slice(0,-1).concat([0]));if(n&&r(n)){let t=a[a.length-1],r=q({[t]:[]},i),o=gr([t],r.items[0].value);U(e),V(e,n,o);continue}let o;for(let n=a.length-1;n>=1;n--){let r=Z(e,a.slice(0,n));if(r&&t(r)){o=r;break}}if(o){let t=a.slice(o.key.item.value.length),n=gr(t,q({[t[t.length-1]]:[]},i).items[0].value);U(e),V(e,o,n);continue}}let n=gr(a,q({[a[a.length-1]]:[]},i).items[0].value);U(e),V(e,e,n,Ha(e))}U(e)}}function Qa(e,t,n){let r=[],i=(e,r)=>{for(let o=e.items.length-1;o>=0;o--){let s=e.items[o];if(a(s)&&d(s.value)){let c=[...e.key.item.value,...s.key.value];if(Gr(c)<(n.inlineTableStart??1)&&n.inlineTableStart!==0){let n=G(c);for(let e of s.value.items)u(e)&&a(e.item)&&V(t,n,e.item,void 0);e.items.splice(o,1),Wr(e),r.push(n),i(n,r)}}}};return i(e,r),r}function $a(e,t,n,r){let i=G([...t.key.item.value,...e.key.value]);if(d(e.value))for(let t of e.value.items)u(t)&&a(t.item)&&V(n,i,t.item,void 0);V(n,n,i,void 0),Wr(t);let o=Qa(i,n,r);for(let e of o)V(n,n,e,void 0)}function eo(e,n){let r=L({...n,inlineTableStart:0},n),i=ai(q({__tmp__:e},r).items,r),o=Array.from(Be(i))[0];if(a(o))return o.value;if(t(o)&&o.items.length>0&&a(o.items[0]))return o.items[0].value}function to(e,t){for(let n=t.length-1;n>0;n--){let r=Z(e,t.slice(0,n));if(!r||!a(r)||!d(r.value))continue;let i=Z(e,t.slice(0,n-1));if(i&&p(i))return{container:i,row:r}}}function $(e,n){let i=[];for(let o of e.items){let e;a(o)?e=o.key.value:(t(o)||r(o))&&(e=o.key.item.value),e&&I(e.slice(0,n.length),n)&&i.push(o)}return i}function no(e,t){let n=[];for(let i of t)typeof i==`number`&&$(e,n).some(r)||n.push(i);return n}function ro(e,n,i,o){let s=[...n.items];for(let c=s.length-1;c>=0;c--){let l=s[c];if(l===o)continue;let u=a(l)?l.key.value:t(l)||r(l)?l.key.item.value:void 0;u&&u.length>i.length&&I(u.slice(0,i.length),i)&&Q(e,n,l)}}function io(e,t){return e.line===t.line?e.column-t.column:e.line-t.line}function ao(e,t,n){let r={line:t,column:n},i=[],a=null;for(let t of e){let e=io(t.loc.end,r)<0,n=io(t.loc.start,r)<0;if(e)i.push(t),a=t.loc.end;else if(n&&!e)break;else break}return{truncatedCst:i,lastEndPosition:a}}var oo=class{_cst;_currentTomlString;_format;_integersAsBigInt;_temporal;constructor(e,t){let n=typeof e==`string`?e:Ot(e),r=At(n);this._currentTomlString=r,this._cst=Array.from(Be(r)),this._integersAsBigInt=(t==null?void 0:t.integersAsBigInt)??`asNeeded`,this._temporal=(t==null?void 0:t.temporal)??!1,this._format=R.autoDetectFormatWithCst(n,this._cst)}get toTomlString(){return this._format.leadingBom?`${this._currentTomlString}`:this._currentTomlString}get toJsObject(){let e=J(this._cst,this._currentTomlString,{integersAsBigInt:this._integersAsBigInt,temporal:this._temporal});return this._temporal?e:lo(e)}get cst(){return this._cst}patch(e,t){let n=L(t,this._format),r=this._currentTomlString;if(!this._temporal&&so(e)){let t=J(this._cst,r,{integersAsBigInt:this._integersAsBigInt,temporal:!1});e=co(e,t)}let i=t instanceof R||t!=null&&Object.prototype.hasOwnProperty.call(t,`indentWidth`),a=t instanceof R||t!=null&&Object.prototype.hasOwnProperty.call(t,`trailingComma`);Qt(this._cst,r);let o=Da(this._cst,e,n,i,a);this._cst=Array.from(Be(o.tomlString)),this._format=n,this._currentTomlString=o.tomlString}update(e){let t=At(e);if(t===this._currentTomlString){this._format.leadingBom=kt(e);return}let n=this._currentTomlString.split(this._format.newLine),r=zt(t),i=t.split(r),a=0;for(;a0&&d>0&&(f[0]=f[0].substring(d));let p=f.join(this._format.newLine);this._cst=Array.from(Ve(c,p,{line:u,column:d})),this._currentTomlString=t,this._format=R.autoDetectFormatWithCst(e,this._cst)}overwrite(e){let t=At(e);if(t===this._currentTomlString){this._format.leadingBom=kt(e);return}this._cst=Array.from(Be(t)),this._currentTomlString=t,this._format=R.autoDetectFormatWithCst(e,this._cst)}};function so(e,t=new WeakSet){return e instanceof Date?e.constructor===Date:!e||typeof e!=`object`||t.has(e)?!1:(t.add(e),Array.isArray(e)?e.some(e=>so(e,t)):Object.keys(e).some(n=>so(e[n],t)))}function co(e,t,n=new WeakSet){if(e instanceof Date)return e.constructor===Date&&t instanceof Date&&t.constructor!==Date&&t.getTime()===e.getTime()?t:e;if(!e||typeof e!=`object`||n.has(e))return e;if(n.add(e),Array.isArray(e)){if(!Array.isArray(t))return e;let r=!1,i=e.map((e,i)=>{let a=co(e,t[i],n);return a!==e&&(r=!0),a});return r?i:e}if(!F(t))return e;let r=!1,i={};for(let a of Object.keys(e)){let o=e[a],s=co(o,t[a],n);i[a]=s,s!==o&&(r=!0)}return r?i:e}function lo(e){if(e instanceof Date)return new Date(e.getTime());if(Array.isArray(e))return e.map(lo);if(e&&typeof e==`object`){let t={};for(let[n,r]of Object.entries(e))t[n]=lo(r);return t}return e}const uo=Object.freeze(R.default());function fo(e){let t=uo;return e.newLine===t.newLine&&e.trailingNewline===t.trailingNewline&&e.trailingComma===t.trailingComma&&e.bracketSpacing===t.bracketSpacing&&e.inlineTableStart===t.inlineTableStart&&e.truncateZeroTimeInDates===t.truncateZeroTimeInDates&&e.useTabsForIndentation===t.useTabsForIndentation&&e.indentWidth===t.indentWidth&&e.minimumDecimals===t.minimumDecimals&&e.leadingBom===t.leadingBom&&e.updateOrder===t.updateOrder&&e.multilineTable===t.multilineTable&&e.multilineArray===t.multilineArray}function po(e,t){let n=At(typeof e==`string`?e:Ot(e));return J(Be(n),n,{integersAsBigInt:(t==null?void 0:t.integersAsBigInt)??`asNeeded`,temporal:(t==null?void 0:t.temporal)??!1})}function mo(e,t){let n=t==null?uo:L(t,R.default()),r=q(e,n),i=fo(n)?si(r.items,n):ai(r.items,n);return n.leadingBom?`${i}`:i}function ho(e,t){return new oo(e,t)}export{ge as LocalDate,ve as LocalDateTime,_e as LocalTime,ye as OffsetDateTime,oo as TomlDocument,R as TomlFormat,po as parse,ho as parseDocument,xa as patch,mo as stringify};
\ No newline at end of file
diff --git a/docker/t3-client/setup-bridge.js b/docker/t3-client/setup-bridge.js
index 43a3325..5805a98 100644
--- a/docker/t3-client/setup-bridge.js
+++ b/docker/t3-client/setup-bridge.js
@@ -69,7 +69,8 @@
const PALETTE_ITEMS = [
{ id: 'setup', route: null, title: 'Open setup', meta: 'Agents, skills, toolchains, source control, ports, devices and updates on this server', terms: ['setup console'] },
{ id: 'agents', route: 'agents', title: 'Agents', meta: 'Setup · sign in, install and update', terms: [] },
- { id: 'context', route: 'instructions', title: 'Instructions & skills', meta: 'Setup · global AGENTS.md and skills', terms: ['agents.md global instructions', 'context'] },
+ { id: 'context', route: 'instructions', title: 'Instructions & skills', meta: 'Setup · global AGENTS.md, skills and MCP servers', terms: ['agents.md global instructions', 'context', 'mcp servers tools'] },
+ { id: 'mcp', route: 'instructions/mcp', title: 'MCP servers', meta: 'Setup · shared definitions and provider configuration', terms: ['mcp tools servers'] },
{ id: 'pair', route: 'access', title: 'Pair a device', meta: 'Setup · Access', terms: ['devices', 'pairing'] },
{ id: 'toolchains', route: 'toolchains', title: 'Toolchains', meta: 'Setup · Go, Rust, Bun, Deno, uv and any mise tool', terms: [] },
{ id: 'sourcecontrol', route: 'sourcecontrol', title: 'Source control', meta: 'Setup · sign in to GitHub, GitLab, Forgejo, Gitea and Azure DevOps', terms: ['codeberg', 'gh glab fj tea az'] },
diff --git a/docs/agent-browser.md b/docs/agent-browser.md
index b0e184e..5a94fe0 100644
--- a/docs/agent-browser.md
+++ b/docs/agent-browser.md
@@ -32,9 +32,9 @@ docker compose exec t3code t3-browser-mcp --remove
This registers headless Chromium as an MCP server with the installed Claude
Code, Codex and OpenCode, so agents can navigate, take screenshots, click and
read the browser console, whichever client you use. The console's
-**Instructions & skills → MCP servers** tab manages the same servers as shared
-entries (linked per provider) with the same restart rule; `t3-browser-mcp`
-remains the quick CLI equivalent. Run either after the first
+**Instructions & skills → MCP servers** tab can define these commands and
+apply them per provider, with the same restart rule. `t3-browser-mcp` writes
+provider entries directly without creating shared definitions. Run either after the first
start has installed those agents (the setup page shows when), then start a new
thread, because agents read their MCP configuration when a session starts.
diff --git a/docs/context-management.md b/docs/context-management.md
index e72324f..cc3a0a7 100644
--- a/docs/context-management.md
+++ b/docs/context-management.md
@@ -356,37 +356,81 @@ still race the final check. Start a new conversation to load policy changes.
### Shared MCP servers
-The **MCP servers** tab holds one canonical list of Model Context Protocol
-servers at `$T3CODE_HOME/setup/mcp/servers.json` (mode `0600`, at most 128
-entries) and links each entry into the providers that should use it. A local
-server runs a command (`playwright-mcp --headless …`); a remote server points
-at an `http(s)` URL. Environment variables (local) and headers (remote) are
-stored with the entry and written into linked provider configs; they never
-appear in `/status` polling, which carries redacted summaries only
-(`envKeys`, `secretCount`). The full entry loads on demand in the editor.
-
-Linking a provider merges the translated entry into its own config file and
-leaves every other server and setting in place. Unlinking removes only the
-shared entry. A provider-side edit shows as diverged: relink to apply the
-shared entry again. Provider-only servers are never touched. Start a new
-session after changing links: agents read MCP configuration at startup.
-
-How well each provider is supported:
-
-| Provider | Config file | Local | Remote | Notes |
-| --- | --- | --- | --- | --- |
-| Claude Code | `~/.claude.json` (`mcpServers`) | ✅ command/args/env | ✅ `type: http` + url/headers | File-based; project `.mcp.json` is out of scope |
-| Codex | `~/.codex/config.toml` (`[mcp_servers.*]`) | ✅ command/args/env | ✅ url/`http_headers` | Only `mcp_servers` sections are parsed; the rest of the file is preserved byte-for-byte |
-| OpenCode | `opencode.json[c]` (`.mcp`) | ✅ `type: local`, command array | ✅ `type: remote` + url/headers | JSONC wins when both exist; comments preserved |
-| Cursor | `~/.cursor/mcp.json` (`mcpServers`) | ✅ command/args/env | ✅ url/headers | Same shape as Claude user scope |
-| Antigravity | `~/.gemini/settings.json` (`mcpServers`) | ✅ command/args/env | ✅ `httpUrl`/headers | Gemini CLI shape; Antigravity divergence from it is unverified |
-| Grok Build | — | — | — | MCP layout unverified against the pinned CLI; not managed here yet |
-| Muse Code | — | — | — | MCP layout unverified against the pinned CLI; not managed here yet |
-
-Only Codex and OpenCode have a native enabled flag, so a disabled shared
-server links there and stays unlinked elsewhere (unlink represents disabled).
-`t3-browser-mcp` remains the quick CLI path for the two bundled browser
-servers; the console manages the shared list above it.
+The **MCP servers** tab holds local command and Streamable HTTP definitions at
+`$T3CODE_HOME/setup/mcp/servers.json` (0600, at most 128 entries and 256 KiB).
+Add/edit the command as a JSON array and environment/headers as JSON objects,
+so empty arguments, whitespace and multiline values survive editing. Values
+are literal; variable interpolation follows each provider's native rules.
+
+**Save updates the shared definition. Apply updates a provider's copy.**
+Existing copies stay unchanged after a shared edit and show **Differs** until
+explicitly applied or unlinked. Switches show whether a same-name entry exists,
+including disabled or differing entries; they do not establish a live connection.
+**Apply** replaces that complete entry, including provider-only options, after
+confirmation. Unlink removes it. Both preserve a private config backup next to
+the resolved target (`.t3-mcp-backup.`); these files contain secrets
+and can be removed manually once no longer needed. Unlink from every supported
+provider before removing the shared definition. Other provider entries remain
+in place. Start a new provider process/session after applying changes.
+
+The tab manages default global config files, honoring the server environment's
+Claude/Codex/XDG/OpenCode directory overrides. Custom T3 provider-instance homes,
+project overrides, enterprise policy, approvals, plugin servers and effective
+runtime availability are outside its inventory. An unreadable provider config
+blocks removal rather than silently leaving an unknown copy behind.
+
+| Provider | Native global config | Support in this release |
+| --- | --- | --- |
+| Claude Code | `~/.claude.json`, or `$CLAUDE_CONFIG_DIR/.claude.json` | Local command/args/env and HTTP URL/headers |
+| Codex | `$CODEX_HOME/config.toml` (default `~/.codex/config.toml`) | Local and HTTP; native `enabled`; full TOML parsing with edits limited to the selected server |
+| OpenCode 1 | `opencode.json[c]`, `.mcp[name]` | Local and HTTP; native `enabled` |
+| OpenCode 2 | `opencode.json[c]`, `.mcp.servers[name]` | Local and HTTP; native `disabled`; existing native V2 files stay V2; new files use the V1-compatible format also accepted by V2 |
+| Cursor | `~/.cursor/mcp.json` | Local and HTTP; native approval/login still applies |
+| Antigravity | T3's isolated provider profile | Not integrated; Gemini CLI settings are not an Antigravity integration |
+| Grok Build | `~/.grok/config.toml` | Not managed; Grok may also load Claude/Cursor MCP entries through native compatibility imports |
+| Muse Code | `$XDG_CONFIG_HOME/muse/settings.json` | Not managed in this release |
+
+OpenCode JSONC wins if both files exist. Codex uses a pinned vendored TOML
+syntax tree; nested tables, arrays of tables and unrelated comments are preserved.
+All provider writes share the native-settings serialization queue with skills,
+follow existing file/directory symlinks, reject stale revisions/retargeting and
+publish atomically with owner-only permissions. Other applications can still
+race between the final check and rename; this is not a cross-process transaction.
+With credential persistence enabled, Claude's default `.claude.json` also lives
+on the state volume. Custom roots need their own persistent mounts.
+
+Only Codex and OpenCode have a disabled state managed by this tab. For Claude
+and Cursor, unlink to disable a copy. Changing the definition's enabled flag
+does not stop old copies or existing sessions. A disabled definition with an
+old copy remains visible as **Differs** and can always be unlinked.
+
+Environment/header values load only in the authenticated editor. Polling and
+lists contain key names/counts, and remote URLs show only their origin (paths
+and queries can contain credentials). Commands are displayed, so put secrets in
+Environment or Headers. Credentials in URL userinfo are rejected. OAuth is not
+shared: sign in through `claude mcp login`, `codex mcp login`, `opencode mcp auth`
+or `cursor-agent mcp login` where required. Legacy SSE-only endpoints are not a
+portable substitute for Streamable HTTP.
+
+Native checks on 2026-10-19 passed local and HTTP handshakes, environment/header
+translation and unlink for Claude 2.1.303, Codex 0.163.0, OpenCode 1.14.19,
+OpenCode 2.0.18/2.0.32 and Cursor 2026.10.18-ea085cb. Codex/OpenCode disabled
+entries are checked not to start/connect. Checks use disposable homes and
+fixture servers, without a model prompt or real credentials. This verifies
+native configuration and MCP negotiation, not model-selected tool calls or
+OAuth. Reproduce with `node scripts/mcp-native-smoke.mjs PROVIDER /absolute/path`.
+
+Native contracts: [Claude](https://code.claude.com/docs/en/mcp),
+[Codex](https://learn.chatgpt.com/docs/extend/mcp?surface=cli),
+[OpenCode V1](https://opencode.ai/docs/mcp-servers/),
+[OpenCode V2](https://dev.opencode.ai/v2/docs/mcp-servers/),
+[Cursor](https://cursor.com/docs/cli/mcp),
+[Antigravity](https://antigravity.google/docs/cli/gcli-migration/),
+[Grok](https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-pager/docs/user-guide/07-mcp-servers.md),
+and [Muse](https://meta-models.github.io/muse-code-sdk/next/guides/extend/mcp-servers/).
+
+`t3-browser-mcp` remains a separate quick CLI path for the bundled browser
+servers; it writes provider entries directly and does not create shared definitions.
## Validation
@@ -426,10 +470,13 @@ also cover catalog selection, raw Markdown previews, provider selection,
replacement consent/conflicts, failed backup moves/restoration, supporting-file
edits with restored mtimes, shared-root retargeting and partial results.
Unit/HTTP checks for shared MCP servers (`tests/mcp.test.mjs`,
-`tests/mcp-api.test.mjs`) cover store validation and revisions, TOML section
+`tests/mcp-api.test.mjs`, `tests/mcp-regressions.test.mjs`) cover store validation and revisions, TOML section
preservation, provider translation round-trips, link/diverged detection,
comment-preserving JSONC writes, unsupported providers and the authenticated
-boundary including secret redaction in polling responses.
+boundary including secret redaction in polling responses. `scripts/mcp-ui-audit.js`
+checks immediate refresh, editor revisions, lossless JSON values, apply/unlink,
+failed/late reads and mobile/desktop/embedded layouts in both themes. Its API
+responses are fixtures; no real provider configuration is modified.
The adapters were also checked with Codex 0.163.0 in a disposable home. Native
enable/disable, stale-request rejection, settings persistence across fresh
diff --git a/docs/media/mcp/desktop-dark.png b/docs/media/mcp/desktop-dark.png
new file mode 100644
index 0000000000000000000000000000000000000000..027caee0874828ff7c15f732f53ceb04bfb7beba
GIT binary patch
literal 419010
zcmY(KWl&sO*R64PcWB(*T^hHhaSa}v;KAJ`xQ1Yjy9NpF?rwqL7J?-~Z=ZYb`@QG;
z(_PiIcXid;bB!_QGuDm;X((c%k)grBz+fsX0l_dZh=njP2uLV!&|fsJYn1rMuXD6`OozJHhS9n&6tdno-;pXHZ}CS+h@1?_=`vH@9yra4afW3
zvMlhFNI8k|U;Ut%q63g@tjt@Q;*Bedyb#+XFrhMC@0D1yEcNho4ni-6$`hGRxEw$
z?>N6(CQW9tsql+?q|lHNT9gAOs0w}8?4-^IZ({FET8v}CuQJ@M2~tf#zM1NATT?m=
zF89NICu!@EkT7ORCg%
z*Co}pv*zjKt2(Y5b=edY!1VdFxX_%2$SSkT`eV!w^+QMs6CL_|Tj}ahj~NW=kCDg;
z`7D6jSvb2%np?`rvMzSwa!X|T=nrDkyK(!)u))0#!W3B1|9rjvweaGtQmn4h$Rw&XxNc*F0wkvr7h{G6`pa<(^8NjOyP7{=}7a+4`|E2qC0r8BdrgFt)^rZVfJ}
z>3e_{)A=92?x-KSS4iS8*~FBrq%cKZpeJG+daZT*&sH2u=uzRMgK35=I6o3CVR9l^
zGUw0hJbCY#jXJRl>0sp|y!uUSsG{EM&QgAs8~QZAWAK69xN?dVIWR9eY`dTJ}WoOlWrNGTB#5qqWn8>qmvPfhurmzBx<
zOxh}>5|)LIO`ew45;Nl)_l7bt0eh?&7DHwd`oiISd}VB?*g@3@Y_%*Ned_SyWFx}=
zUB~~ooKyHp?T2+Yn0m3XO=z{k+8G>5fNbBi&tK00VPoH#T^KBRH)xy7hNECVD=A2D
zxZ10}i$B+?M7Wan-f62?)5=OSVXkPwznZfuF<`(&H38E5gglCy3xStz0%)t9_P9J}
za}YUyZl3owIlw{4xaKNCTIRyY83eMC7*H=5gLN81cu^nIdvZ)<0En_z#RbNCKg4}L
z^T+wcFoNok_Y7(8*dEb7MvbZTH1THZK7Rr$W%El^;G)VXM=Y!*j4T|1qo-fYn3+COHWyY@}s0h)Od6i!%G>^Q@P8`(#GgcR@PKg%OemLBK4A3!ic_8p+iW&lPOV+
zDdB4n5liQlXw)aBm?V6W+a%iy`O)U=sE<)_aF+Ktn9alxs1(l91-Vsj!_0zhiyP5Q
z*2XiF8)kHlij%`i$pdyWgQkl%rm{|T#jZ$EZehX8G@58ClTM;GyI?2Z-!x2p0
zT|MidH>pWb!P3O6IIYa1pzmZ%4sVSDE*FGQ(QtwGZRD}wvSe=RASc7w*4^?8m_={4pVR*>cGE(S?wcM
zrwl)ya*%u%r#rs?uu-7(swwfOCX%8ad>wG(@S&f@>phx@lAC9yk#JMEbN_SGJh;R!3BX@
z7Ou{)boD3m8w6hAEz>Fv!UH{}puVE;gt&Vtgekmfj=ouXG@=NykZ<8r@|6TgJC2xQ
zM&su#c|Zs~4(U=swkt9}3Ptk-SxpDyiVGo%dNCfS0zmq?@UuK5WH>=k`g|#gf*YhW
z`eA_=5mz~DMbv~Q>H9_nMM-SS(FumQifNZ!EjvvbclOj5#uHhZ;62eztYJpn
z*=k7?1%N^{z4Qt8@~L@i(IF@&_KGuaHpcDUxEc_xz106U`%?Pj?DENaT2u3XycDwM2=+U&gg7$ATgDS*AON|
zXOa)D@6LCThCQ05tzl&O2>9mvoZrT7gmw05yzXF?Lku(w>l-J!?Z;brre}GDz*iW?oYJQ(w6O(Cr^Am%h(@&r1J6Woj=*z?DgmnP8vbng
zHRfnI&PRCGAXvZr7qK+DQfkb1br
z#%rlTzO{`2KQ#n={-+6M9OVEgTZ7PZW
zY~1j@So>?*|6e1bxJ4psAa}lK5M+RGKM!r+WbO=+8zD(TWEZ8q!L|cR(SlHaBa!3}MQtbhUKt$=UE6P(9!z>jN=ZavI>G`@apU3~D67+kDa}QdOcowjQ+>RaGF4#We8A
zn-UA}#5tdu7vTvfB>RSj2$(|gBJB4Iao9kYb8}i|-zi1PI&mXM5d|}4^Q_{heZFhB
zYauJDLbO(|S!aqoP(x8znhRJakR}blv?FWMFXa57!Qu_3^bP+k5NJ)OE+oCY^f#)+
z!3rb3<4ksjPBV#=Zv^%KIr;x}plqSJK65+?g5j$(FAu%7(1qrGHRPsXe93D04n
zN9nr<#q^Dy^oKe!Gk-DvLYd-Cr@*$hQ$+w-@+L1a(zzO
z$H8>AQ~N_iQv(J#s;W7foEhp(GbJEDOhRs=@BsrN+utMR>S_!txVeh)W?Co7XPg9G
z%p`{JGR@pP=
zA{%Wlb>QL6A}r?G%=mN?;!WZMSza6NhHRwx#I^T-kXIbDwd7vQVQ`3A%3D-#jGNp
zsCS^i{Jt&Nk}kd@B(>MB1I`=88$Bnt1c6T>w(PlZ4N;G=b08%`X%v6nD^ff4``6Se
zme_wakVmi(km?FiE_$wsClX3O2HE$;@zixc%ua_)l?b?9Ofm~kH`dGoqpIg3jBT}9
zUB*-$@zkO=C9eVu@8z2fhn>%2hO!lHDJz_?&7ZhU4$0)f&WuYf#kVY&W8Tx?wLu05
zoypW|Xi}c2_|o{`24)`mAq=gkt3#7h?Sx3f$Y){8E1z!|0-U8Ka8WTaAcs%Nt-Ks3
zZ73_myxF%Pb&0({_8#MU`O;VP>ou7!sfoOed=-iO;X-H~bKZ{8-N$P-t)B2oE7jV|
zINtpg_|Zl|Yr{A1R=RJrCaOp-35EA`qf0((t6Ur^AEdCREtMYXNKP6>#0jV(`qg|e
zI(fiD(zm(~NGTvwN5=|x?0)#UHYL{aZ(YNw5(-^T%y9yeocy6PW>v|mQ;|?)n(ON-
zK4n?*cW?IuYQ0NlG|ny4SKX|88ld3(LGw5iccQk7xY@BC`csKm0w_4Bb=yHvelV`h7MRACKU%6ySEE(a!B*`8v)%*u2oYZS~|
z%860Gm_Lkg!eT(kR>KPEXi`$(Ce5&;%c=~kTxyre1{S@|EQGQ#?;
zS15Cr5vL?&mo4Y$BGDs=010k(4v7&lf(0ysWDLer9aep$TI&{$#EIDz)3jd4l}25e
zNt%2bXjxX4TdrB(R5>dPv#K`XOPLv`nQ>~&13OxrqRvh#ojO0wsVD!tfCwQoD9wD|
zTCQR@q>7FT3z6b%s!p755*{!&5x=oE5Ti4w-<$PvBduVBOnkN_Jy~YX;Oeu@uaT1{
zXLjAQ+8A&SZOirId&t8uh%Mu34Owoye)J
zE0^O+g>zk+6DSG+Yk4JlbwzLnt|e)RGU-<;c@zG7&|sTOibFajY3U1j$7n+9Fi(Y>
z4pv5Z5KWKzTePeKOAF^w9g&7ehBNnAWQ6)hH#0$HS)7zaO64aizFr?fdb-Rg5wJb+
zi0saR3|Ult*MDe(OfYznmv7*uw~QkakStM)-qMJ*pq6)*
z3U(uL$P?9vYsYruZom%dFyHQp8dBm
zzJ<$O%ARH3;*S5T*rIk4QW@&2wm;G{++`#y-;^s+?E~+P%oZJimfM6yCSf;@#sSBt
zA}|sWhV4K`JW8h2|3?a^n$_ZCM$Rd})i8Jj13gg|BE5Q(eMi
zu)&EWH!uR+irOeRm>%kSmV6vTWh|!N@>O2NcF)bP5jM8C+~|;Mxo3c#)zWK(o&~>3
z1z{>Qm8)ak;_Oq^B(rI4onzWw)jY6S;L^yS8;{?3tRrt!aV+M^IoGWB(l|?_B{R*X
zsWV$=0hS(UE-K5XQ_8K9I4#hRVNV?=GZ$Vw*tYap);3Oo9NuW?Mu+{+3Ie`*ai}B8
zvXa2MnqfSiQBJmH$$mjHN7`zFIo&$FZ4t@3&bDxI>5kwC
z?nfV2YDQC}eM@O(ZhmnIpg`JV)@}mc4@@6Xiy$m^aHdVlqKqOyL~k@7KNCHWOz^1P
zFes~wL-r@^G570ZHB~9ZY_O4m<>xdVolyeY+4~=AXsph;0Sg=OIqjXBxhc8Dqza%;
zJT($I$24i!n2VlOt2|ABX?Rdq8e6;YTXjZGmj
zP6sv)dxi>)ZmEu1pRrP&0lLm&@`R^7HGKkx{t-6pe^2c&>WUE7&l?^-mJCF^<*&duT7aFB<
z6AQuG=i*!QV$2CNrFH2+%g>?C3GNch+WK(CMIRW+RU^3d4{6d3>_oTY^4$vs>E}60
zwIY@5vxSXMNH(mFk@M81@`15%cSSmxA={!5`8x#Ob`rn*ljjx#b-_0JGat|617PyU
z)ZC3LJlV}$IhBZrp#&+UI%4mpgMjp6aO+H875t>M4_J_&aK=CMslvD940oPg`?SR}
zE4)co{j4!I)(n7Zt;j>Fc)(yKNlW9hh76os;IaDr{l6PL$rk}r7bbGNy5?!F61M~w
zk(r=&n)Ws`sBlx>+5pK)p^#YVK|`;&quJf=ga`E&JzLu0ufyLa2E8s#q!>`Nc`h!w
z%EBwsPUQPvF45BCxhFpgw|P({ivvtQ$va18o~~Pp_4y7tI64GkjspqwtiBrcW+~*+
zgDue$Ca}TUn$riI0F2heY<~GSN9hus-g#C@n5@EX!Zo(p5fX-cQ$jr*Y)s1R%T-%y
zTJ&q??{Eq8Cl5u=e4ue|?J{@mOdUAwHIr@u+Dm;I4{Hylel}ZAqjbYYx9p1&?m+rN
z2868?#4~!ZJpWk*XE~8z0xY`k0-qwLkq1iAQnI4u53TSDBau*PR0T-(s4ZzimR}@j
z5m&C_R1~meKzX4P$%GRN^l%FOGDOd%lqMjQdd46Ayut51Wx(_wW(b`$taf0kmI?fg
z7KQGrq>~~EAq>V8%2-4oGRGMdUo?&{e5$8QZsh&E8cKPPwpIvujaKIjVH(G1;`GBFa{$E7EFo-({J=0Z28&i#pj
zw@`lu0yqu-l4TCt-5LGaShPs6Q#o$=D4Z&HDWL@;i6#F$vbB1SMb4g>Qv?eQ7x^1^
zVR|7cYnK&&Uj#+ylx*JK?@(=fufC&bhbwY!92pn%aA!x$q>e`LR(}-P1FFSC3StR|
zaFgYnOM2!bF(G<};$xGE)q*Z;4N-uHFW|PSre6%#
zg$B~0a!v3rOV3uEi$@XF@=_RwZ&r8+A(_uu?{yeAW_EuJ4_&KSedv!niKc@ss-
zw^{xrSI}z2bBr~c-7!(Tlvn7Ee|njz*n<%uZ#h#pG23EyYN4(9eg+j2?vU?0=~<90
zF~ed+dbSi+qs+kFwVbF+cb`O$qa*W0-6Ph&l@(B>#J(BTCqyk;SpM6v5P@9Whtdr*M5rxI4Zt_-qx9Bq*G)!DNc`%X93T55Ad*buGl`;@8?gXyE$PR^R6#rtuB`UWo
zvPecjzY-P}#aFN-6YyIKL+ALj)jTWla<_OrFHNjW7wMyrg-NOQWA1aGdU3=EoLB-E
zGtotuc7>%kpx6V7uiL#c4(NTe8aZ!iL56$MXGj*y{!UV
zjNV0GQ5Ut%4U8kDI*4t?tOiQl*J&9-jGagv{~zy!X{nkrc$;S0(s0@H~JeL5!oKBJ$NP;v0#O%a+Hi#2fVEa4R#n
zcHZc#e7`M`rtxnlOh$$S4UlYxVryT_kpXJ1)ks3ltoZFkd*ct@or)$z5^vhq@Nz>_
z9@=5_(pCC%FH~EV4TE9V6l$CH@8$4O9Yz&uj9+VJRdw^Pr5OOdf(EoTi6&WG2I&~0
zAh6IEl4|ziI0|q@gho>+)XcTMK!C=kF3Ab`^Gr6^96D@X4auD?re`X}@bQM}2qmUd
zqbp!5Cza{H8Fi#1p`g?S(@&*e{LOvTnDeg`eK8#?2ln=9x`@sv5P4y-IfqWr{>RJ*
z_p%iW6_A)gzO$06Prr{uVSA%><}l2iN!wGXtJ-1sf!fT<4OFm`phyN=nF;#>tx8pn
zjyuLYm!`V~)lXysXM54qJwK8*->#$Mr$WPY-3r;FMk3o@ax%PPmiGYJ3B{;Nk~|9l
zBbF^M<_Pomixa(c3JD+GH~4S=#9UC$o3=Q=GuCmhekhH#@ov(KQYuB7CC?|dnR6{3
zXK($k#{wMZE1M+|^RDqa#PGyX3Fbe^Q@Mk{M=-!kxckH084*8(^|_Y;*4Z0XST
z&{ksCiJ7-!O~`f0ZLG}6Ga5{ou(Pz0yHg{(=Pb3U%=Kw0wRsUHcPkWfJ%4r2|INEacYdy!IZu
z6Z$*Xnq?%+>;;?PX<*Kdte%3Ewy&P8+dO^zVTCXKWcTCp`!Bvn62G_n`~UXt+L!0!
zP~@FB$FdVL*gULxVBdN6>9sD6rAlwA=O;*_JK5nFb!s78DyN)vx-;wW^sNu6*ac>M
zDO2NM5GnR=K}zh?Aw$8>W%c{ZrI&AOT}+{ZvD^svg~HZRh8T;TVMgh<8({f8X(Ikhz10c?xAB6$1S~ksx
zj+!yE+j_ngJHm5-F&~?PYtR+(fNxTmNFjfMc4FD*0^y#^B@iClUq}yUyuu$PghMC5
z$cwFb!r4zKw=EjpDcb+{t>A5Q%Ed93a
zKFJ}p^YKT)^8wRus0h4!dt6PSQ#5lwWdJ*0b^Yv_WN)6b$$D*6i0i`yMEo61T(^j(
zJ4ieya8VL@DHQFov5>$K0I4V#plN)$_-H_jiwW@B`nNjAf#oR$X*ko~W`4
zw?VKPoCGY}6bf#Hk#%~TJcasDxWP$(tO5o7{19$8RYgu_fAv}3IEb`FT1|d&g~p%>
zzOmtt&^n?gzwHQsK-w{?WUfdW44}ugvUjgkW@fdZNu#xn$kiG9b=ZA!-ak_-ui%@x
zG-oW2NPWQ^I>4aQt1A+~Rt`}h7kMxT$A8wtp>=*YZOBm?0n3?_$t>G?=O;pTE!@BGzC%2eNp%FsNX|$Z<7h#w
z`tVBdUqi_DnM*t48AVl$`wt6X;1E{#gGn7UA;I0jt)dC@d*urH#a&5#oaZVD5Y`ai
zi%@<2$Rdy7p7jy}zr%4cGOze{qBu&mNKbUCK9YDRuxe7h`}3Xq`0&)RlUvujymyuc
zR*B1BH~h|_#V{rIv(sZZLvPD|l=3iNXxno!;@6LV3gw^mznSEE=a*ySy=4(?E{N^6
zdD%wZds19p5HvDH%E#!;XFuEhAo}{}2R@yPBfh%MkwQ*b(B^=W?jlGbfy8aM`?rSw
zcv53U9GZ_g>x53=$TsP<_ol!Z#z
z@fb#@V$}-Fr*CM>-YX*9Crb|(28PemcWCObO4mkv)c_%v*_%#DrF1;zKj6;suT?7R
z;AWV@{Q3ld@LJbg%j$G57z+-p6G+sK(_$cdTP2L)IL)Sw16Sz<(eD^^^Eb%+iscRL^vRB@1`J)`>uU
z_v_=iYuDupDPP&H-z`TPljhEcLCiy?^ZMRbt4|W%dy&K%1(Gihy|22=*a_04sjVv}
zw?4g3+e^shk`I5Zh+p=K4>OHjUJ*wKJ-YedYkKD|!C>Aar@SSJUA1EeT!+6CeQs!7
zHO;#MP`{ig{@(Q2ofg^+D9zB@u})LR5;;ijy6W`rx@>#7&w1}ocy^_C~cjTk!u@X0b!`
z>Jb(ID*h)fp7~Z>^7uZ57D`0?rFH5IDJ_$5O69`&fod;k!Axuj{sX
z`Q97jXnS_JEU@H!iHL!yDII`#_NK9AeN$&;QXX+`ud`goI+jrbyS>PtT|Xs#*eUTh{C*tnYeQhkB{Q
zMhjd0H}^qIq3oOLC-i(F3vvJf!mXpB$X92nuxXSr;S`G&JEx){SqV`~{&BgIL;<^z
zkV=*fv9*-Y4U=Yz%^uQ7a6PP}P8A>j56HV(=Jb1Lpz0#4QOJc~5y%4Go4sIpv&cne
zfHSfITUrGbM0V7U2UsufLSofyQ)F7t_Sda`d@6m@dx!#XSN2Dwb@nmP<_X|j8hlYz
zaVQRL!zF7P7%52>FMg&kd8Z^mCn}GdSxr;LJC+6U{M1=otSG-yuG6D7i)&lD94LU1
z>UVlQKbDsfv2bUpro(ylzZfNopHqhUu8bv&L*YHJ<|+Zl12Qy0I$U19T!hct$gTvMW=6(X6~og(e)$n
zp6DxTJ`BQ2A${a2JDYBY1tbOA&DS4#*ZDDg^J^%Z-ijbi2wN>zWcJ{Z^yKZyNa!bi-fh##|i_VG15%(0-yIhgAvPH}8CGU$M
z5FY}etCmx2G$A}=Y{7=SZ^Nx@gR@6hEWoOl+2-FMQc1A0r4{aw43M}E>Y>wuNzD#I
zL#vz%qB9`kN;OFCg>xxAN%1-!X*XXg^I);t2C$V9nd@&zrXB8L>G2IJ%JZlPN3+v}7L*^yNI2gR{oV?@V@SGk~Zvm1p{ydY1%>sXol}p}Ux(53nX6W}^b)Hv|Mr7uLV_xs;gDLrJr_RlO
z8!6ZuAfa`NPzB!2Gh~{;38r=a-H+$uoE-}vr4A05mnU{6qpCKwo|pd!^3|=nE6p+-
z8G$)ZMEH#q6&3PjJ|!*Iu2_Q0z^gn4g{&!mw~IVH4vvaJ8^a)PZ+O`S<&ugHsCSdPB;
zz>ZL2QB9a~&A-Hyz6>AtL8cCk+n}(B)pc;Rk
zs>+!o=$yMUcR4j1^w9i|tCvf?ROY#Kp2pB6{dxV}-+wOc~{qU-k;|DQciSBLrr7MHE7uZLX0EQ%eExz)SB
zcYZrJ^hZIx%*=b;X7!zGVA98t1k%-a!7t=H*HNd7<-5>1SCzAIs5;B|YG0MJ_!OaP
zb*zq*0T;C8y)~evNZAT)QziZ$q=BFQX?$lTo-Rfu9~)f*&y8iv5~w8n$|-%m1y7{M
zb9SD5J$`u*+xFnC|d^1@P{OYUseOB`6Vm3JF@%eT(ct4hD$A7n*
zCqjk*xdrb;KemyqdE+flkJfvZ*KnoE?*q?8qx8aMFiX!m0B@dtK$5`76-k>D1Oc#>
z;^n2+Je8cI%KlDKC_Zq2$j0N`eg~Fgh^eocT5_@rb1j3o(-1R@8GHa-MPG!9BU*16
zGiKhw2jFk!%N~~*l`Cf<#eP$%mv*rz#&hJFXfv|bxH~}e*Ox|{4FD8SXlq>-3L3g6GN3JAIA&lSshbD?&ww_Z=RMLw5j4Y
zHiBP0sTJ@8NwNj6lIZibo%w!LPFgW*@ZgH8NpSeRVj>w~AHnCq2Scd;JJ!cK*0nx#
zp4W9PfmNaHnpJS;1+N7^iCpK|S+z^+q27hvR?>D49xu%ddTty&iH8M**c0CLIpX{B=*FZ}aI$2wXb6LVWBD)&c)F-2K5@%of5v&{2itL6L{>Hul@%iu
z-;8WMfa|B7$%>_%DjuvHNXOzDnI^>|35wA~m0AfVJ$0#5Fd91O^zziLIMEnL%3@+D
z