Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 7 additions & 1 deletion apps/web/landing/src/components/Hero.astro
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,14 @@ const promises = [
</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="/docs/plugins/" class="text-foreground underline-offset-2 hover:underline">All five plugin downloads</a>
</p>
</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
29 changes: 29 additions & 0 deletions apps/web/landing/src/pages/docs/agent-plugin.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
import manifest from "../../../../../../plugins/exeora-agent/plugin.json";
import {
AGENT_PLUGIN_DOWNLOAD,
AGENT_PLUGIN_METADATA,
} from "../../../../../../scripts/agent-plugin-bundle.js";
import PluginDownload from "../../components/PluginDownload.astro";
import Doc from "../../layouts/Doc.astro";
---

<Doc title="Agent Plugin" description="Download Exeora's pure Agent Plugins 1.0.0 package with portable skills and Streamable HTTP MCP, without provider extensions." canonicalPath="/docs/agent-plugin/">
<p><strong>Agent Plugin</strong> is Exeora's pure, portable distribution. Its root <code>plugin.json</code>, <code>skills/</code> and <code>mcp.json</code> follow Agent Plugins 1.0.0. The package identifier is <code>exeora-agent-plugin</code>; the visible channel label stays outside the standard manifest.</p>
<PluginDownload label="Agent Plugin" version={manifest.version} download={AGENT_PLUGIN_DOWNLOAD} metadata={AGENT_PLUGIN_METADATA} />

<h2 id="install">Use in a compatible client</h2>
<ol class="[&_li]:list-decimal!">
<li>Download and verify the ZIP. Follow your client's documented Agent Plugins 1.0.0 import workflow. If it takes a directory, extract into a dedicated folder with <code>plugin.json</code> directly at the root.</li>
<li>Confirm that the client discovers the three skills and supports <strong>Streamable HTTP</strong> MCP. Supporting skills alone is insufficient for this package's remote tools.</li>
<li>Use the client's own connection and authorization flow for <code>https://exeora.dev/mcp</code>, then verify access by listing authorized Exeora projects.</li>
</ol>

<h2 id="portable">What is portable</h2>
<p>The ZIP includes only standard manifest/MCP data, portable skills, README and license. It contains no provider extensions or directories, client-specific metadata, icon fields or native OAuth configuration. Project/workspace routing, user authorization, existing changes and Exeora policy remain part of every skill.</p>
<p>The <a href="https://agent-plugins.org/specification">standard</a> controls package discovery, not installation UI, marketplace, shared identity or embedded apps. Clients can support different components/transports; remote authentication remains client-managed. No universal ZIP importer or portable OAuth fields are defined by version 1.0.0.</p>

<h2 id="validation">Validation and limits</h2>
<p>Both JSON documents are checked against the <a href="https://agent-plugins.org/schemas">official Draft 2020-12 schemas</a> during every build, using pinned local copies. Additional distribution checks reject extensions, extra resources and credentials. Deterministic ZIP/checksum and regression tests preserve the other four distributions.</p>
<p>Schema conformance and a downloaded ZIP do not establish host installation, skill execution or real OAuth. Check those separately in each intended client. Exeora's selected workspace is remote; the host's local directory is not a substitute.</p>
</Doc>
Loading
Loading