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..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 @@ -77,11 +91,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 0000000..14e232f Binary files /dev/null and b/overrides/agent-plugins/glean-dev-docs/assets/avatar.png differ diff --git a/overrides/agent-plugins/glean/LICENSE b/overrides/agent-plugins/glean/LICENSE new file mode 100644 index 0000000..eb02a81 --- /dev/null +++ b/overrides/agent-plugins/glean/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Glean + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/overrides/agent-plugins/glean/README.md b/overrides/agent-plugins/glean/README.md new file mode 100644 index 0000000..51216f8 --- /dev/null +++ b/overrides/agent-plugins/glean/README.md @@ -0,0 +1,62 @@ +# Glean + +Official Glean plugin in the [Agent Plugins](https://agent-plugins.org/specification) +format — enterprise search, code exploration, and people discovery directly in +your development workflow. Works in Codex and GitHub Copilot CLI. + +## Setup + +### 1. Install the plugin + +Codex: + +```bash +codex plugin marketplace add gleanwork/glean-plugins +codex plugin add glean@glean-plugins +``` + +GitHub Copilot CLI: + +```bash +copilot plugin marketplace add gleanwork/glean-plugins +copilot plugin install glean@glean-plugins +``` + +### 2. Set up Glean + +Start a new session and prompt the harness: `Set up Glean for me`. Complete +the browser OAuth flow when prompted, then start a new session so both the +bundled skills and Glean MCP tools are available. + +## What's Included + +The plugin ships a library of skills that auto-trigger by task — there's no +per-skill install. They cover: + +- **Enterprise search & knowledge** — find documents, Slack messages, and email; vet results for freshness and authority. +- **Code across repos** — explore implementations, find usage examples and similar code, identify code owners, and gather architectural context. +- **People & org** — find experts by contribution, and identify stakeholders for a change or project. +- **Meetings** — prep for upcoming meetings and catch up on what you missed. +- **Onboarding & projects** — ramp up on a team or area, read quick project status, and generate comprehensive project handoffs. +- **Personal productivity** — summarize your own activity, prep status updates, and surface what needs your attention. +- **Skill authoring** — discover automation opportunities and generate new skills. + +In Copilot, the plugin also adds custom agents for focused research tasks, +such as people discovery and meeting analysis. + +## 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 + +## 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 — see [LICENSE](LICENSE) for details. diff --git a/overrides/agent-plugins/glean/assets/avatar.png b/overrides/agent-plugins/glean/assets/avatar.png new file mode 100644 index 0000000..14e232f Binary files /dev/null and b/overrides/agent-plugins/glean/assets/avatar.png differ diff --git a/overrides/agent-plugins/glean/mcp/config.json b/overrides/agent-plugins/glean/mcp/config.json new file mode 100644 index 0000000..fc1ae4b --- /dev/null +++ b/overrides/agent-plugins/glean/mcp/config.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "glean_plugin": { + "command": "node", + "args": ["./mcp/start.mjs"], + "env": { + "ENABLE_HITL": "true", + "HITL_TIMEOUT_MS": "300000" + } + } + } +} diff --git a/overrides/agent-plugins/root/LICENSE b/overrides/agent-plugins/root/LICENSE new file mode 100644 index 0000000..eb02a81 --- /dev/null +++ b/overrides/agent-plugins/root/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Glean + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/overrides/agent-plugins/root/README.md b/overrides/agent-plugins/root/README.md new file mode 100644 index 0000000..b6a04dd --- /dev/null +++ b/overrides/agent-plugins/root/README.md @@ -0,0 +1,73 @@ +# Glean Plugins + +> **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..779e3aa 100644 --- a/package.json +++ b/package.json @@ -13,13 +13,14 @@ "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", "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) => { 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; +}