mirror of
https://github.com/redis/agent-skills.git
synced 2026-09-19 01:25:14 +08:00
Add Codex plugin packaging
This commit is contained in:
@@ -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
@@ -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:
|
||||
|
||||
|
||||
@@ -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
@@ -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 |
@@ -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".
|
||||
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user