Add Codex plugin packaging

This commit is contained in:
DimoHG
2026-08-19 16:41:52 +03:00
parent f04e664b5f
commit 989eb057a6
8 changed files with 200 additions and 8 deletions
+5 -3
View File
@@ -1,6 +1,6 @@
# AGENTS.md
This file provides guidance to AI coding agents (Claude Code, Cursor, Copilot, etc.) when working with code in this repository.
This file provides guidance to AI coding agents (ChatGPT, Codex, Claude Code, Cursor, Copilot, etc.) when working with code in this repository.
## Repository Overview
@@ -17,7 +17,7 @@ A collection of agentskills.io-compliant skills for AI coding agents working wit
## Where Skills Live
`skills/` is the source of truth. `plugins/redis-development/skills/` holds **generated real copies** of it, committed to the repo, and `npm run sync:plugins` regenerates them.
`skills/` is the source of truth. `plugins/redis-development/skills/` holds **generated real copies** of it for the ChatGPT, Codex, and Claude Code plugin package, committed to the repo, and `npm run sync:plugins` regenerates them.
Copies rather than symlinks, because Claude Code and Cursor both drop a symlink that escapes the plugin root when a plugin is installed from git, so the plugin loads no skills at all. Real files also survive a Windows checkout, where git writes symlinks as plain text files unless `core.symlinks` is on.
@@ -59,6 +59,7 @@ Use [skills/redis-core/](skills/redis-core/) as the reference layout. Editorial
3. If the skill needs internal eval coverage, add `evals/<skill-name>/<suite-name>/{evals.json, model-matrix.json}` at the repo root, run the suite, and promote a baseline (`npm run eval:baseline`) — validation requires every suite to carry a current baseline.
4. Create `.cursor-plugin/plugin.json` (`name`, `version`, `description`, `license`, `keywords` — see any existing skill).
5. To publish via the marketplaces:
- ChatGPT and Codex: the sync vendors every skill into `plugins/redis-development/`; keep its `.codex-plugin/plugin.json` version aligned with the Claude manifest before submitting the package to OpenAI.
- Claude Code: nothing to wire up. The sync vendors every skill under `skills/`, so committing runs it and the directory's nightly bot picks the change up.
- Cursor: add an entry to `.cursor-plugin/marketplace.json` pointing at `<skill-name>`, then re-submit the repo at [cursor.com/marketplace/publish](https://cursor.com/marketplace/publish). Cursor does not pull from git.
6. Validate: `npm run validate` (covers plugin manifests, the vendored copies, eval baselines, and the agentskills.io spec).
@@ -69,7 +70,8 @@ Use [skills/redis-core/](skills/redis-core/) as the reference layout. Editorial
npm run validate # plugin manifests + vendored copies + eval baselines + agentskills.io spec
npm run validate:eval-baselines # every eval suite has a baseline matching its evals and matrix
npm run validate:skill-structure # spec validation only (across all skills)
npm run validate:plugins # claude + cursor plugin manifests + vendored copies
npm run validate:plugins # codex + claude + cursor plugin manifests + vendored copies
npm run validate:codex-plugins # Codex manifest, assets, metadata, and cross-manifest consistency
npm run validate:plugin-skills # vendored copies match skills/, with no symlinks
npm run sync:plugins # regenerate the vendored copies (the fix when the above fails)
```
+2 -2
View File
@@ -7,7 +7,7 @@ npm install
```
This installs dependencies and sets up the Husky pre-commit hook, which validates
the plugin manifests and eval baselines before every commit. Skill-structure
the Codex, Claude, and Cursor plugin manifests and eval baselines before every commit. Skill-structure
validation runs in CI, not in the hook.
Skill-structure validation additionally needs `skill-validator`, a Go binary that
@@ -25,7 +25,7 @@ with it.
## Source of Truth
`skills/` is where you edit. `plugins/redis-development/skills/` is generated from it: real copies, committed, because Claude Code and Cursor both drop symlinks that escape a plugin root when a plugin is installed from git, and because the Claude Code directory only notices an update when that subdirectory itself changes.
`skills/` is where you edit. `plugins/redis-development/skills/` is generated from it: real copies, committed for the ChatGPT, Codex, and Claude Code package because plugin installers may drop symlinks that escape a plugin root, and because the Claude Code directory only notices an update when that subdirectory itself changes.
Editing a skill therefore touches two paths, and the hook handles the second one:
+9 -1
View File
@@ -34,6 +34,14 @@ You can also install the skills as a Claude Code plugin:
/plugin install redis-development@redis
```
### ChatGPT and Codex Plugin
The distributable plugin package for ChatGPT and Codex lives at
[`plugins/redis-development/`](plugins/redis-development/). It includes the
required `.codex-plugin/plugin.json` manifest and real copies of every skill.
Archive that directory—not the repository root—when preparing a skills-only
submission to the OpenAI plugin submission portal.
### Cursor Plugin
This repository also includes Cursor plugin packaging. Run this command in chat:
@@ -42,7 +50,7 @@ This repository also includes Cursor plugin packaging. Run this command in chat:
/add-plugin redis
```
The top-level `skills/` directory remains the source of truth. `plugins/redis-development/skills/` holds generated real copies of it (not symlinks, which Claude Code and Cursor drop when a plugin is installed from git); `npm run sync:plugins` regenerates them and the pre-commit hook keeps them current. See [AGENTS.md](AGENTS.md#where-skills-live).
The top-level `skills/` directory remains the source of truth. `plugins/redis-development/skills/` holds generated real copies used by the ChatGPT, Codex, and Claude Code plugin package (not symlinks, which plugin installation flows may drop); `npm run sync:plugins` regenerates them and the pre-commit hook keeps them current. See [AGENTS.md](AGENTS.md#where-skills-live).
## Usage
+2 -1
View File
@@ -12,10 +12,11 @@
"validate": "npm run validate:plugins && npm run validate:eval-baselines && npm run validate:skill-structure",
"validate:eval-baselines": "node scripts/validate-eval-baselines.mjs",
"validate:skill-structure": "node scripts/validate-skill-structure.mjs --all --enforce",
"validate:codex-plugins": "node scripts/validate-codex-plugins.mjs",
"validate:claude-plugins": "node scripts/validate-claude-plugins.mjs",
"validate:cursor-plugins": "node scripts/validate-cursor-plugins.mjs",
"validate:plugin-skills": "node scripts/sync-plugin-skills.mjs --check",
"validate:plugins": "npm run validate:claude-plugins && npm run validate:cursor-plugins && npm run validate:plugin-skills",
"validate:plugins": "npm run validate:codex-plugins && npm run validate:claude-plugins && npm run validate:cursor-plugins && npm run validate:plugin-skills",
"prepare": "husky"
},
"skillValidatorVersion": "v1.5.6",
@@ -0,0 +1,41 @@
{
"name": "redis-development",
"version": "1.4.0",
"description": "Redis development best practices for data modeling, search, caching, connections, clustering, security, observability, and agent memory",
"author": {
"name": "Redis",
"email": "support@redis.com",
"url": "https://redis.io"
},
"homepage": "https://redis.io",
"repository": "https://github.com/redis/agent-skills",
"license": "MIT",
"keywords": [
"redis",
"database",
"caching",
"vector-search",
"performance",
"best-practices"
],
"skills": "./skills/",
"interface": {
"displayName": "Redis Development",
"shortDescription": "Build reliable, production-ready Redis applications",
"longDescription": "Design, implement, review, and troubleshoot Redis applications with focused guidance for data structures, connections, Redis Search, semantic caching, clustering, security, observability, and Iris agent memory.",
"developerName": "Redis",
"category": "Developer Tools",
"capabilities": [
"Read",
"Write"
],
"websiteURL": "https://redis.io",
"defaultPrompt": [
"Design a Redis data model for this feature.",
"Review this Redis code for correctness and performance.",
"Build a Redis Search schema and query for this application."
],
"brandColor": "#FF4438",
"logo": "./assets/redis-logo.png"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+2 -1
View File
@@ -1,7 +1,8 @@
#!/usr/bin/env node
// Vendors real copies of every skill from the top-level `skills/` source of
// truth into the Claude Code plugin. The plugin directory fetches only the
// truth into the ChatGPT, Codex, and Claude Code plugin package. The Claude
// Code plugin directory fetches only the
// `plugins/redis-development` subdirectory and advances our pinned commit only
// when that subdirectory's own contents change, so a skill has to exist there
// as real files to be published at all. See AGENTS.md, "Where Skills Live".
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env node
import { promises as fs } from "node:fs";
import path from "node:path";
import process from "node:process";
const repoRoot = process.cwd();
const pluginDir = path.join(repoRoot, "plugins", "redis-development");
const codexManifestPath = path.join(pluginDir, ".codex-plugin", "plugin.json");
const claudeManifestPath = path.join(pluginDir, ".claude-plugin", "plugin.json");
const errors = [];
const semverPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
function addError(message) {
errors.push(message);
}
async function readJson(filePath, label) {
try {
return JSON.parse(await fs.readFile(filePath, "utf8"));
} catch (error) {
addError(`${label} is missing or invalid (${path.relative(repoRoot, filePath)}): ${error.message}`);
return null;
}
}
function requireString(value, label) {
if (typeof value !== "string" || value.trim().length === 0) {
addError(`${label} must be a non-empty string.`);
return null;
}
return value;
}
function safePluginPath(value) {
if (typeof value !== "string" || !value.startsWith("./")) return null;
const resolved = path.resolve(pluginDir, value);
const relative = path.relative(pluginDir, resolved);
if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) return null;
return resolved;
}
async function validatePath(value, label) {
const resolved = safePluginPath(value);
if (resolved === null) {
addError(`${label} must start with "./" and stay inside the plugin root.`);
return;
}
try {
await fs.access(resolved);
} catch {
addError(`${label} references a missing path: ${value}`);
}
}
async function main() {
const [manifest, claudeManifest] = await Promise.all([
readJson(codexManifestPath, "Codex plugin manifest"),
readJson(claudeManifestPath, "Claude plugin manifest"),
]);
if (manifest === null || claudeManifest === null) return report();
if (JSON.stringify(manifest).includes("[TODO:")) {
addError("Codex plugin manifest contains an unfinished TODO placeholder.");
}
const name = requireString(manifest.name, "plugin.json name");
if (name !== path.basename(pluginDir)) {
addError(`plugin.json name must match the plugin directory (${path.basename(pluginDir)}).`);
}
const version = requireString(manifest.version, "plugin.json version");
if (version !== null && !semverPattern.test(version)) {
addError("plugin.json version must be strict semver.");
}
requireString(manifest.description, "plugin.json description");
requireString(manifest.author?.name, "plugin.json author.name");
if (manifest.skills !== "./skills/") {
addError('plugin.json skills must be "./skills/".');
} else {
await validatePath(manifest.skills, "plugin.json skills");
}
const interfaceMetadata = manifest.interface;
if (interfaceMetadata === null || typeof interfaceMetadata !== "object" || Array.isArray(interfaceMetadata)) {
addError("plugin.json interface must be an object.");
} else {
for (const field of ["displayName", "shortDescription", "longDescription", "developerName", "category"]) {
requireString(interfaceMetadata[field], `plugin.json interface.${field}`);
}
if (!Array.isArray(interfaceMetadata.capabilities) || interfaceMetadata.capabilities.length === 0 ||
!interfaceMetadata.capabilities.every((value) => typeof value === "string" && value.trim())) {
addError("plugin.json interface.capabilities must be a non-empty array of strings.");
}
if (!Array.isArray(interfaceMetadata.defaultPrompt) || interfaceMetadata.defaultPrompt.length === 0 ||
interfaceMetadata.defaultPrompt.length > 3) {
addError("plugin.json interface.defaultPrompt must contain one to three prompts.");
} else {
for (const [index, prompt] of interfaceMetadata.defaultPrompt.entries()) {
if (typeof prompt !== "string" || prompt.length === 0 || prompt.length > 128) {
addError(`plugin.json interface.defaultPrompt[${index}] must contain 1-128 characters.`);
}
}
}
for (const field of ["composerIcon", "logo", "logoDark"]) {
if (interfaceMetadata[field] !== undefined) {
await validatePath(interfaceMetadata[field], `plugin.json interface.${field}`);
}
}
if (interfaceMetadata.screenshots !== undefined) {
if (!Array.isArray(interfaceMetadata.screenshots)) {
addError("plugin.json interface.screenshots must be an array.");
} else {
for (const [index, screenshot] of interfaceMetadata.screenshots.entries()) {
await validatePath(screenshot, `plugin.json interface.screenshots[${index}]`);
}
}
}
}
for (const field of ["name", "version", "repository", "license"]) {
if (manifest[field] !== claudeManifest[field]) {
addError(`Codex and Claude plugin manifests must agree on ${field}.`);
}
}
report();
}
function report() {
if (errors.length > 0) {
console.error("Codex plugin validation failed:");
for (const error of errors) console.error(`- ${error}`);
process.exit(1);
}
console.log("Codex plugin validation passed.");
}
await main();