From c2bfe544be29a38e18e98ce6b1c50976fe58004a Mon Sep 17 00:00:00 2001 From: Akshay Dodeja Date: Wed, 22 Jul 2026 11:51:54 -0700 Subject: [PATCH 1/2] feat: bootstrap Terminal49 agent plugin marketplace --- .agents/plugins/marketplace.json | 20 +++ .claude-plugin/marketplace.json | 18 +++ .cursor-plugin/marketplace.json | 18 +++ .github/workflows/validate.yml | 22 +++ .gitignore | 3 + AGENTS.md | 36 +++++ CONTRIBUTING.md | 23 +++ README.md | 107 +++++++++++++- package-lock.json | 16 +++ package.json | 13 ++ plugins/terminal49/.claude-plugin/plugin.json | 22 +++ plugins/terminal49/.codex-plugin/plugin.json | 35 +++++ plugins/terminal49/.cursor-plugin/plugin.json | 23 +++ plugins/terminal49/.mcp.json | 8 ++ plugins/terminal49/README.md | 23 +++ plugins/terminal49/assets/logo.svg | 4 + plugins/terminal49/mcp.json | 8 ++ .../terminal49/skills/terminal49-mcp/SKILL.md | 131 +++++++++++++++++ scripts/validate.mjs | 133 ++++++++++++++++++ 19 files changed, 661 insertions(+), 2 deletions(-) create mode 100644 .agents/plugins/marketplace.json create mode 100644 .claude-plugin/marketplace.json create mode 100644 .cursor-plugin/marketplace.json create mode 100644 .github/workflows/validate.yml create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 CONTRIBUTING.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 plugins/terminal49/.claude-plugin/plugin.json create mode 100644 plugins/terminal49/.codex-plugin/plugin.json create mode 100644 plugins/terminal49/.cursor-plugin/plugin.json create mode 100644 plugins/terminal49/.mcp.json create mode 100644 plugins/terminal49/README.md create mode 100644 plugins/terminal49/assets/logo.svg create mode 100644 plugins/terminal49/mcp.json create mode 100644 plugins/terminal49/skills/terminal49-mcp/SKILL.md create mode 100644 scripts/validate.mjs diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..782c6eb --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,20 @@ +{ + "name": "terminal49", + "interface": { + "displayName": "Terminal49" + }, + "plugins": [ + { + "name": "terminal49", + "source": { + "source": "local", + "path": "./plugins/terminal49" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + } + ] +} diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..cdcd185 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,18 @@ +{ + "name": "terminal49", + "owner": { + "name": "Terminal49" + }, + "metadata": { + "description": "Terminal49 container-tracking tools and agent skills", + "version": "0.1.0" + }, + "plugins": [ + { + "name": "terminal49", + "source": "./plugins/terminal49", + "description": "Use Terminal49 MCP tools to track and investigate container shipments", + "version": "0.1.0" + } + ] +} diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json new file mode 100644 index 0000000..cdcd185 --- /dev/null +++ b/.cursor-plugin/marketplace.json @@ -0,0 +1,18 @@ +{ + "name": "terminal49", + "owner": { + "name": "Terminal49" + }, + "metadata": { + "description": "Terminal49 container-tracking tools and agent skills", + "version": "0.1.0" + }, + "plugins": [ + { + "name": "terminal49", + "source": "./plugins/terminal49", + "description": "Use Terminal49 MCP tools to track and investigate container shipments", + "version": "0.1.0" + } + ] +} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..24de736 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,22 @@ +name: Validate plugins + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci --ignore-scripts + - run: npm run validate diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a7a460b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +node_modules/ +npm-debug.log* diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..911af22 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,36 @@ +# Agent instructions + +This is a public, cross-platform plugin marketplace for Terminal49. + +## Safety + +- Never commit API keys, OAuth tokens, internal URLs, customer identifiers, or + customer shipment data. +- Use placeholders or clearly illustrative identifiers in examples. +- Keep the production connector URL exactly + `https://mcp.terminal49.com`. Do not add `/mcp` to it; the origin is the OAuth + resource identifier. +- Do not ask users to paste credentials into chat. Let the client initiate its + OAuth connection flow. + +## Shared source of truth + +- `plugins/terminal49/skills/terminal49-mcp/SKILL.md` is the shared behavioral + guidance for all clients. +- `plugins/terminal49/.mcp.json` and `plugins/terminal49/mcp.json` are the MCP + adapters for Claude/Codex and Cursor respectively. Keep their server entries + identical. +- `.cursor-plugin`, `.claude-plugin`, and `.codex-plugin` manifests are thin + platform adapters. Keep their names, versions, descriptions, and paths in + sync. +- Marketplace files must continue to point to `./plugins/terminal49`. + +## Validation + +Run `npm run validate` after every change. When a supported client is installed, +also use its native validator before publishing. + +## Releases + +Bump the version in all three plugin manifests and both versioned marketplace +files for every release. Keep releases small and document user-visible changes. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..80a8beb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# Contributing + +Thanks for improving the Terminal49 agent plugins. + +## Principles + +1. Keep the shared skill client-neutral. +2. Prefer tool-selection guidance over duplicating the Terminal49 API docs. +3. Treat `track_container` as a write: search first and create a tracking + request only when the user intends to start tracking. +4. Preserve the distinction between missing data, unsupported features, and + operational states. +5. Never include secrets or real customer shipment data. + +## Make a change + +1. Update the shared skill or the smallest applicable platform adapter. +2. Keep manifest metadata and versions synchronized. +3. Run `npm run validate`. +4. Test installation in any client affected by the change. + +Platform-specific behavior belongs in its adapter. Guidance that applies to all +clients belongs in `plugins/terminal49/skills/terminal49-mcp/SKILL.md`. diff --git a/README.md b/README.md index 0f190e4..48035b4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,105 @@ -# agent-plugins- -Terminal49 container tracking an execution Agent Skills and tools for Claude, ChatGPT, CoPilot and other LLMs +# Terminal49 agent plugins + +Install Terminal49 container-tracking tools and workflow guidance in Cursor, +Claude Code, Codex, and GitHub Copilot CLI. + +This marketplace currently publishes one plugin: + +- **Terminal49** — connects to the hosted Terminal49 MCP server and teaches + agents how to search, track, and investigate container shipments. + +## What the plugin provides + +- OAuth connection to `https://mcp.terminal49.com` +- A shared `terminal49-mcp` skill for choosing and sequencing MCP tools +- Guidance for status, pickup readiness, holds, ETAs, routes, delays, and + demurrage-risk questions +- Guardrails around tracking-request creation, credentials, dates, and missing + data + +The plugin does not contain API keys or customer data. Authentication happens +through the MCP client's OAuth flow. + +## Install + +### Cursor + +In Cursor chat, run: + +```text +/add-plugin https://github.com/dodeja/agent-plugins +``` + +Then select and install **Terminal49**. Cursor will prompt you to connect the +MCP server when the plugin first needs it. + +### Claude Code + +```sh +claude plugin marketplace add dodeja/agent-plugins +claude plugin install terminal49@terminal49 +``` + +### Codex + +```sh +codex plugin marketplace add dodeja/agent-plugins +codex plugin add terminal49@terminal49 +``` + +### GitHub Copilot CLI + +Copilot CLI can read the Claude-compatible marketplace included in this +repository: + +```sh +copilot plugin marketplace add dodeja/agent-plugins +copilot plugin install terminal49@terminal49 +``` + +## Try it + +After connecting your Terminal49 account, ask your agent: + +- “Where is container CAIU2885402?” +- “Is this container ready for pickup, and are there any holds?” +- “Explain what changed in this container's journey.” +- “Show containers updated since yesterday and group them by status.” + +The example identifier is illustrative. Results depend on the shipments visible +to the authenticated Terminal49 account. + +## Repository layout + +```text +. +├── .agents/plugins/marketplace.json # Codex marketplace +├── .claude-plugin/marketplace.json # Claude Code and Copilot marketplace +├── .cursor-plugin/marketplace.json # Cursor marketplace +└── plugins/terminal49/ + ├── .claude-plugin/plugin.json + ├── .codex-plugin/plugin.json + ├── .cursor-plugin/plugin.json + ├── .mcp.json # Claude and Codex MCP adapter + ├── mcp.json # Cursor MCP adapter + └── skills/terminal49-mcp/SKILL.md +``` + +The skill is shared. Each platform-specific manifest and MCP file is a thin +adapter around that shared content, with validation keeping the endpoint in +sync. + +## Development + +Requires Node.js 22 or newer. + +```sh +npm run validate +``` + +See [CONTRIBUTING.md](CONTRIBUTING.md) before changing the shared skill or +platform manifests. + +## License + +Apache-2.0. See [LICENSE](LICENSE). diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..409d6a3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,16 @@ +{ + "name": "terminal49-agent-plugins", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "terminal49-agent-plugins", + "version": "0.1.0", + "license": "Apache-2.0", + "engines": { + "node": ">=22" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..523910b --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "terminal49-agent-plugins", + "version": "0.1.0", + "private": true, + "description": "Cross-platform Terminal49 agent plugin marketplace", + "license": "Apache-2.0", + "engines": { + "node": ">=22" + }, + "scripts": { + "validate": "node scripts/validate.mjs" + } +} diff --git a/plugins/terminal49/.claude-plugin/plugin.json b/plugins/terminal49/.claude-plugin/plugin.json new file mode 100644 index 0000000..2fbad1b --- /dev/null +++ b/plugins/terminal49/.claude-plugin/plugin.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", + "name": "terminal49", + "displayName": "Terminal49", + "version": "0.1.0", + "description": "Track and investigate container shipments with Terminal49 MCP tools and workflow guidance.", + "author": { + "name": "Terminal49" + }, + "homepage": "https://terminal49.com", + "repository": "https://github.com/dodeja/agent-plugins", + "license": "Apache-2.0", + "keywords": [ + "container-tracking", + "logistics", + "mcp", + "ocean-freight", + "supply-chain" + ], + "skills": "./skills/", + "mcpServers": "./.mcp.json" +} diff --git a/plugins/terminal49/.codex-plugin/plugin.json b/plugins/terminal49/.codex-plugin/plugin.json new file mode 100644 index 0000000..035056e --- /dev/null +++ b/plugins/terminal49/.codex-plugin/plugin.json @@ -0,0 +1,35 @@ +{ + "name": "terminal49", + "version": "0.1.0", + "description": "Track and investigate container shipments with Terminal49 MCP tools and workflow guidance.", + "author": { + "name": "Terminal49" + }, + "homepage": "https://terminal49.com", + "repository": "https://github.com/dodeja/agent-plugins", + "license": "Apache-2.0", + "keywords": [ + "container-tracking", + "logistics", + "mcp", + "ocean-freight", + "supply-chain" + ], + "skills": "./skills/", + "mcpServers": "./.mcp.json", + "interface": { + "displayName": "Terminal49", + "shortDescription": "Track and investigate container shipments", + "longDescription": "Connect to Terminal49 and use guided workflows for container status, pickup readiness, holds, ETAs, routes, delays, and demurrage risk.", + "developerName": "Terminal49", + "category": "Productivity", + "capabilities": [ + "Interactive", + "Write" + ], + "websiteURL": "https://terminal49.com", + "defaultPrompt": [ + "Track a container or investigate the latest status of my shipments" + ] + } +} diff --git a/plugins/terminal49/.cursor-plugin/plugin.json b/plugins/terminal49/.cursor-plugin/plugin.json new file mode 100644 index 0000000..b889e5f --- /dev/null +++ b/plugins/terminal49/.cursor-plugin/plugin.json @@ -0,0 +1,23 @@ +{ + "name": "terminal49", + "displayName": "Terminal49", + "version": "0.1.0", + "description": "Track and investigate container shipments with Terminal49 MCP tools and workflow guidance.", + "author": { + "name": "Terminal49" + }, + "homepage": "https://terminal49.com", + "repository": "https://github.com/dodeja/agent-plugins", + "license": "Apache-2.0", + "logo": "assets/logo.svg", + "keywords": [ + "container-tracking", + "logistics", + "mcp", + "ocean-freight", + "supply-chain" + ], + "category": "Productivity", + "skills": "./skills/", + "mcpServers": "./mcp.json" +} diff --git a/plugins/terminal49/.mcp.json b/plugins/terminal49/.mcp.json new file mode 100644 index 0000000..19aedb0 --- /dev/null +++ b/plugins/terminal49/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "terminal49": { + "type": "http", + "url": "https://mcp.terminal49.com" + } + } +} diff --git a/plugins/terminal49/README.md b/plugins/terminal49/README.md new file mode 100644 index 0000000..3e8dccd --- /dev/null +++ b/plugins/terminal49/README.md @@ -0,0 +1,23 @@ +# Terminal49 plugin + +This plugin connects an agent to Terminal49's hosted MCP server and supplies a +shared skill for reliable container-tracking workflows. + +## Authentication + +The MCP client should open an OAuth authorization flow when Terminal49 tools are +first used. Authenticate in the browser and return to the client. Do not paste +API keys, access tokens, or one-time codes into chat. + +## Included skill + +`terminal49-mcp` covers: + +- finding an existing container or shipment from a business identifier +- starting a new tracking request when the user intends to do so +- inspecting container, shipment, terminal, event, and route details +- querying fleets with filters and pagination +- presenting dates, holds, missing information, and paid-feature boundaries + +The Terminal49 API and MCP implementation live in +[Terminal49/API](https://github.com/Terminal49/API). diff --git a/plugins/terminal49/assets/logo.svg b/plugins/terminal49/assets/logo.svg new file mode 100644 index 0000000..36175f2 --- /dev/null +++ b/plugins/terminal49/assets/logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/terminal49/mcp.json b/plugins/terminal49/mcp.json new file mode 100644 index 0000000..19aedb0 --- /dev/null +++ b/plugins/terminal49/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "terminal49": { + "type": "http", + "url": "https://mcp.terminal49.com" + } + } +} diff --git a/plugins/terminal49/skills/terminal49-mcp/SKILL.md b/plugins/terminal49/skills/terminal49-mcp/SKILL.md new file mode 100644 index 0000000..f17421d --- /dev/null +++ b/plugins/terminal49/skills/terminal49-mcp/SKILL.md @@ -0,0 +1,131 @@ +--- +name: terminal49-mcp +description: Use Terminal49 MCP tools to find, track, and investigate ocean container shipments. Use when a user asks about container status, shipment details, pickup readiness, terminal holds, ETAs, transport events, routes, delays, tracking requests, carrier support, or demurrage risk. +--- + +# Terminal49 MCP + +Use the Terminal49 MCP server for container and shipment questions. Prefer the +smallest tool sequence that answers the question, and preserve the difference +between observed data and inference. + +## Before using tools + +1. Confirm that Terminal49 MCP tools are available. +2. If the client requests authentication, let it start the OAuth browser flow. +3. Never ask the user to paste an API key, access token, password, or one-time + code into chat. +4. If the tools remain unavailable, say that the Terminal49 connection must be + enabled and authenticated. Do not invent shipment data. + +## Identifier rules + +Users normally provide a container number, bill of lading, booking number, or +reference number. Detail tools require a Terminal49 UUID. + +- Use `search_container` to resolve a user-facing identifier to a container or + shipment UUID. +- Normalize obvious whitespace and casing, but preserve the identifier in the + response. +- If search returns several plausible matches, show the distinguishing fields + and ask the user to choose. Do not select silently. +- If search returns no match, do not immediately create a tracking request. + Explain that the identifier is not currently visible and offer to start + tracking it. + +## Search before creating + +`track_container` can create a tracking request. Treat it as a write operation. + +1. Call `search_container` first unless the user explicitly asks to add or start + tracking a new identifier and has already confirmed it is not tracked. +2. Call `track_container` only when the user intends to start tracking. +3. Prefer the `number` field. Supply `numberType` only when the type is known, + and `scac` only when known or confirmed. +4. If the carrier is uncertain, use `get_supported_shipping_lines` to resolve a + name or SCAC rather than guessing. +5. Report whether a tracking request was created and whether results may still + be pending. + +## Choose the right tool + +| User intent | Tool sequence | +| --- | --- | +| Find a container, BL, booking, or reference | `search_container` | +| Start tracking a new identifier | `search_container`, then `track_container` after intent is clear | +| Current status or basic container details | `search_container`, then `get_container` with `shipment` | +| Pickup readiness, terminal availability, holds, or LFD | `search_container`, then `get_container` with `shipment` and `pod_terminal` | +| What happened, where it moved, or delay analysis | `search_container`, then `get_container_transport_events` | +| Full shipment and its containers | `search_container`, then `get_shipment_details` | +| Multi-leg route, transshipment, vessels, or itinerary | `search_container`, then `get_container_route` | +| Containers or shipments matching operational filters | `list_containers` or `list_shipments` | +| Tracking-request status or audit | `list_tracking_requests` | +| Supported carrier or SCAC lookup | `get_supported_shipping_lines` | + +Use the `terminal49://docs/mcp-query-guidance` resource when available for the +server's current intent-to-tool playbooks. Use +`terminal49://docs/milestone-glossary` when event terminology needs explanation. + +## Efficient detail loading + +For `get_container`, request only the related data needed: + +- `shipment` for routing, bill of lading, carrier, and reference context +- `pod_terminal` for pickup availability, terminal state, holds, and demurrage + questions +- `transport_events` only when the full event history is needed; prefer + `get_container_transport_events` when the question is purely about the + timeline + +Do not call every detail tool by default. Follow up only when the first result +leaves a material question unanswered. + +## Fleet queries + +- Use `list_containers` for container-level operational views and + `list_shipments` for shipment-level views. +- Apply filters supplied by the user: `status`, POD `port` LOCODE, carrier SCAC, + and `updated_after` in ISO 8601 form. +- Use pagination instead of silently assuming the first page is complete. +- State the filters and time boundary used in the answer. + +## Interpret and present results + +1. Lead with the current answer: status, location, readiness, or risk. +2. Separate actual timestamps from estimated timestamps and label each clearly. +3. Preserve source time zones. If converting a time, state the target time zone. +4. Call out terminal holds, customs issues, and pickup blockers explicitly. +5. Treat missing dates as unknown, not as evidence that an event did not occur. +6. Treat `FeatureNotEnabled` from `get_container_route` as an account capability + boundary, not as missing route data. Use available shipment and event data as + the alternative. +7. For multi-row results, use a compact table and honor any response-contract + presentation guidance returned by the server. +8. End with at most two useful next checks when important information is + incomplete. + +## Common workflows + +### Status or pickup readiness + +1. Resolve the identifier with `search_container`. +2. Call `get_container` with `shipment` and `pod_terminal`. +3. Summarize current status, terminal, availability, holds, and relevant dates. +4. If the state is unclear, call `get_container_transport_events` for context. + +### Delay investigation + +1. Resolve the identifier. +2. Call `get_container_transport_events`. +3. Order events chronologically and compare estimates with actuals. +4. Identify the last confirmed movement and the first material divergence. +5. Describe likely causes as hypotheses unless the data explicitly names one. + +### Demurrage-risk review + +1. Use `list_containers` with the narrowest known operational filters. +2. Fetch `get_container` with `pod_terminal` for the candidates that need detail. +3. Prioritize known pickup LFDs, active terminal holds, discharge state, and + availability. +4. Do not calculate fees unless the required tariff and timing inputs are + present. Label any risk assessment separately from an actual charge. diff --git a/scripts/validate.mjs b/scripts/validate.mjs new file mode 100644 index 0000000..50e319f --- /dev/null +++ b/scripts/validate.mjs @@ -0,0 +1,133 @@ +import { readFile, access } from "node:fs/promises"; +import { resolve } from "node:path"; +import process from "node:process"; + +const root = process.cwd(); +const errors = []; +const connectorUrl = "https://mcp.terminal49.com"; + +const paths = { + cursorMarketplace: ".cursor-plugin/marketplace.json", + claudeMarketplace: ".claude-plugin/marketplace.json", + codexMarketplace: ".agents/plugins/marketplace.json", + cursorPlugin: "plugins/terminal49/.cursor-plugin/plugin.json", + claudePlugin: "plugins/terminal49/.claude-plugin/plugin.json", + codexPlugin: "plugins/terminal49/.codex-plugin/plugin.json", + mcp: "plugins/terminal49/.mcp.json", + cursorMcp: "plugins/terminal49/mcp.json", + skill: "plugins/terminal49/skills/terminal49-mcp/SKILL.md", +}; + +async function readJson(relativePath) { + try { + const source = await readFile(resolve(root, relativePath), "utf8"); + const value = JSON.parse(source); + const formatted = `${JSON.stringify(value, null, 2)}\n`; + if (source !== formatted) { + errors.push(`${relativePath} is not consistently formatted`); + } + return value; + } catch (error) { + errors.push(`${relativePath}: ${error.message}`); + return null; + } +} + +async function requireFile(relativePath) { + try { + await access(resolve(root, relativePath)); + } catch { + errors.push(`${relativePath} is missing`); + } +} + +const [cursorMarketplace, claudeMarketplace, codexMarketplace, cursorPlugin, claudePlugin, codexPlugin, mcp, cursorMcp] = + await Promise.all([ + readJson(paths.cursorMarketplace), + readJson(paths.claudeMarketplace), + readJson(paths.codexMarketplace), + readJson(paths.cursorPlugin), + readJson(paths.claudePlugin), + readJson(paths.codexPlugin), + readJson(paths.mcp), + readJson(paths.cursorMcp), + ]); + +await Promise.all(["README.md", "LICENSE", "AGENTS.md", "CONTRIBUTING.md", paths.skill].map(requireFile)); + +const plugins = [cursorPlugin, claudePlugin, codexPlugin].filter(Boolean); +const names = new Set(plugins.map((plugin) => plugin.name)); +const versions = new Set(plugins.map((plugin) => plugin.version)); +const descriptions = new Set(plugins.map((plugin) => plugin.description)); + +if (plugins.length !== 3) errors.push("all three plugin manifests must parse"); +if (names.size !== 1 || !names.has("terminal49")) errors.push("plugin manifest names must all be terminal49"); +if (versions.size !== 1) errors.push("plugin manifest versions must match"); +if (descriptions.size !== 1) errors.push("plugin manifest descriptions must match"); + +for (const [label, marketplace] of [ + ["Cursor", cursorMarketplace], + ["Claude", claudeMarketplace], +]) { + if (!marketplace) continue; + if (marketplace.name !== "terminal49") errors.push(`${label} marketplace name must be terminal49`); + const entry = marketplace.plugins?.find((plugin) => plugin.name === "terminal49"); + if (!entry) { + errors.push(`${label} marketplace is missing terminal49`); + continue; + } + if (entry.source !== "./plugins/terminal49") errors.push(`${label} marketplace source is incorrect`); + if (entry.version !== plugins[0]?.version) errors.push(`${label} marketplace version must match plugin version`); +} + +const codexEntry = codexMarketplace?.plugins?.find((plugin) => plugin.name === "terminal49"); +if (!codexEntry) { + errors.push("Codex marketplace is missing terminal49"); +} else { + if (codexEntry.source?.source !== "local" || codexEntry.source?.path !== "./plugins/terminal49") { + errors.push("Codex marketplace source is incorrect"); + } + if (codexEntry.policy?.installation !== "AVAILABLE") errors.push("Codex installation policy must be AVAILABLE"); + if (codexEntry.policy?.authentication !== "ON_INSTALL") errors.push("Codex authentication policy must be ON_INSTALL"); +} + +for (const [label, plugin] of [ + ["Claude", claudePlugin], + ["Codex", codexPlugin], +]) { + if (!plugin) continue; + if (plugin.skills !== "./skills/") errors.push(`${label} plugin must use the shared skills directory`); + if (plugin.mcpServers !== "./.mcp.json") errors.push(`${label} plugin must use the shared MCP config`); + if (plugin.license !== "Apache-2.0") errors.push(`${label} plugin license must be Apache-2.0`); +} + +if (cursorPlugin?.skills !== "./skills/") errors.push("Cursor plugin must use the shared skills directory"); +if (cursorPlugin?.mcpServers !== "./mcp.json") errors.push("Cursor plugin must use the Cursor MCP config"); +if (cursorPlugin?.license !== "Apache-2.0") errors.push("Cursor plugin license must be Apache-2.0"); + +if (mcp?.mcpServers?.terminal49?.type !== "http") errors.push("Terminal49 MCP transport must be http"); +if (mcp?.mcpServers?.terminal49?.url !== connectorUrl) { + errors.push(`Terminal49 MCP URL must be exactly ${connectorUrl}`); +} +if (JSON.stringify(cursorMcp) !== JSON.stringify(mcp)) { + errors.push("Cursor and Claude/Codex MCP configurations must match"); +} + +try { + const skill = await readFile(resolve(root, paths.skill), "utf8"); + if (!skill.startsWith("---\n")) errors.push("skill must start with YAML frontmatter"); + if (!/^name: terminal49-mcp$/m.test(skill)) errors.push("skill name must be terminal49-mcp"); + if (!/^description: .+$/m.test(skill)) errors.push("skill must have a description"); + if (!skill.includes("search_container")) errors.push("skill must explain search_container"); + if (!skill.includes("track_container")) errors.push("skill must explain track_container"); +} catch (error) { + errors.push(`${paths.skill}: ${error.message}`); +} + +if (errors.length > 0) { + console.error("Plugin validation failed:\n"); + for (const error of errors) console.error(`- ${error}`); + process.exit(1); +} + +console.log(`Validated Terminal49 plugin ${plugins[0].version} for Cursor, Claude Code, Codex, and Copilot CLI.`); From b2409cf38c6c74c755a8da925795c2f937954e05 Mon Sep 17 00:00:00 2001 From: Akshay Dodeja Date: Wed, 22 Jul 2026 13:07:55 -0700 Subject: [PATCH 2/2] fix: enforce review findings in validator and docs - Validate metadata.version in both versioned marketplace files - Enforce manifest sync for displayName, homepage, repository, author, keywords - Enforce connector URL has no path suffix across docs and skill - Require Cursor logo asset and plugin README to exist - Add displayName to Codex manifest; use clearly fake example container ID - Document CI workflow and clarify manifest sync scope in AGENTS.md Co-Authored-By: Claude Fable 5 --- AGENTS.md | 12 ++++-- README.md | 7 ++- plugins/terminal49/.codex-plugin/plugin.json | 1 + scripts/validate.mjs | 45 ++++++++++++++++++-- 4 files changed, 57 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 911af22..c0fd10e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,14 +21,18 @@ This is a public, cross-platform plugin marketplace for Terminal49. adapters for Claude/Codex and Cursor respectively. Keep their server entries identical. - `.cursor-plugin`, `.claude-plugin`, and `.codex-plugin` manifests are thin - platform adapters. Keep their names, versions, descriptions, and paths in - sync. + platform adapters. Keep their shared fields in sync: `name`, `displayName`, + `version`, `description`, `author`, `homepage`, `repository`, `license`, + `keywords`, and the skills/MCP paths (`npm run validate` enforces this). + Platform-only fields may diverge: Cursor's `logo` and `category`, and Codex's + `interface` block (its `displayName` must still match the manifest). - Marketplace files must continue to point to `./plugins/terminal49`. ## Validation -Run `npm run validate` after every change. When a supported client is installed, -also use its native validator before publishing. +Run `npm run validate` after every change. CI runs the same script on every +push and pull request (`.github/workflows/validate.yml`). When a supported +client is installed, also use its native validator before publishing. ## Releases diff --git a/README.md b/README.md index 48035b4..6e3d436 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ copilot plugin install terminal49@terminal49 After connecting your Terminal49 account, ask your agent: -- “Where is container CAIU2885402?” +- “Where is container ABCU1234567?” - “Is this container ready for pickup, and are there any holds?” - “Explain what changed in this container's journey.” - “Show containers updated since yesterday and group them by status.” @@ -82,6 +82,8 @@ to the authenticated Terminal49 account. ├── .cursor-plugin/plugin.json ├── .mcp.json # Claude and Codex MCP adapter ├── mcp.json # Cursor MCP adapter + ├── README.md + ├── assets/logo.svg # Referenced by the Cursor manifest └── skills/terminal49-mcp/SKILL.md ``` @@ -97,6 +99,9 @@ Requires Node.js 22 or newer. npm run validate ``` +CI runs the same validation on every push and pull request via +[`.github/workflows/validate.yml`](.github/workflows/validate.yml). + See [CONTRIBUTING.md](CONTRIBUTING.md) before changing the shared skill or platform manifests. diff --git a/plugins/terminal49/.codex-plugin/plugin.json b/plugins/terminal49/.codex-plugin/plugin.json index 035056e..674668e 100644 --- a/plugins/terminal49/.codex-plugin/plugin.json +++ b/plugins/terminal49/.codex-plugin/plugin.json @@ -1,5 +1,6 @@ { "name": "terminal49", + "displayName": "Terminal49", "version": "0.1.0", "description": "Track and investigate container shipments with Terminal49 MCP tools and workflow guidance.", "author": { diff --git a/scripts/validate.mjs b/scripts/validate.mjs index 50e319f..2975eec 100644 --- a/scripts/validate.mjs +++ b/scripts/validate.mjs @@ -53,17 +53,35 @@ const [cursorMarketplace, claudeMarketplace, codexMarketplace, cursorPlugin, cla readJson(paths.cursorMcp), ]); -await Promise.all(["README.md", "LICENSE", "AGENTS.md", "CONTRIBUTING.md", paths.skill].map(requireFile)); +await Promise.all( + ["README.md", "LICENSE", "AGENTS.md", "CONTRIBUTING.md", "plugins/terminal49/README.md", paths.skill].map(requireFile), +); const plugins = [cursorPlugin, claudePlugin, codexPlugin].filter(Boolean); +const pluginVersion = claudePlugin?.version ?? cursorPlugin?.version ?? codexPlugin?.version; const names = new Set(plugins.map((plugin) => plugin.name)); const versions = new Set(plugins.map((plugin) => plugin.version)); const descriptions = new Set(plugins.map((plugin) => plugin.description)); +const displayNames = new Set(plugins.map((plugin) => plugin.displayName)); +const homepages = new Set(plugins.map((plugin) => plugin.homepage)); +const repositories = new Set(plugins.map((plugin) => plugin.repository)); +const authors = new Set(plugins.map((plugin) => plugin.author?.name)); +const keywords = new Set(plugins.map((plugin) => JSON.stringify(plugin.keywords))); if (plugins.length !== 3) errors.push("all three plugin manifests must parse"); if (names.size !== 1 || !names.has("terminal49")) errors.push("plugin manifest names must all be terminal49"); if (versions.size !== 1) errors.push("plugin manifest versions must match"); if (descriptions.size !== 1) errors.push("plugin manifest descriptions must match"); +if (displayNames.size !== 1 || !displayNames.has("Terminal49")) { + errors.push("plugin manifest displayNames must all be Terminal49"); +} +if (homepages.size !== 1) errors.push("plugin manifest homepages must match"); +if (repositories.size !== 1) errors.push("plugin manifest repositories must match"); +if (authors.size !== 1) errors.push("plugin manifest author names must match"); +if (keywords.size !== 1) errors.push("plugin manifest keywords must match"); +if (codexPlugin && codexPlugin.interface?.displayName !== codexPlugin.displayName) { + errors.push("Codex interface displayName must match the manifest displayName"); +} for (const [label, marketplace] of [ ["Cursor", cursorMarketplace], @@ -77,7 +95,12 @@ for (const [label, marketplace] of [ continue; } if (entry.source !== "./plugins/terminal49") errors.push(`${label} marketplace source is incorrect`); - if (entry.version !== plugins[0]?.version) errors.push(`${label} marketplace version must match plugin version`); + if (pluginVersion) { + if (entry.version !== pluginVersion) errors.push(`${label} marketplace version must match plugin version`); + if (marketplace.metadata?.version !== pluginVersion) { + errors.push(`${label} marketplace metadata.version must match plugin version`); + } + } } const codexEntry = codexMarketplace?.plugins?.find((plugin) => plugin.name === "terminal49"); @@ -113,6 +136,20 @@ if (JSON.stringify(cursorMcp) !== JSON.stringify(mcp)) { errors.push("Cursor and Claude/Codex MCP configurations must match"); } +if (cursorPlugin?.logo) await requireFile(`plugins/terminal49/${cursorPlugin.logo}`); + +for (const docPath of ["README.md", "CONTRIBUTING.md", "AGENTS.md", "plugins/terminal49/README.md", paths.skill]) { + try { + const text = await readFile(resolve(root, docPath), "utf8"); + const suffixed = text.match(/mcp\.terminal49\.com\/[^\s)`"']*/); + if (suffixed) { + errors.push(`${docPath} must reference ${connectorUrl} with no path (found ${suffixed[0]})`); + } + } catch { + // missing docs are reported by requireFile above + } +} + try { const skill = await readFile(resolve(root, paths.skill), "utf8"); if (!skill.startsWith("---\n")) errors.push("skill must start with YAML frontmatter"); @@ -130,4 +167,6 @@ if (errors.length > 0) { process.exit(1); } -console.log(`Validated Terminal49 plugin ${plugins[0].version} for Cursor, Claude Code, Codex, and Copilot CLI.`); +console.log( + `Validated Terminal49 plugin ${pluginVersion} for Cursor, Claude Code, and Codex. Copilot CLI installs via the Claude marketplace.`, +);