From 7d30e7883d2faf5bb9870ecff883860fbdd0a719 Mon Sep 17 00:00:00 2001 From: Steve Calvert Date: Wed, 21 Oct 2026 11:59:39 -0700 Subject: [PATCH 1/2] feat: publish Agent Plugins packages for Codex and Copilot to glean-plugins Add an agent-plugins target that builds each plugin once as an Agent Plugins package, with a marketplace for Codex and one for GitHub Copilot, and publish it to gleanwork/glean-plugins on release. - The packages match the codex target's: the same plugin.json (Codex's card under extensions["com.openai"]), mcp.json and skills. The Codex interface and entry values are now shared constants, so the two targets can't drift. - Copilot also gets the ten custom agents, as com.github.copilot/agents/NAME.agent.md. - overrides/agent-plugins/ holds READMEs with install steps for both clients, plus the Codex override's MCP config and assets. skill-creation-guide uses the shared version. - validate, the changelog check, publish.yml, README and RELEASE.md cover the new target. Requires pluginpack with agent-plugins clients support. --- .github/workflows/publish.yml | 3 + README.md | 7 +- RELEASE.md | 7 +- .../agent-plugins/glean-dev-docs/README.md | 44 ++++++ .../glean-dev-docs/assets/avatar.png | Bin 0 -> 2481 bytes overrides/agent-plugins/glean/LICENSE | 21 +++ overrides/agent-plugins/glean/README.md | 62 ++++++++ .../agent-plugins/glean/assets/avatar.png | Bin 0 -> 2481 bytes overrides/agent-plugins/glean/mcp/config.json | 12 ++ overrides/agent-plugins/root/LICENSE | 21 +++ overrides/agent-plugins/root/README.md | 73 ++++++++++ package.json | 2 +- pluginpack.config.ts | 132 +++++++++++------- scripts/check-plugin-changelogs.mjs | 2 + 14 files changed, 333 insertions(+), 53 deletions(-) create mode 100644 overrides/agent-plugins/glean-dev-docs/README.md create mode 100644 overrides/agent-plugins/glean-dev-docs/assets/avatar.png create mode 100644 overrides/agent-plugins/glean/LICENSE create mode 100644 overrides/agent-plugins/glean/README.md create mode 100644 overrides/agent-plugins/glean/assets/avatar.png create mode 100644 overrides/agent-plugins/glean/mcp/config.json create mode 100644 overrides/agent-plugins/root/LICENSE create mode 100644 overrides/agent-plugins/root/README.md diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d063682..57519fe 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -51,6 +51,9 @@ jobs: repo: cursor-plugins - target: codex repo: codex-plugins + # Agent Plugins packages with a Codex and a Copilot marketplace. + - target: agent-plugins + repo: glean-plugins concurrency: group: pluginpack-sync-${{ matrix.target }} cancel-in-progress: true diff --git a/README.md b/README.md index 996448b..a93e3af 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,10 @@ Nothing under `shared/` ships implicitly. Each emitted plugin names its shared - **Codex** — a marketplace + an [Agent Plugins](https://agent-plugins.org/specification) package for each plugin: a root `plugin.json` with Codex's install-surface metadata under `extensions["com.openai"]`, `mcp.json`, and bundled skills. +- **Agent Plugins** — the same portable packages for any client that supports + the format, with a marketplace for Codex (`.agents/plugins/marketplace.json`) + and GitHub Copilot (`.github/plugin/marketplace.json`). Copilot's custom + agents ship in each package's `com.github.copilot/agents/`. Skills use the open Agent Skills format — `SKILL.md` with `name`/`description` frontmatter and optional `references/` loaded on demand — which Claude Code, @@ -91,4 +95,5 @@ In this repo the skill markdown _is_ the shipped product, so: ### Releasing See [`RELEASE.md`](./RELEASE.md) for how to cut a release and how that flows -into the `claude-plugins`, `cursor-plugins`, and `codex-plugins` repos. +into the `claude-plugins`, `cursor-plugins`, `codex-plugins`, and +`glean-plugins` repos. diff --git a/RELEASE.md b/RELEASE.md index 3ed4aec..c5d6705 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -77,11 +77,12 @@ release is what triggers the actual plugin distribution (below). Publishing the GitHub Release triggers [`.github/workflows/publish.yml`](./.github/workflows/publish.yml), which: -1. Rebuilds and validates all 3 targets (`claude`, `cursor`, `codex`). +1. Rebuilds and validates all 4 targets (`claude`, `cursor`, `codex`, + `agent-plugins`). 2. Opens — or force-updates — one PR per target in that target's output repo (`gleanwork/claude-plugins`, `gleanwork/cursor-plugins`, - `gleanwork/codex-plugins`), on a machine-owned `pluginpack/sync-` - branch labeled `pluginpack-sync`. + `gleanwork/codex-plugins`, `gleanwork/glean-plugins`), on a machine-owned + `pluginpack/sync-` branch labeled `pluginpack-sync`. Those PRs are regenerated and force-pushed on every sync — **don't push to them by hand or hand-edit their content**. Review and merge each one in its diff --git a/overrides/agent-plugins/glean-dev-docs/README.md b/overrides/agent-plugins/glean-dev-docs/README.md new file mode 100644 index 0000000..5425689 --- /dev/null +++ b/overrides/agent-plugins/glean-dev-docs/README.md @@ -0,0 +1,44 @@ +# Glean Developer Docs + +Search the public [Glean developer documentation](https://docs.glean.com) — APIs, SDKs, +MCP, authentication, indexing, and integration guides for building *with* Glean. + +This is a separate, focused plugin for people building on the Glean platform. For +searching your own company's internal knowledge, use the main **Glean** plugin instead. + +## Setup + +### 1. Install the plugin + +Install `glean-dev-docs@glean-plugins` from the `gleanwork/glean-plugins` +marketplace in Codex or GitHub Copilot CLI. + +### 2. Configure the Glean Dev Docs MCP server + +This plugin uses the public Glean Developer Docs MCP server. No Glean account +is required. Add it with: + +```bash +# Codex +codex mcp add glean-dev-docs --url https://developers.glean.com/mcp + +# GitHub Copilot CLI +copilot mcp add --transport http glean-dev-docs https://developers.glean.com/mcp +``` + +Start a new session so the MCP tools are available. + +## What's Included + +A single skill that auto-triggers when you ask about building with Glean — API +syntax, SDK usage, MCP setup, indexing, authentication, and platform integration. +It searches the public developer documentation and returns cited answers. + +## Support + +- [Glean Developer Documentation](https://docs.glean.com) +- [GitHub Issues](https://github.com/gleanwork/agent-plugins/issues) + +## License + +MIT — see [LICENSE](LICENSE) for details. diff --git a/overrides/agent-plugins/glean-dev-docs/assets/avatar.png b/overrides/agent-plugins/glean-dev-docs/assets/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..14e232fae59dbd6b4f55236db116b9d77014ee2a GIT binary patch literal 2481 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 **Generated repository.** Built from +> [gleanwork/agent-plugins](https://github.com/gleanwork/agent-plugins) via +> [pluginpack](https://github.com/gleanwork/pluginpack). Don't hand-edit managed +> files here — changes are made in `agent-plugins` and synced through generated +> pull requests. + +Official Glean plugins in the [Agent Plugins](https://agent-plugins.org/specification) +format — enterprise knowledge, search, people, code, meetings, and Glean +developer docs. One package per plugin works in every client that supports the +format. + +## Install + +### Codex + +```bash +codex plugin marketplace add gleanwork/glean-plugins +codex plugin add glean@glean-plugins +codex plugin add glean-dev-docs@glean-plugins +``` + +### GitHub Copilot CLI + +```bash +copilot plugin marketplace add gleanwork/glean-plugins +copilot plugin install glean@glean-plugins +copilot plugin install glean-dev-docs@glean-plugins +``` + +Then start a new session and prompt the harness to set up Glean, for example: +`Set up Glean for me`. Complete the browser OAuth flow when prompted. + +The **Glean developer docs** plugin uses a separate public MCP server. If you +installed that plugin, add it before starting a new session: + +```bash +codex mcp add glean-dev-docs --url https://developers.glean.com/mcp +copilot mcp add --transport http glean-dev-docs https://developers.glean.com/mcp +``` + +## Plugins + +| Plugin | Description | +|--------|-------------| +| **[glean](plugins/glean)** | Enterprise knowledge — search docs/Slack/email, cross-repo code exploration, people & experts, meetings, onboarding, and productivity. | +| **[glean-dev-docs](plugins/glean-dev-docs)** | Search Glean's public developer documentation — APIs, SDKs, MCP, and integration guides. | + +## Layout + +- `plugins//` — one Agent Plugins package per plugin: `plugin.json`, + `mcp.json`, and `skills/`. Copilot's custom agents are in + `plugins/glean/com.github.copilot/agents/`. +- `.agents/plugins/marketplace.json` — the Codex marketplace. +- `.github/plugin/marketplace.json` — the Copilot marketplace. + +## Requirements + +- A client that supports Agent Plugins, such as + [Codex](https://developers.openai.com/codex) or + [GitHub Copilot CLI](https://docs.github.com/copilot/how-tos/copilot-cli) +- A Glean account with MCP access (for the `glean` plugin) + +## Support + +- [Glean MCP Documentation](https://docs.glean.com/mcp) +- [Glean Support](https://help.glean.com) +- [GitHub Issues](https://github.com/gleanwork/agent-plugins/issues) + +## License + +MIT License — see [LICENSE](LICENSE) for details. diff --git a/package.json b/package.json index 7ec25ce..4aeceef 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "prune": "pluginpack prune", "typecheck:bundle": "tsc --noEmit -p shared/glean/mcp/tsconfig.json", "test:bundle": "vitest run --root shared/glean/mcp", - "validate": "pluginpack validate --target claude --dir dist/claude && pluginpack validate --target cursor --dir dist/cursor && pluginpack validate --target codex --dir dist/codex", + "validate": "pluginpack validate --target claude --dir dist/claude && pluginpack validate --target cursor --dir dist/cursor && pluginpack validate --target codex --dir dist/codex && pluginpack validate --target agent-plugins --dir dist/agent-plugins", "test": "npm run build && npm run validate", "prebuild": "node scripts/sync-plugin-changelogs.mjs && npm run build:bundle", "release": "release-it" diff --git a/pluginpack.config.ts b/pluginpack.config.ts index 49be212..f9e2522 100644 --- a/pluginpack.config.ts +++ b/pluginpack.config.ts @@ -4,6 +4,55 @@ import pkg from "./package.json" with { type: "json" }; const gleanPluginDescription = "Use Glean to search your company's knowledge, chat with Glean Assistant, and discover and use the skills and connected tools available to your organization."; +const devDocsDescription = + "Search the public Glean developer documentation — APIs, SDKs, MCP, and integration guides for building with Glean."; + +// Codex's plugin card. The codex target moves it under +// extensions["com.openai"] itself; the agent-plugins target needs it there +// explicitly, because one package serves several clients. +const gleanCodexInterface = { + displayName: "Glean", + shortDescription: "Enterprise knowledge in Codex", + longDescription: + "Search enterprise documents, Slack, email, code, and people; prepare for meetings; and synthesize trusted company knowledge in Codex.", + developerName: "Glean", + category: "Productivity", + capabilities: ["Read", "Search"], + defaultPrompt: [ + "Search Glean for the latest project decision.", + "Find an expert on this codebase.", + "Prepare me for my next meeting.", + ], + composerIcon: "./assets/avatar.png", + logo: "./assets/avatar.png", +}; + +const devDocsCodexInterface = { + displayName: "Glean Developer Docs", + shortDescription: "Build with Glean in Codex", + longDescription: + "Search public Glean API, SDK, MCP, authentication, and integration documentation while you build in Codex.", + developerName: "Glean", + category: "Productivity", + capabilities: ["Read", "Search"], + defaultPrompt: [ + "How do I authenticate with the Glean API?", + "Find the Glean Indexing API documentation.", + "Show me a Glean SDK example.", + ], + composerIcon: "./assets/avatar.png", + logo: "./assets/avatar.png", +}; + +// Codex marketplace entry fields pluginpack can't derive. +const codexEntry = { + policy: { + installation: "AVAILABLE", + authentication: "ON_INSTALL", + }, + category: "Productivity", +}; + export default defineConfig({ name: "glean-plugins", version: pkg.version, @@ -115,64 +164,51 @@ export default defineConfig({ source: "shared/glean", overrides: "overrides/codex/glean", description: gleanPluginDescription, + manifest: { interface: gleanCodexInterface }, + entry: codexEntry, + }, + "glean-dev-docs": { + source: "shared/glean-dev-docs", + overrides: "overrides/codex/glean-dev-docs", + include: ["skills", "assets", "static"], + description: devDocsDescription, manifest: { - interface: { - displayName: "Glean", - shortDescription: "Enterprise knowledge in Codex", - longDescription: - "Search enterprise documents, Slack, email, code, and people; prepare for meetings; and synthesize trusted company knowledge in Codex.", - developerName: "Glean", - category: "Productivity", - capabilities: ["Read", "Search"], - defaultPrompt: [ - "Search Glean for the latest project decision.", - "Find an expert on this codebase.", - "Prepare me for my next meeting.", - ], - composerIcon: "./assets/avatar.png", - logo: "./assets/avatar.png", - }, + homepage: "https://developers.glean.com/", + interface: devDocsCodexInterface, }, - entry: { - policy: { - installation: "AVAILABLE", - authentication: "ON_INSTALL", - }, - category: "Productivity", + entry: codexEntry, + }, + }, + }, + // One Agent Plugins package per plugin, published to gleanwork/glean-plugins + // with a marketplace for each listed client. + "agent-plugins": { + outDir: "dist/agent-plugins", + repositoryFiles: "overrides/agent-plugins/root", + clients: { + codex: { manifest: { interface: { displayName: "Glean" } } }, + copilot: {}, + }, + plugins: { + glean: { + source: "shared/glean", + overrides: "overrides/agent-plugins/glean", + description: gleanPluginDescription, + manifest: { + extensions: { "com.openai": { interface: gleanCodexInterface } }, }, + clients: { codex: { entry: codexEntry } }, }, "glean-dev-docs": { source: "shared/glean-dev-docs", - overrides: "overrides/codex/glean-dev-docs", + overrides: "overrides/agent-plugins/glean-dev-docs", include: ["skills", "assets", "static"], - description: - "Search the public Glean developer documentation — APIs, SDKs, MCP, and integration guides for building with Glean.", + description: devDocsDescription, manifest: { homepage: "https://developers.glean.com/", - interface: { - displayName: "Glean Developer Docs", - shortDescription: "Build with Glean in Codex", - longDescription: - "Search public Glean API, SDK, MCP, authentication, and integration documentation while you build in Codex.", - developerName: "Glean", - category: "Productivity", - capabilities: ["Read", "Search"], - defaultPrompt: [ - "How do I authenticate with the Glean API?", - "Find the Glean Indexing API documentation.", - "Show me a Glean SDK example.", - ], - composerIcon: "./assets/avatar.png", - logo: "./assets/avatar.png", - }, - }, - entry: { - policy: { - installation: "AVAILABLE", - authentication: "ON_INSTALL", - }, - category: "Productivity", + extensions: { "com.openai": { interface: devDocsCodexInterface } }, }, + clients: { codex: { entry: codexEntry } }, }, }, }, diff --git a/scripts/check-plugin-changelogs.mjs b/scripts/check-plugin-changelogs.mjs index fc915c4..ce76568 100644 --- a/scripts/check-plugin-changelogs.mjs +++ b/scripts/check-plugin-changelogs.mjs @@ -16,6 +16,8 @@ const paths = [ "dist/cursor/glean-dev-docs/CHANGELOG.md", "dist/codex/plugins/glean/CHANGELOG.md", "dist/codex/plugins/glean-dev-docs/CHANGELOG.md", + "dist/agent-plugins/plugins/glean/CHANGELOG.md", + "dist/agent-plugins/plugins/glean-dev-docs/CHANGELOG.md", ]; const mismatches = paths.filter((relativePath) => { From c924e743e8453f149fb710a65682dfef90e6a06a Mon Sep 17 00:00:00 2001 From: Steve Calvert Date: Thu, 22 Oct 2026 09:55:43 -0700 Subject: [PATCH 2/2] test: install the Agent Plugins output in Codex and Copilot Add npm run e2e:install. It installs both plugins from a local build or a GitHub marketplace ref in Codex and GitHub Copilot CLI, each with a throwaway home. It then checks the skills, the Copilot agents, and that the Glean MCP server answers initialize and tools/list from each client's resolved config. RELEASE.md now runs it against the glean-plugins sync branch before a release. --- RELEASE.md | 14 ++ package.json | 1 + scripts/e2e-install.mjs | 398 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 413 insertions(+) create mode 100644 scripts/e2e-install.mjs diff --git a/RELEASE.md b/RELEASE.md index c5d6705..77dc63f 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -38,6 +38,20 @@ release is what triggers the actual plugin distribution (below). npm install ``` +- Test the Agent Plugins output in real clients before you release. Sync + without a release, then install from the `glean-plugins` sync branch: + + ```sh + gh workflow run publish.yml --repo gleanwork/agent-plugins -f mode=sync + npm run e2e:install -- --source gleanwork/glean-plugins --ref pluginpack/sync-agent-plugins + ``` + + `e2e:install` installs both plugins in Codex and GitHub Copilot CLI, each + with a throwaway home, and checks the skills, the Copilot agents and that + the Glean MCP server lists its tools. Run it with no options to test a + local `npm run build` instead. It doesn't sign in to Glean, so also check a + signed-in session by hand. + ## Release - `release-it`'s GitHub plugin needs a token with `repo` scope to create the diff --git a/package.json b/package.json index 4aeceef..779e3aa 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "build": "pluginpack build && npm run check:plugin-changelogs", "build:bundle": "node shared/glean/mcp/build.mjs", "clean": "pluginpack clean", + "e2e:install": "node scripts/e2e-install.mjs", "check:no-shell": "node scripts/check-no-shell-scripts.mjs", "check:plugin-changelogs": "node scripts/check-plugin-changelogs.mjs", "check:release-version": "node scripts/check-release-version.mjs", diff --git a/scripts/e2e-install.mjs b/scripts/e2e-install.mjs new file mode 100644 index 0000000..4c086d8 --- /dev/null +++ b/scripts/e2e-install.mjs @@ -0,0 +1,398 @@ +#!/usr/bin/env node +// @ts-check +// Install the Agent Plugins output in real clients and check what each one +// registered. Every client runs with its own throwaway home, so your own +// client config and Glean credentials are never read or changed. +// +// node scripts/e2e-install.mjs --source dist/agent-plugins +// node scripts/e2e-install.mjs --source gleanwork/glean-plugins --ref pluginpack/sync-agent-plugins +// +// Options: +// --source Marketplace source. Default: dist/agent-plugins +// --ref Git ref for a GitHub source +// --clients Comma-separated. Default: codex,copilot +// --keep Keep the temporary homes for inspection +// +// Checks, per client: the marketplace adds, both plugins install, the glean +// plugin has its skills (and, in Copilot, its agents), and the Glean MCP server +// starts from the client's resolved config and answers initialize + tools/list. +// It does not sign in to Glean or call a tool; do that in a real session. +import { spawn, spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const MARKETPLACE = "glean-plugins"; +const PLUGINS = ["glean", "glean-dev-docs"]; +const MCP_SERVER = "glean_plugin"; +const MCP_TIMEOUT_MS = 30_000; + +const args = parseArgs(process.argv.slice(2)); +const source = looksLocal(args.source) + ? path.resolve(args.source) + : args.source; +const clients = args.clients.split(",").map((c) => c.trim()); + +/** @type {{ client: string, check: string, ok: boolean, detail: string }[]} */ +const results = []; +const homes = []; + +for (const client of clients) { + const home = fs.mkdtempSync(path.join(os.tmpdir(), `e2e-${client}-`)); + homes.push(home); + if (client === "codex") await runCodex(home); + else if (client === "copilot") await runCopilot(home); + else record(client, "known client", false, `unknown client "${client}"`); +} + +report(); +if (!args.keep) + for (const home of homes) fs.rmSync(home, { recursive: true, force: true }); +else console.log(`\nKept: ${homes.join(" ")}`); +process.exit(results.every((r) => r.ok) ? 0 : 1); + +// --- clients --------------------------------------------------------------- + +/** @param {string} home */ +async function runCodex(home) { + const env = { ...process.env, CODEX_HOME: home }; + const add = ["plugin", "marketplace", "add", source]; + if (args.ref) add.push("--ref", args.ref); + if (!step("codex", "marketplace add", "codex", add, env)) return; + for (const plugin of PLUGINS) { + step( + "codex", + `install ${plugin}`, + "codex", + ["plugin", "add", `${plugin}@${MARKETPLACE}`], + env, + ); + } + + const root = newestDir( + path.join(home, "plugins", "cache", MARKETPLACE, "glean"), + ); + checkPackage("codex", root, { agents: false }); + + const list = run("codex", ["mcp", "list", "--json"], env); + const server = list.ok + ? JSON.parse(list.stdout).find((s) => s.name === MCP_SERVER) + : undefined; + if (!server) + return record( + "codex", + "MCP server registered", + false, + list.stderr || "not listed", + ); + record( + "codex", + "MCP server registered", + true, + `${server.transport.command} ${server.transport.args.join(" ")}`, + ); + await checkMcp("codex", server.transport, server.transport.cwd, home); +} + +/** @param {string} home */ +async function runCopilot(home) { + const env = { ...process.env, COPILOT_HOME: home }; + const [bin, ...pre] = copilotCommand(); + const src = + args.ref && !looksLocal(args.source) ? `${source}#${args.ref}` : source; + if ( + !step( + "copilot", + "marketplace add", + bin, + [...pre, "plugin", "marketplace", "add", src], + env, + ) + ) + return; + for (const plugin of PLUGINS) { + step( + "copilot", + `install ${plugin}`, + bin, + [...pre, "plugin", "install", `${plugin}@${MARKETPLACE}`], + env, + ); + } + + // A local marketplace is loaded in place; a GitHub one is copied. + const installed = path.join(home, "installed-plugins", MARKETPLACE, "glean"); + const root = fs.existsSync(installed) + ? installed + : looksLocal(args.source) + ? path.join(source, "plugins", "glean") + : undefined; + checkPackage("copilot", root, { agents: true }); + + const list = run(bin, [...pre, "mcp", "list", "--json"], env); + const server = list.ok + ? JSON.parse(list.stdout).mcpServers?.[MCP_SERVER] + : undefined; + if (!server) + return record( + "copilot", + "MCP server registered", + false, + list.stderr || "not listed", + ); + record( + "copilot", + "MCP server registered", + true, + `${server.command} ${server.args.join(" ")}`, + ); + await checkMcp("copilot", server, server.env?.PLUGIN_ROOT ?? root, home); +} + +function copilotCommand() { + const onPath = + spawnSync("copilot", ["--version"], { stdio: "ignore" }).status === 0; + return onPath ? ["copilot"] : ["npx", "-y", "@github/copilot"]; +} + +// --- checks ---------------------------------------------------------------- + +/** + * @param {string} client + * @param {string | undefined} root + * @param {{ agents: boolean }} opts + */ +function checkPackage(client, root, opts) { + if (!root || !fs.existsSync(path.join(root, "plugin.json"))) { + return record( + client, + "glean package installed", + false, + `no plugin.json under ${root}`, + ); + } + record(client, "glean package installed", true, root); + const skills = countFiles(path.join(root, "skills"), (f) => f === "SKILL.md"); + record(client, "glean skills present", skills > 0, `${skills} skills`); + if (opts.agents) { + const dir = path.join(root, "com.github.copilot", "agents"); + const agents = countFiles(dir, (f) => f.endsWith(".agent.md")); + record(client, "Copilot agents present", agents > 0, `${agents} agents`); + } +} + +/** + * Start the server the way the client would and ask it for its tools. + * HOME points at the throwaway home so a fallback data dir never reaches + * your real ~/.glean. + * @param {string} client + * @param {{ command: string, args: string[], env?: Record }} server + * @param {string | undefined} cwd + * @param {string} home + */ +async function checkMcp(client, server, cwd, home) { + const env = { + ...process.env, + ...(server.env ?? {}), + HOME: home, + USERPROFILE: home, + }; + try { + const tools = await listTools(server.command, server.args, { cwd, env }); + record( + client, + "MCP initialize + tools/list", + tools.length > 0, + `${tools.length} tools`, + ); + } catch (err) { + record( + client, + "MCP initialize + tools/list", + false, + String(err instanceof Error ? err.message : err), + ); + } +} + +/** + * @param {string} command + * @param {string[]} argv + * @param {{ cwd?: string, env: NodeJS.ProcessEnv }} opts + * @returns {Promise} + */ +function listTools(command, argv, opts) { + return new Promise((resolve, reject) => { + const child = spawn(command, argv, { + ...opts, + stdio: ["pipe", "pipe", "pipe"], + }); + let buffer = ""; + let stderr = ""; + const timer = setTimeout( + () => + finish( + new Error( + `no tools/list reply in ${MCP_TIMEOUT_MS}ms. ${stderr.trim().slice(-300)}`, + ), + ), + MCP_TIMEOUT_MS, + ); + /** @param {Error | null} err @param {string[]} [tools] */ + function finish(err, tools) { + clearTimeout(timer); + child.kill(); + if (err) reject(err); + else resolve(tools ?? []); + } + /** @param {object} msg */ + const send = (msg) => + child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", ...msg })}\n`); + + child.on("error", (err) => finish(err)); + child.on("exit", (code) => + finish( + new Error(`server exited with ${code}. ${stderr.trim().slice(-300)}`), + ), + ); + child.stderr.on("data", (d) => (stderr += d)); + child.stdout.on("data", (d) => { + buffer += d; + let nl; + while ((nl = buffer.indexOf("\n")) >= 0) { + const line = buffer.slice(0, nl).trim(); + buffer = buffer.slice(nl + 1); + if (!line) continue; + let msg; + try { + msg = JSON.parse(line); + } catch { + continue; + } + if (msg.id === 1) { + if (msg.error) + return finish(new Error(`initialize: ${msg.error.message}`)); + send({ method: "notifications/initialized" }); + send({ id: 2, method: "tools/list" }); + } else if (msg.id === 2) { + if (msg.error) + return finish(new Error(`tools/list: ${msg.error.message}`)); + finish( + null, + (msg.result?.tools ?? []).map((t) => t.name), + ); + } + } + }); + + send({ + id: 1, + method: "initialize", + params: { + protocolVersion: "2025-06-18", + capabilities: {}, + clientInfo: { name: "e2e-install", version: "0" }, + }, + }); + }); +} + +// --- helpers --------------------------------------------------------------- + +/** + * Run a CLI step and record it. + * @param {string} client @param {string} check @param {string} bin + * @param {string[]} argv @param {NodeJS.ProcessEnv} env + */ +function step(client, check, bin, argv, env) { + const r = run(bin, argv, env); + const out = (r.stdout + r.stderr) + .split("\n") + .filter((l) => l && !l.startsWith("npm warn")); + record(client, check, r.ok, out.at(-1) ?? ""); + return r.ok; +} + +/** @param {string} bin @param {string[]} argv @param {NodeJS.ProcessEnv} env */ +function run(bin, argv, env) { + const r = spawnSync(bin, argv, { + env, + encoding: "utf8", + cwd: os.tmpdir(), + timeout: 300_000, + }); + return { + ok: r.status === 0, + stdout: r.stdout ?? "", + stderr: (r.stderr ?? "") + (r.error ? String(r.error) : ""), + }; +} + +/** @param {string} client @param {string} check @param {boolean} ok @param {string} detail */ +function record(client, check, ok, detail) { + results.push({ client, check, ok, detail }); +} + +function report() { + const width = Math.max( + ...results.map((r) => `${r.client} ${r.check}`.length), + ); + console.log(`Source: ${source}${args.ref ? ` @ ${args.ref}` : ""}\n`); + for (const r of results) { + const label = `${r.client} ${r.check}`.padEnd(width); + console.log(`${r.ok ? "PASS" : "FAIL"} ${label} ${r.detail}`); + } + const failed = results.filter((r) => !r.ok).length; + console.log(`\n${results.length - failed} passed, ${failed} failed`); +} + +/** @param {string} dir */ +function newestDir(dir) { + if (!fs.existsSync(dir)) return undefined; + const entries = fs + .readdirSync(dir, { withFileTypes: true }) + .filter((e) => e.isDirectory()) + .map((e) => path.join(dir, e.name)) + .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs); + return entries[0]; +} + +/** @param {string} dir @param {(name: string) => boolean} match */ +function countFiles(dir, match) { + if (!fs.existsSync(dir)) return 0; + let n = 0; + for (const e of fs.readdirSync(dir, { + withFileTypes: true, + recursive: true, + })) { + if (e.isFile() && match(e.name)) n++; + } + return n; +} + +/** @param {string} s */ +function looksLocal(s) { + return s.startsWith(".") || s.startsWith("/") || fs.existsSync(s); +} + +/** @param {string[]} argv */ +function parseArgs(argv) { + const out = { + source: "dist/agent-plugins", + ref: "", + clients: "codex,copilot", + keep: false, + }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === "--keep") out.keep = true; + else if (a === "--source") out.source = argv[++i]; + else if (a === "--ref") out.ref = argv[++i]; + else if (a === "--clients") out.clients = argv[++i]; + else { + console.error(`Unknown argument: ${a}`); + process.exit(2); + } + } + return out; +}