import { useCallback, useEffect, useState } from "react"; import { Button, Icon, SettingsCard, StatusBadge, TextField } from "@nodedc/ui-react"; interface FoundryAgentDevice { id: string; name: string; createdAt: string; lastUsedAt: string | null; lastCheckAt: string | null; revokedAt: string | null; } interface FoundryAgent { id: string; name: string; avatarUrl: string | null; status: "active" | "revoked"; devices: FoundryAgentDevice[]; deviceCount: number; lastUsedAt: string | null; lastCheckAt: string | null; createdAt: string; updatedAt: string; } interface SetupCommand { command: string; expiresAt: string; } async function api(pathname: string, init?: RequestInit): Promise { const response = await fetch(pathname, { cache: "no-store", ...init, headers: { accept: "application/json", ...(init?.body ? { "content-type": "application/json" } : {}), ...init?.headers, }, }); const payload = await response.json().catch(() => ({})); if (!response.ok || payload?.ok === false) throw new Error(payload?.error || `foundry_agent_api_${response.status}`); return payload as T; } function formatMoment(value: string | null) { if (!value) return "—"; const date = new Date(value); return Number.isFinite(date.getTime()) ? date.toLocaleString("ru-RU") : value; } export function FoundryCodexAgentSettings() { const [agents, setAgents] = useState([]); const [newAgentName, setNewAgentName] = useState("Foundry Codex"); const [draftNames, setDraftNames] = useState>({}); const [setupByAgent, setSetupByAgent] = useState>({}); const [loading, setLoading] = useState(true); const [pending, setPending] = useState(""); const [message, setMessage] = useState(""); const [error, setError] = useState(""); const load = useCallback(async () => { setLoading(true); setError(""); try { const payload = await api<{ agents: FoundryAgent[] }>("/api/foundry-agent-api/agents"); setAgents(payload.agents); setDraftNames(Object.fromEntries(payload.agents.map((agent) => [agent.id, agent.name]))); } catch (loadError) { setError(loadError instanceof Error ? loadError.message : "foundry_agent_load_failed"); } finally { setLoading(false); } }, []); useEffect(() => { void load(); }, [load]); const run = async (key: string, action: () => Promise) => { setPending(key); setError(""); setMessage(""); try { await action(); } catch (actionError) { setError(actionError instanceof Error ? actionError.message : "foundry_agent_operation_failed"); } finally { setPending(""); } }; const createAgent = () => run("create", async () => { const payload = await api<{ agent: FoundryAgent }>("/api/foundry-agent-api/agents", { method: "POST", body: JSON.stringify({ name: newAgentName.trim() || "Foundry Codex" }), }); setAgents((current) => [payload.agent, ...current]); setDraftNames((current) => ({ ...current, [payload.agent.id]: payload.agent.name })); setMessage("Foundry Agent создан. Теперь можно получить одноразовую setup-команду."); }); const saveAgent = (agent: FoundryAgent) => run(`save:${agent.id}`, async () => { const payload = await api<{ agent: FoundryAgent }>(`/api/foundry-agent-api/agents/${encodeURIComponent(agent.id)}`, { method: "PATCH", body: JSON.stringify({ name: draftNames[agent.id]?.trim() || agent.name }), }); setAgents((current) => current.map((item) => item.id === agent.id ? payload.agent : item)); setMessage("Имя агента сохранено."); }); const revokeAgent = (agent: FoundryAgent) => run(`revoke:${agent.id}`, async () => { const payload = await api<{ agent: FoundryAgent }>(`/api/foundry-agent-api/agents/${encodeURIComponent(agent.id)}/revoke`, { method: "POST", body: "{}", }); setAgents((current) => current.map((item) => item.id === agent.id ? payload.agent : item)); setSetupByAgent((current) => { const next = { ...current }; delete next[agent.id]; return next; }); setMessage("Agent отозван. Foundry и Ontology credentials этого агента больше не принимаются."); }); const issueSetup = (agent: FoundryAgent) => run(`setup:${agent.id}`, async () => { const payload = await api<{ install: SetupCommand }>(`/api/foundry-agent-api/agents/${encodeURIComponent(agent.id)}/setup-code`, { method: "POST", body: "{}", }); setSetupByAgent((current) => ({ ...current, [agent.id]: payload.install })); setMessage("Одноразовая команда выпущена. Она установит Foundry MCP и отдельный read-only Ontology MCP."); }); const copySetup = async (agentId: string) => { const command = setupByAgent[agentId]?.command; if (!command) return; await navigator.clipboard.writeText(command); setMessage("Setup-команда скопирована."); }; return (
NODE.DC / SOURCE-FREE DEVELOPMENT

Codex Agent API

Foundry + Ontology

Одна setup-команда добавляет в Codex два независимых MCP: полный текущий Foundry contour и отдельную read-only Ontology. Sharing, роли и межпользовательская видимость в этот срез не входят.

{error ?
{error}
: null} {message ?
{message}
: null}
setNewAgentName(event.target.value)} />
{loading ? Загружаю agents… : null} {!loading && agents.length === 0 ? Агенты ещё не созданы. : null} {agents.map((agent) => { const setup = setupByAgent[agent.id]; const active = agent.status === "active"; return ( {active ? "Активен" : "Отозван"}} >
setDraftNames((current) => ({ ...current, [agent.id]: event.target.value }))} />
{setup ? (
Одноразовая setup-командаДействует до {formatMoment(setup.expiresAt)}. После выполнения полностью перезапустите Codex Desktop.
{setup.command}
) : null}
); })}
); }