History

Focused add screen and real icons for Codex plugins

Rhys Sullivan committed 2 months ago

Commit 7f2cfa41 parent f0f38a76 files · +243 −174Browse files

Unified · Split

6 files
  1. packages/plugins/mcp/src/api/group.ts +2 −0
  2. packages/plugins/mcp/src/react/AddMcpIntegration.tsx +17 −13
  3. packages/plugins/mcp/src/react/CodexPluginAdd.tsx +149 −0
  4. packages/plugins/mcp/src/react/CodexPluginsSection.tsx +0 −144
  5. packages/plugins/mcp/src/sdk/codex-plugins.ts +68 −13
  6. packages/plugins/mcp/src/sdk/presets.ts +7 −4
packages/plugins/mcp/src/api/group.ts+2−0
@@ -145,6 +145,8 @@ const CodexPluginEntrySchema = Schema.Struct({ · 144 unmodified lines
145 cwd: Schema.optional(Schema.String),145 cwd: Schema.optional(Schema.String),
146 env: Schema.optional(StringMap),146 env: Schema.optional(StringMap),
147 setupHint: Schema.optional(Schema.String),147 setupHint: Schema.optional(Schema.String),
148 /** The plugin's own icon from its local install, as a data URI. */
149 icon: Schema.optional(Schema.String),
148});150});
149 151
150const ListCodexPluginsResponse = Schema.Struct({152const ListCodexPluginsResponse = Schema.Struct({
packages/plugins/mcp/src/react/AddMcpIntegration.tsx+17−13
@@ -41,7 +41,7 @@ import { McpRemoteIntegrationFields } from "./McpRemoteIntegrationFields"; · 40 unmodified lines
41import { McpRequestHeadersEditor } from "./McpRequestHeadersEditor";41import { McpRequestHeadersEditor } from "./McpRequestHeadersEditor";
42import { mcpHeadersFromRows, type McpHeaderRow } from "./request-headers";42import { mcpHeadersFromRows, type McpHeaderRow } from "./request-headers";
43import { mcpAuthMethodInputFromEditorValue, mcpWireAuthInput } from "./auth-method-config";43import { mcpAuthMethodInputFromEditorValue, mcpWireAuthInput } from "./auth-method-config";
44import { CodexPluginsSection } from "./CodexPluginsSection";44import CodexPluginAdd from "./CodexPluginAdd";
45import { parseStdioArgs } from "./stdio-fields";45import { parseStdioArgs } from "./stdio-fields";
46import { isProbableMcpEndpoint } from "./probe-url";46import { isProbableMcpEndpoint } from "./probe-url";
47import { cloudflareNeedsCodemodeOptOut } from "../sdk/cloudflare-codemode";47import { cloudflareNeedsCodemodeOptOut } from "../sdk/cloudflare-codemode";
@@ -169,15 +169,14 @@ export default function AddMcpIntegration(props: { · 121 unmodified lines
169 // Drop stdio presets when stdio is disabled — the caller should have169 // Drop stdio presets when stdio is disabled — the caller should have
170 // already filtered these out, but defence-in-depth.170 // already filtered these out, but defence-in-depth.
171 const preset = rawPreset?.transport === "stdio" && !allowStdio ? undefined : rawPreset;171 const preset = rawPreset?.transport === "stdio" && !allowStdio ? undefined : rawPreset;
172 // A Codex plugin preset is a catalog pointer, not a spawn recipe: it opens172 // A Codex plugin preset is a catalog pointer, not a spawn recipe: it gets
173 // the stdio tab and highlights the matching Codex-plugins card (which173 // its own focused add screen (rendered below, before the generic form),
174 // carries the server-resolved command and availability) instead of174 // fed by the server-side scanner.
175 // prefilling the manual form.
176 const isCodexPreset = isCodexPresetId(preset?.id);175 const isCodexPreset = isCodexPresetId(preset?.id);
177 const isStdioPreset = preset?.transport === "stdio" && !isCodexPreset;176 const isStdioPreset = preset?.transport === "stdio" && !isCodexPreset;
178 177
179 const [transport, setTransport] = useState<"remote" | "stdio">(178 const [transport, setTransport] = useState<"remote" | "stdio">(
180 (isStdioPreset || isCodexPreset) && allowStdio ? "stdio" : "remote",179 isStdioPreset && allowStdio ? "stdio" : "remote",
181 );180 );
182 181
183 // --- Stdio state ---182 // --- Stdio state ---
@@ -398,6 +397,18 @@ export default function AddMcpIntegration(props: { · 214 unmodified lines
398 397
399 // ---- Render ----398 // ---- Render ----
400 399
400 // Placed after every hook so the hook order is identical on all renders;
401 // `isCodexPreset` is fixed for the component's lifetime (route search param).
402 if (isCodexPreset && preset) {
403 return (
404 <CodexPluginAdd
405 presetId={preset.id}
406 onComplete={props.onComplete}
407 onCancel={props.onCancel}
408 />
409 );
410 }
411
401 return (412 return (
402 <div className="flex flex-1 flex-col gap-6">413 <div className="flex flex-1 flex-col gap-6">
403 <div>414 <div>
@@ -525,13 +536,6 @@ export default function AddMcpIntegration(props: { · 121 unmodified lines
525 </>536 </>
526 ) : (537 ) : (
527 <>538 <>
528 {/* Locally installed Codex plugins — one-click presets, with an
529 install hint for entries whose binaries are missing. */}
530 <CodexPluginsSection
531 onComplete={(slug) => props.onComplete(slug)}
532 {...(isCodexPreset && preset ? { highlightId: preset.id } : {})}
533 />
534
535 {/* Stdio form */}539 {/* Stdio form */}
536 <CardStack>540 <CardStack>
537 <CardStackContent className="border-t-0">541 <CardStackContent className="border-t-0">
packages/plugins/mcp/src/react/CodexPluginAdd.tsx+149−0
@@ -0,0 +1,149 @@
1import { useState } from "react";
2import { useAtomValue, useAtomSet } from "@effect/atom-react";
3import * as Exit from "effect/Exit";
4import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
5
6import { Button } from "@executor-js/react/components/button";
7import { FloatActions } from "@executor-js/react/components/float-actions";
8import { integrationsOptimisticAtom } from "@executor-js/react/api/atoms";
9import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys";
10import { addIntegrationErrorMessage } from "@executor-js/react/lib/integration-add";
11
12import { addMcpServer, codexPluginsAtom } from "./atoms";
13
14// ---------------------------------------------------------------------------
15// Focused add screen for one Codex plugin, reached from its catalog preset
16// (e.g. searching "imessage" in the connect dialog). The preset is only a
17// pointer; everything shown here — icon, availability, spawn recipe — comes
18// from the server-side scanner reading the user's local Codex install. One
19// primary action: Add. No transport toggle, no manual command form.
20// ---------------------------------------------------------------------------
21
22export default function CodexPluginAdd(props: {
23 readonly presetId: string;
24 readonly onComplete: (slug?: string) => void;
25 readonly onCancel: () => void;
26}) {
27 const pluginsResult = useAtomValue(codexPluginsAtom);
28 const integrationsResult = useAtomValue(integrationsOptimisticAtom);
29 const doAddServer = useAtomSet(addMcpServer, { mode: "promiseExit" });
30
31 const [adding, setAdding] = useState(false);
32 const [error, setError] = useState<string | null>(null);
33
34 const plugin = AsyncResult.isSuccess(pluginsResult)
35 ? pluginsResult.value.plugins.find((entry) => entry.id === props.presetId)
36 : undefined;
37
38 const added =
39 plugin !== undefined &&
40 AsyncResult.isSuccess(integrationsResult) &&
41 integrationsResult.value.some((integration) => String(integration.slug) === plugin.slug);
42
43 const handleAdd = async () => {
44 if (plugin === undefined) return;
45 setAdding(true);
46 setError(null);
47 const exit = await doAddServer({
48 payload: {
49 transport: "stdio" as const,
50 name: plugin.name,
51 slug: plugin.slug,
52 description: plugin.summary,
53 command: plugin.command,
54 args: [...plugin.args],
55 ...(plugin.cwd !== undefined ? { cwd: plugin.cwd } : {}),
56 ...(plugin.env !== undefined ? { env: { ...plugin.env } } : {}),
57 },
58 reactivityKeys: integrationWriteKeys,
59 });
60 if (Exit.isFailure(exit)) {
61 setError(addIntegrationErrorMessage(exit, plugin.slug, "Failed to add plugin"));
62 setAdding(false);
63 return;
64 }
65 props.onComplete(exit.value.slug);
66 };
67
68 if (!AsyncResult.isSuccess(pluginsResult)) {
69 return (
70 <div className="flex flex-1 flex-col gap-6">
71 <p className="text-[13px] text-muted-foreground">Checking this machine for Codex…</p>
72 </div>
73 );
74 }
75
76 if (plugin === undefined) {
77 return (
78 <div className="flex flex-1 flex-col gap-6">
79 <p className="text-[13px] text-muted-foreground">
80 This Codex plugin was not found on this machine.
81 </p>
82 <FloatActions>
83 <Button type="button" variant="ghost" onClick={() => props.onCancel()}>
84 Back
85 </Button>
86 </FloatActions>
87 </div>
88 );
89 }
90
91 return (
92 <div className="flex flex-1 flex-col gap-6">
93 <div className="flex items-start gap-4">
94 {plugin.icon !== undefined && (
95 <img
96 src={plugin.icon}
97 alt=""
98 className="mt-0.5 size-12 shrink-0 rounded-xl border border-border"
99 />
100 )}
101 <div className="min-w-0">
102 <h1 className="text-xl font-semibold text-foreground">{plugin.name}</h1>
103 <p className="mt-1 text-[13px] text-muted-foreground">{plugin.summary}</p>
104 </div>
105 </div>
106
107 <div className="flex flex-col gap-1 rounded-lg border border-border px-3 py-2.5">
108 <div className="flex items-baseline justify-between gap-3">
109 <span className="font-mono text-[11px] uppercase tracking-wider text-muted-foreground">
110 Status
111 </span>
112 <span className="font-mono text-[11px] text-muted-foreground">
113 {added ? "Added" : plugin.available ? "Ready" : "Requires Codex"}
114 </span>
115 </div>
116 {!plugin.available && plugin.setupHint !== undefined && (
117 <p className="text-[12px] text-muted-foreground">{plugin.setupHint}</p>
118 )}
119 {plugin.available && !added && (
120 <p className="text-[12px] text-muted-foreground">
121 Runs the plugin from your Codex install. Nothing is downloaded.
122 </p>
123 )}
124 </div>
125
126 {error !== null && <p className="text-[12px] text-destructive">{error}</p>}
127
128 <FloatActions>
129 <Button type="button" variant="ghost" onClick={() => props.onCancel()} disabled={adding}>
130 Cancel
131 </Button>
132 {added ? (
133 <Button type="button" onClick={() => props.onComplete(plugin.slug)}>
134 View integration
135 </Button>
136 ) : (
137 <Button
138 type="button"
139 onClick={() => void handleAdd()}
140 disabled={!plugin.available}
141 loading={adding}
142 >
143 Add integration
144 </Button>
145 )}
146 </FloatActions>
147 </div>
148 );
149}
packages/plugins/mcp/src/react/CodexPluginsSection.tsx+0−144
@@ -1,144 +0,0 @@
1import { useState } from "react";
2import { useAtomValue, useAtomSet } from "@effect/atom-react";
3import * as Exit from "effect/Exit";
4import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
5
6import { Button } from "@executor-js/react/components/button";
7import { integrationsOptimisticAtom } from "@executor-js/react/api/atoms";
8import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys";
9import { addIntegrationErrorMessage } from "@executor-js/react/lib/integration-add";
10
11import { addMcpServer, codexPluginsAtom } from "./atoms";
12
13// ---------------------------------------------------------------------------
14// Codex plugins — one-click stdio presets for OpenAI Codex plugins found on
15// this machine (Apple Messages, Computer Use, Computer History, plus anything
16// else in the plugin cache with a local MCP server). Entries whose binaries
17// are missing still render, with the install hint instead of an Add action:
18// the integration stays discoverable on a machine without Codex, and nothing
19// of OpenAI's ships with executor to make that happen.
20// ---------------------------------------------------------------------------
21
22type CodexPluginRow = {
23 readonly id: string;
24 readonly name: string;
25 readonly summary: string;
26 readonly available: boolean;
27 readonly slug: string;
28 readonly command: string;
29 readonly args: readonly string[];
30 readonly cwd?: string;
31 readonly env?: Readonly<Record<string, string>>;
32 readonly setupHint?: string;
33};
34
35export function CodexPluginsSection(props: {
36 readonly onComplete: (slug: string) => void;
37 /** Card to emphasise — set when the user arrived via a Codex catalog
38 * preset (e.g. searched "imessage" in the connect dialog). */
39 readonly highlightId?: string;
40}) {
41 const pluginsResult = useAtomValue(codexPluginsAtom);
42 const integrationsResult = useAtomValue(integrationsOptimisticAtom);
43 const doAddServer = useAtomSet(addMcpServer, { mode: "promiseExit" });
44
45 const [addingId, setAddingId] = useState<string | null>(null);
46 const [errors, setErrors] = useState<Readonly<Record<string, string>>>({});
47
48 if (!AsyncResult.isSuccess(pluginsResult)) return null;
49 const plugins: readonly CodexPluginRow[] = pluginsResult.value.plugins;
50 if (plugins.length === 0) return null;
51
52 const existingSlugs = new Set(
53 AsyncResult.isSuccess(integrationsResult)
54 ? integrationsResult.value.map((integration) => String(integration.slug))
55 : [],
56 );
57
58 const handleAdd = async (plugin: CodexPluginRow) => {
59 setAddingId(plugin.id);
60 setErrors((prev) => ({ ...prev, [plugin.id]: "" }));
61 const exit = await doAddServer({
62 payload: {
63 transport: "stdio" as const,
64 name: plugin.name,
65 slug: plugin.slug,
66 description: plugin.summary,
67 command: plugin.command,
68 args: [...plugin.args],
69 ...(plugin.cwd !== undefined ? { cwd: plugin.cwd } : {}),
70 ...(plugin.env !== undefined ? { env: { ...plugin.env } } : {}),
71 },
72 reactivityKeys: integrationWriteKeys,
73 });
74 if (Exit.isFailure(exit)) {
75 setErrors((prev) => ({
76 ...prev,
77 [plugin.id]: addIntegrationErrorMessage(exit, plugin.slug, "Failed to add plugin"),
78 }));
79 setAddingId(null);
80 return;
81 }
82 props.onComplete(exit.value.slug);
83 };
84
85 return (
86 <div className="flex flex-col gap-2">
87 <div className="flex items-baseline justify-between">
88 <span className="font-mono text-[11px] uppercase tracking-wider text-muted-foreground">
89 Codex plugins
90 </span>
91 <span className="font-mono text-[11px] text-muted-foreground">
92 {plugins.filter((plugin) => plugin.available).length}/{plugins.length} available
93 </span>
94 </div>
95 <div className="divide-y divide-border rounded-lg border border-border">
96 {plugins.map((plugin) => {
97 const added = existingSlugs.has(plugin.slug);
98 const error = errors[plugin.id];
99 const highlighted = plugin.id === props.highlightId;
100 return (
101 <div
102 key={plugin.id}
103 className={`flex flex-col gap-1 px-3 py-2.5 ${highlighted ? "bg-muted/40" : ""}`}
104 >
105 <div className="flex items-center justify-between gap-3">
106 <div className="min-w-0">
107 <p className="truncate text-sm font-medium text-foreground">{plugin.name}</p>
108 <p className="mt-0.5 text-[12px] text-muted-foreground">{plugin.summary}</p>
109 </div>
110 {added ? (
111 <span className="shrink-0 font-mono text-[11px] text-muted-foreground">
112 Added
113 </span>
114 ) : plugin.available ? (
115 <Button
116 type="button"
117 variant="outline"
118 size="sm"
119 onClick={() => void handleAdd(plugin)}
120 loading={addingId === plugin.id}
121 disabled={addingId !== null}
122 className="shrink-0"
123 >
124 Add
125 </Button>
126 ) : (
127 <span className="shrink-0 font-mono text-[11px] text-muted-foreground">
128 Requires Codex
129 </span>
130 )}
131 </div>
132 {!plugin.available && plugin.setupHint !== undefined && (
133 <p className="text-[11px] text-muted-foreground">{plugin.setupHint}</p>
134 )}
135 {error !== undefined && error.length > 0 && (
136 <p className="text-[12px] text-destructive">{error}</p>
137 )}
138 </div>
139 );
140 })}
141 </div>
142 </div>
143 );
144}
packages/plugins/mcp/src/sdk/codex-plugins.ts+68−13
@@ -40,6 +40,9 @@ export interface CodexPluginEntry { · 39 unmodified lines
40 readonly env?: Readonly<Record<string, string>>;40 readonly env?: Readonly<Record<string, string>>;
41 /** Shown when `available` is false. */41 /** Shown when `available` is false. */
42 readonly setupHint?: string;42 readonly setupHint?: string;
43 /** The plugin's own icon from its local install, as a data URI. Read at
44 * runtime from the user's disk — never shipped with executor. */
45 readonly icon?: string;
43}46}
44 47
45/** The Codex Computer Use client binary — the stable, unversioned entry point48/** The Codex Computer Use client binary — the stable, unversioned entry point
@@ -75,6 +78,7 @@ const PluginManifest = Schema.Struct({ · 29 unmodified lines
75 Schema.Struct({78 Schema.Struct({
76 displayName: Schema.optional(Schema.String),79 displayName: Schema.optional(Schema.String),
77 shortDescription: Schema.optional(Schema.String),80 shortDescription: Schema.optional(Schema.String),
81 logo: Schema.optional(Schema.String),
78 }),82 }),
79 ),83 ),
80 description: Schema.optional(Schema.String),84 description: Schema.optional(Schema.String),
@@ -159,6 +163,48 @@ const sanitizeId = (value: string): string => · 78 unmodified lines
159 .replace(/[^a-z0-9]+/g, "-")163 .replace(/[^a-z0-9]+/g, "-")
160 .replace(/^-+|-+$/g, "");164 .replace(/^-+|-+$/g, "");
161 165
166/** Bound on inlined icon bytes. The bundled icons run 100KB–1MB; anything
167 * past this is not worth inlining into a list response for a 20px avatar. */
168const MAX_ICON_BYTES = 1_000_000;
169
170const ICON_MIME: Readonly<Record<string, string>> = {
171 ".png": "image/png",
172 ".jpg": "image/jpeg",
173 ".jpeg": "image/jpeg",
174 ".webp": "image/webp",
175 ".svg": "image/svg+xml",
176};
177
178/** The plugin's own icon as a data URI, read from the user's local install. */
179const readIconDataUri = (file: string): string | undefined =>
180 tryOrElse(() => {
181 const mime = ICON_MIME[path.extname(file).toLowerCase()];
182 if (mime === undefined) return undefined;
183 if (fs.statSync(file).size > MAX_ICON_BYTES) return undefined;
184 return `data:${mime};base64,${fs.readFileSync(file).toString("base64")}`;
185 }, undefined);
186
187/** Icon for a curated plugin: its cache entry (any source, newest version)
188 * declares a `logo` path in the manifest. The curated SPAWN target is the
189 * stable client binary, but the icon only exists in the cache. */
190const curatedIconDataUri = (codexHome: string, pluginName: string): string | undefined => {
191 const cacheDir = path.join(codexHome, "plugins", "cache");
192 for (const sourceName of listDirs(cacheDir)) {
193 const pluginDir = path.join(cacheDir, sourceName, pluginName);
194 const version = [...listDirs(pluginDir)].sort(compareVersionsDesc)[0];
195 if (version === undefined) continue;
196 const versionDir = path.join(pluginDir, version);
197 const manifest = Option.getOrUndefined(
198 decodeManifestJson(readText(path.join(versionDir, ".codex-plugin", "plugin.json"))),
199 );
200 const logo = manifest?.interface?.logo;
201 if (logo === undefined) continue;
202 const icon = readIconDataUri(path.resolve(versionDir, logo));
203 if (icon !== undefined) return icon;
204 }
205 return undefined;
206};
207
162// ---------------------------------------------------------------------------208// ---------------------------------------------------------------------------
163// Scan209// Scan
164// ---------------------------------------------------------------------------210// ---------------------------------------------------------------------------
@@ -192,6 +238,10 @@ const scanCachedPlugin = ( · 27 unmodified lines
192 manifest.interface?.shortDescription ??238 manifest.interface?.shortDescription ??
193 manifest.description?.split("\n")[0] ??239 manifest.description?.split("\n")[0] ??
194 `Local MCP server from the Codex plugin "${manifest.name}".`;240 `Local MCP server from the Codex plugin "${manifest.name}".`;
241 const icon =
242 manifest.interface?.logo === undefined
243 ? undefined
244 : readIconDataUri(path.resolve(versionDir, manifest.interface.logo));
195 245
196 const localServers = Object.entries(servers.mcpServers).flatMap(([serverKey, spec]) =>246 const localServers = Object.entries(servers.mcpServers).flatMap(([serverKey, spec]) =>
197 spec.command !== undefined && spec.type !== "http" && spec.url === undefined247 spec.command !== undefined && spec.type !== "http" && spec.url === undefined
@@ -226,6 +276,7 @@ const scanCachedPlugin = ( · 28 unmodified lines
226 // the integration after adding it.276 // the integration after adding it.
227 env: { CODEX_HOME: codexHome },277 env: { CODEX_HOME: codexHome },
228 ...(available ? {} : { setupHint: CODEX_SETUP_HINT }),278 ...(available ? {} : { setupHint: CODEX_SETUP_HINT }),
279 ...(icon === undefined ? {} : { icon }),
229 };280 };
230 });281 });
231};282};
@@ -248,19 +299,23 @@ export const scanCodexPlugins = (options?: { · 16 unmodified lines
248 const client = clientBinaryPath(codexHome);299 const client = clientBinaryPath(codexHome);
249 const clientAvailable = isExecutableFile(client);300 const clientAvailable = isExecutableFile(client);
250 301
251 const curated: readonly CodexPluginEntry[] = CURATED_CODEX_PLUGINS.map((entry) => ({302 const curated: readonly CodexPluginEntry[] = CURATED_CODEX_PLUGINS.map((entry) => {
252 id: entry.id,303 const icon = curatedIconDataUri(codexHome, entry.pluginName);
253 name: entry.name,304 return {
254 summary: entry.summary,305 id: entry.id,
255 available: clientAvailable,306 name: entry.name,
256 slug: entry.slug,307 summary: entry.summary,
257 source: "curated" as const,308 available: clientAvailable,
258 command: client,309 slug: entry.slug,
259 args: entry.args,310 source: "curated" as const,
260 cwd: path.join(codexHome, "computer-use"),311 command: client,
261 env: { CODEX_HOME: codexHome },312 args: entry.args,
262 ...(clientAvailable ? {} : { setupHint: CODEX_SETUP_HINT }),313 cwd: path.join(codexHome, "computer-use"),
263 }));314 env: { CODEX_HOME: codexHome },
315 ...(clientAvailable ? {} : { setupHint: CODEX_SETUP_HINT }),
316 ...(icon === undefined ? {} : { icon }),
317 };
318 });
264 319
265 const cacheDir = path.join(codexHome, "plugins", "cache");320 const cacheDir = path.join(codexHome, "plugins", "cache");
266 const scanned = listDirs(cacheDir).flatMap((sourceName) => {321 const scanned = listDirs(cacheDir).flatMap((sourceName) => {
packages/plugins/mcp/src/sdk/presets.ts+7−4
@@ -28,14 +28,17 @@ export interface McpStdioPreset { · 27 unmodified lines
28export type McpPreset = McpRemotePreset | McpStdioPreset;28export type McpPreset = McpRemotePreset | McpStdioPreset;
29 29
30// Codex plugin presets — searchable catalog entries ("imessage", "computer30// Codex plugin presets — searchable catalog entries ("imessage", "computer
31// use", …) for the plugins the add form's Codex-plugins section installs.31// use", …). `command` is deliberately empty: the real spawn recipe is
32// `command` is deliberately empty: the real spawn recipe is machine-specific32// machine-specific and comes from the server-side scanner
33// and comes from the server-side scanner (`codex-plugins.ts`); picking one of33// (`codex-plugins.ts`); picking one of these opens the focused Codex add
34// these routes to the stdio tab with the matching card highlighted.34// screen, which shows the plugin's own locally installed icon. The list icon
35// is the OpenAI logo — the shared provenance of all three — because a static
36// preset cannot reach the machine-local icon files.
35const codexPluginPresets: readonly McpStdioPreset[] = CURATED_CODEX_PLUGINS.map((plugin) => ({37const codexPluginPresets: readonly McpStdioPreset[] = CURATED_CODEX_PLUGINS.map((plugin) => ({
36 id: plugin.id,38 id: plugin.id,
37 name: plugin.name,39 name: plugin.name,
38 summary: plugin.summary,40 summary: plugin.summary,
41 icon: "https://integrations.sh/logo/openai.com",
39 family: "codex",42 family: "codex",
40 transport: "stdio",43 transport: "stdio",
41 command: "",44 command: "",