Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "exeora",
"description": "Remote Exeora tools and guided workflows for Claude.",
"owner": { "name": "Exeora", "email": "hello@exeora.dev" },
"plugins": [
{
"name": "exeora-workspaces",
"source": "./plugins/exeora-claude",
"description": "Exeora's remote MCP connector and guided workflows for Claude."
}
]
}
18 changes: 18 additions & 0 deletions .cursor-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "exeora",
"owner": {
"name": "Exeora",
"email": "hello@exeora.dev"
},
"metadata": {
"description": "GrokBot Cursor \u2014 Exeora remote MCP and guided skills.",
"version": "1.0.0"
},
"plugins": [
{
"name": "grokbot-cursor",
"source": "plugins/exeora-cursor",
"description": "GrokBot Cursor \u2014 remote Exeora tools and skills."
}
]
}
15 changes: 15 additions & 0 deletions .github/plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "exeora-copilot",
"owner": {
"name": "Exeora",
"email": "hello@exeora.dev"
},
"plugins": [
{
"name": "exeora-copilot",
"source": "./plugins/exeora-copilot",
"description": "Exeora remote MCP and skills for GitHub Copilot.",
"version": "1.0.0"
}
]
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ test-results/

# Working notes kept locally, not published with the source tree.
context.md
.artifacts/
12 changes: 12 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ Exeora Workspace is a dashboard build entry at `/dashboard/mcp-panel` and reuses

Edit the maintained files and bump `plugins/exeora/plugin.json`'s version when releasing package changes. Copy updated square mark PNGs from the generated brand assets into `plugins/exeora/assets/` when the branding changes. Archive timestamps and ordering are fixed, so unchanged source produces the same checksum. The builder reads only the plugin resources, the license and the marketplace entry; credentials and application source are not packaged. Publishing to OpenAI's public directory remains a separate verified-publisher submission, MCP connection, review and publication step. See [Package your plugin](https://developers.openai.com/plugins/build/plugins) and [Submit and publish](https://developers.openai.com/plugins/deploy/submission).

### Building the Claude web plugin

`plugins/exeora-claude/` is a separate Claude web package with `.claude-plugin/plugin.json`, root `.mcp.json`, three skills and a listing PNG. `bun run plugin:build:claude` generates identical ZIP bytes as `exeora-claude.plugin` and `exeora-claude-plugin.zip`, filename-specific checksums and metadata. Landing builds generate all platform packages independently. `.claude-plugin/marketplace.json` provides repository installation for Claude without changing the ChatGPT catalog. Bump only the Claude manifest version for Claude package changes. See [Claude web packaging and acceptance](docs/claude-web-plugin.md) for verified schema, OAuth setup, limits and separate manual web acceptance. CLI validation proves syntax only; it does not prove Claude web installation or OAuth.

### Checks

```bash
Expand Down Expand Up @@ -172,3 +176,11 @@ One-time setup for the workflow:
- Keep changes focused; match the tone and structure of nearby code.
- Run `bun run check`, `bun run typecheck` and `bun run test` before opening a PR. The dependency audit workflow also checks Bun and Rust advisories on dependency changes and nightly.
- Security issues: email hello@exeora.dev (see [SECURITY.md](./SECURITY.md)), do not open a public issue.

### Building the other plugin distributions

`bun run plugin:build:cursor`, `bun run plugin:build:agent` and `bun run plugin:build:copilot` independently generate GrokBot Cursor, Agent Plugin and GitHub Copilot ZIPs, checksums and metadata. Their maintained folders are `plugins/exeora-cursor`, `plugins/exeora-agent` and `plugins/exeora-copilot`; bump the relevant manifest version when releasing changes, and keep the Copilot catalog version in sync. The existing ChatGPT and Claude commands remain independent. Every landing build produces all five distributions, and `/docs/plugins/` links to their installation guides.

Agent Plugin and Copilot validate root JSON documents against the vendored official Agent Plugins 1.0.0 schemas with Ajv Draft 2020-12. The pure package forbids all provider extensions/metadata/directories; Copilot adds adapted instructions and an ordinary README logo, without invented host logo fields. Cursor has its own `.cursor-plugin/marketplace.json`; Copilot has `.github/plugin/marketplace.json`. Neither changes the OpenAI or Claude catalog. See [distribution decisions and acceptance limits](docs/plugin-distributions.md). Package checks do not verify installation, account OAuth or public catalog availability.

Run `bun run plugin:check:submission` for local preparation checks on the unchanged OpenAI package. It does not upload, register, authorize or publish a plugin. See [ChatGPT public distribution](docs/chatgpt-plugin-distribution.md) for the portal workflow and external blockers. Experimental versioned artifacts stay private and are not a workaround for the imported-MCP desktop restriction.
15 changes: 15 additions & 0 deletions apps/gateway/src/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,21 @@ function withAssetHeaders(response: Response, pathname: string, requestUrl: URL)
options.cacheControl = "public, max-age=31536000, immutable";
}
const result = withSecurityHeaders(response, options);
// Static Assets has no MIME mapping for .plugin; all these downloads are ZIP archives.
if (
(response.ok || response.status === 304) &&
(pathname === "/downloads/exeora-claude.plugin" ||
pathname === "/downloads/exeora-claude-plugin.zip" ||
pathname === "/downloads/grokbot-cursor-plugin.zip" ||
pathname === "/downloads/exeora-agent-plugin.zip" ||
pathname === "/downloads/exeora-copilot-plugin.zip")
) {
result.headers.set("Content-Type", "application/zip");
result.headers.set(
"Content-Disposition",
`attachment; filename="${pathname.split("/").at(-1)}"`,
);
}
// These are public static modules/fonts, fetched by the isolated MCP Apps iframe.
if (pathname.startsWith("/dashboard/assets/") || pathname.startsWith("/fonts/")) {
result.headers.set("Access-Control-Allow-Origin", "*");
Expand Down
107 changes: 107 additions & 0 deletions apps/gateway/src/assets.workers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,69 @@ async function assetPathFromShell(): Promise<string> {
}

describe("static files", () => {
it.each([
{
file: "grokbot-cursor-plugin",
name: "grokbot-cursor",
label: "GrokBot Cursor",
route: "grokbot-cursor",
},
{
file: "exeora-agent-plugin",
name: "exeora-agent-plugin",
label: "Agent Plugin",
route: "agent-plugin",
},
{
file: "exeora-copilot-plugin",
name: "exeora-copilot",
label: "GitHub Copilot",
route: "copilot-plugin",
},
])(
"serves $label with matching metadata/checksum and preserves conditional downloads",
async ({ file, name, label, route }) => {
const path = `/downloads/${file}.zip`;
const response = await get(path);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain("application/zip");
expect(response.headers.get("content-disposition")).toBe(
`attachment; filename="${file}.zip"`,
);
const bytes = new Uint8Array(await response.arrayBuffer());
expect([...bytes.slice(0, 4)]).toEqual([0x50, 0x4b, 0x03, 0x04]);
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
const sha256 = [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
expect(await (await get(`/downloads/${file}.json`)).json()).toMatchObject({
name,
displayName: label,
bytes: bytes.length,
sha256,
download: path,
});
expect(await (await get(`${path}.sha256`)).text()).toBe(`${sha256} ${file}.zip\n`);
const etag = response.headers.get("etag");
expect(etag).toBeTruthy();
const cached = await get(path, { "If-None-Match": etag as string });
expect(cached.status).toBe(304);
expect(cached.headers.get("content-type")).toContain("application/zip");
expect((await cached.arrayBuffer()).byteLength).toBe(0);
expect(await (await get(`/docs/${route}/`)).text()).toContain(`href="${path}"`);
},
);
it("lists five production packages without exposing the private experimental example", async () => {
const html = await (await get("/docs/plugins/")).text();
for (const name of [
"ChatGPT",
"Claude web",
"GrokBot Cursor",
"Agent Plugin",
"GitHub Copilot",
])
expect(html).toContain(name);
expect(html).not.toContain("combined-experimental");
expect((await get("/downloads/exeora-combined-experimental.zip")).status).toBe(404);
});
it("serves the plugin download as a ZIP with matching package details", async () => {
const response = await get("/downloads/exeora-plugin.zip");
expect(response.status).toBe(200);
Expand All @@ -41,6 +104,50 @@ describe("static files", () => {
'href="/downloads/exeora-plugin.zip"',
);
});
it("serves identical Claude ZIP and plugin downloads with matching digests and web instructions", async () => {
const zip = await get("/downloads/exeora-claude-plugin.zip");
const plugin = await get("/downloads/exeora-claude.plugin");
expect(zip.status).toBe(200);
expect(plugin.status).toBe(200);
expect(zip.headers.get("content-type")).toContain("application/zip");
expect(plugin.headers.get("content-type")).toContain("application/zip");
expect(plugin.headers.get("content-disposition")).toBe(
'attachment; filename="exeora-claude.plugin"',
);
expect(zip.headers.get("content-disposition")).toBe(
'attachment; filename="exeora-claude-plugin.zip"',
);
const bytes = new Uint8Array(await zip.arrayBuffer());
expect(new Uint8Array(await plugin.arrayBuffer())).toEqual(bytes);
expect([...bytes.slice(0, 4)]).toEqual([0x50, 0x4b, 0x03, 0x04]);
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
const sha256 = [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
expect(await (await get("/downloads/exeora-claude-plugin.json")).json()).toMatchObject({
name: "exeora-workspaces",
bytes: bytes.length,
sha256,
download: "/downloads/exeora-claude.plugin",
zip: "/downloads/exeora-claude-plugin.zip",
});
for (const filename of ["exeora-claude.plugin", "exeora-claude-plugin.zip"]) {
expect(await (await get(`/downloads/${filename}.sha256`)).text()).toBe(
`${sha256} ${filename}\n`,
);
}
const etag = plugin.headers.get("etag");
expect(etag).toBeTruthy();
const revalidated = await get("/downloads/exeora-claude.plugin", {
"If-None-Match": etag as string,
});
expect(revalidated.status).toBe(304);
expect(revalidated.headers.get("content-type")).toContain("application/zip");
expect((await revalidated.arrayBuffer()).byteLength).toBe(0);
const html = await (await get("/docs/claude-plugin/")).text();
expect(html).toContain('href="/downloads/exeora-claude.plugin"');
expect(html).toContain('href="/downloads/exeora-claude-plugin.zip"');
expect(html).toContain("Upload plugin");
expect(html).toContain('href="/docs/plugin/"');
});
it("allows the MCP sandbox to fetch public modules and fonts without opening HTML to framing", async () => {
const script = await get(await assetPathFromShell());
expect(script.headers.get("access-control-allow-origin")).toBe("*");
Expand Down
6 changes: 5 additions & 1 deletion apps/web/landing/src/components/Footer.astro
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ const links = [
{ href: "/docs/", label: "Docs" },
{ href: "/docs/tools/", label: "Tools" },
{ href: "/docs/extension/", label: "Chrome" },
{ href: "/docs/plugin/", label: "Plugin" },
{ href: "/docs/plugin/", label: "ChatGPT plugin" },
{ href: "/docs/claude-plugin/", label: "Claude plugin" },
{ href: "/docs/grokbot-cursor/", label: "GrokBot Cursor" },
{ href: "/docs/agent-plugin/", label: "Agent Plugin" },
{ href: "/docs/copilot-plugin/", label: "GitHub Copilot" },
{ href: "/#security", label: "Security" },
{ href: "/#faq", label: "FAQ" },
{ href: "/brand/", label: "Brand" },
Expand Down
10 changes: 8 additions & 2 deletions apps/web/landing/src/components/Hero.astro
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,20 @@ const promises = [
</ul>

<p class="text-body-md text-foreground-muted mt-5 text-center">
If your client speaks MCP over HTTP with OAuth, it connects.{" "}
Remote MCP setup depends on your client's OAuth support and installation path.{" "}
<a href="/docs/clients/" class="text-foreground underline-offset-2 hover:underline">
Setup for each client
</a>
{" · "}
<a href="/docs/plugin/" class="text-foreground underline-offset-2 hover:underline">
Download the Exeora plugin
ChatGPT plugin
</a>
{" · "}
<a href="/docs/claude-plugin/" class="text-foreground underline-offset-2 hover:underline">
Claude web plugin
</a>
{" · "}
<a href="#plugin-installation" class="text-foreground underline-offset-2 hover:underline">Plugin compatibility and downloads</a>
</p>
</div>
</section>
73 changes: 73 additions & 0 deletions apps/web/landing/src/components/PluginCompatibility.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
const clients = [
{
label: "ChatGPT and Codex",
steps:
"Extract the ZIP, add the folder as a local marketplace in Codex, then install in ChatGPT desktop. This MCP ZIP is Desktop-only when imported; public ChatGPT web needs a published directory listing.",
guide: "/docs/plugin/",
download: "/downloads/exeora-plugin.zip",
action: "Download ChatGPT desktop ZIP",
},
{
label: "Claude web",
steps:
"In Claude web, choose Add → Upload plugin and select the .plugin or ZIP. Then add or connect Exeora in Connectors and sign in. Plugin upload and account authorization are separate steps; plan and workspace policy apply.",
guide: "/docs/claude-plugin/",
download: "/downloads/exeora-claude.plugin",
action: "Download Claude web plugin",
},
{
label: "GrokBot Cursor",
steps:
"Cursor: extract the ZIP into the local plugins folder and reload, then connect Exeora. Grok Bot: add an available plugin from its catalog. Custom ZIP import into Grok Bot is not documented; this download does not establish a catalog listing.",
guide: "/docs/grokbot-cursor/",
download: "/downloads/grokbot-cursor-plugin.zip",
action: "Download GrokBot Cursor ZIP",
},
{
label: "GitHub Copilot",
steps:
"Copilot CLI: extract and install the plugin folder. Copilot app and VS Code: add the repository marketplace and install its Copilot entry. Complete Exeora sign-in in the host you use; installation in one surface does not verify another.",
guide: "/docs/copilot-plugin/",
download: "/downloads/exeora-copilot-plugin.zip",
action: "Download GitHub Copilot ZIP",
},
{
label: "Agent Plugin",
steps:
"Use a client that supports Agent Plugins 1.0 skills and remote MCP. Follow that client's import and account connection steps. The pure package contains no provider extensions; support for the format does not guarantee every host feature.",
guide: "/docs/agent-plugin/",
download: "/downloads/exeora-agent-plugin.zip",
action: "Download Agent Plugin ZIP",
},
];
---

<section id="plugin-installation" aria-labelledby="plugin-installation-title" class="border-border-subtle scroll-mt-24 border-t px-6 py-12 sm:py-16">
<div class="mx-auto max-w-5xl">
<h2 id="plugin-installation-title" class="text-foreground text-2xl font-semibold tracking-tight sm:text-3xl">Plugin compatibility and installation</h2>
<p class="text-body-lg text-foreground-muted mt-3 max-w-3xl">Choose the package and installation path for your client before downloading. Each package includes Exeora workflows and a remote connection. You still sign in with your account and choose the projects it can access.</p>

<div data-chatgpt-import-notice class="border-accent bg-surface mt-7 border-l-4 py-4 pl-5 pr-4">
<h3 class="text-title-lg text-foreground">ChatGPT web needs a published plugin</h3>
<p class="text-body-md text-foreground-muted mt-2">Importing a ZIP with MCP into ChatGPT makes it <strong class="text-foreground">Desktop-only</strong>, even when the MCP server uses remote HTTPS. A successful ZIP import does not enable that connection on web.</p>
<p class="text-body-md text-foreground-muted mt-2">For public ChatGPT web use, Exeora must connect its MCP in the provider portal, verify its domain, pass scans and review, then publish to the directory after approval. Users install the published listing without manually registering a URL. OAuth sign-in may still be required. This download is not an approved public listing; private owner connections are not a public substitute.</p>
<p class="text-body-md mt-3"><a href="https://learn.chatgpt.com/docs/enterprise/plugin-management#desktop-only-plugins" class="text-foreground underline underline-offset-2">OpenAI import limits</a>{" · "}<a href="https://developers.openai.com/plugins/deploy/submission" class="text-foreground underline underline-offset-2">Public publication process</a></p>
</div>

<ul class="mt-6">
{clients.map((client) => (
<li data-plugin-client class="border-border-subtle grid gap-3 border-b py-6 sm:grid-cols-[12rem_1fr] sm:gap-8">
<h3 class="text-title-lg text-foreground">{client.label}</h3>
<div>
<p data-plugin-install-steps class="text-body-md text-foreground-muted max-w-3xl">{client.steps}</p>
<div class="mt-4 flex flex-col items-start gap-3 sm:flex-row sm:items-center sm:gap-5">
<a href={client.download} download class="bg-accent text-on-accent hover:bg-foreground text-title-md inline-flex rounded-lg px-4 py-2.5 transition-colors duration-fast">{client.action}</a>
<a href={client.guide} class="text-body-md text-foreground underline underline-offset-2">Installation guide and checksum</a>
</div>
</div>
</li>
))}
</ul>
</div>
</section>
22 changes: 22 additions & 0 deletions apps/web/landing/src/components/PluginDownload.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
interface Props {
label: string;
version: string;
download: string;
metadata: string;
}
const { label, version, download, metadata } = Astro.props;
const filename = download.split("/").at(-1);
---

<div class="border-border bg-surface my-6 flex flex-col gap-4 rounded-xl border p-5 sm:flex-row sm:items-center sm:justify-between">
<div class="flex items-center gap-3">
<img src="/brand/exeora-mark-256-on-brand.png" alt="" width="48" height="48" class="size-12 rounded-lg" />
<div>
<p class="text-title-lg m-0!">{label}</p>
<p class="text-body-md text-foreground-muted m-0!">Version {version} · Remote MCP + 3 skills</p>
</div>
</div>
<a href={download} download={filename} class="bg-accent text-on-accent hover:bg-foreground text-title-md inline-flex justify-center rounded-lg px-5 py-3 no-underline! transition-colors duration-fast">Download {label} ZIP</a>
</div>
<p><a href={`${download}.sha256`}>SHA-256 checksum</a> · <a href={metadata}>Package details</a> · <a href="/docs/plugins/">All five distributions</a></p>
5 changes: 5 additions & 0 deletions apps/web/landing/src/lib/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ export const DOC_SECTIONS = [
{ href: "/docs/", label: "Getting started" },
{ href: "/docs/clients/", label: "Connecting a client" },
{ href: "/docs/plugin/", label: "Exeora plugin" },
{ href: "/docs/claude-plugin/", label: "Claude web plugin" },
{ href: "/docs/grokbot-cursor/", label: "GrokBot Cursor" },
{ href: "/docs/agent-plugin/", label: "Agent Plugin" },
{ href: "/docs/copilot-plugin/", label: "GitHub Copilot" },
{ href: "/docs/plugins/", label: "All plugin downloads" },
{ href: "/docs/projects/", label: "Projects, locations and workspaces" },
{ href: "/docs/github/", label: "Connecting GitHub" },
],
Expand Down
Loading
Loading